luff 0.2.1

Print files with formatting
Documentation
//! Estimations of io metrics for progress indicators, etc.

/// Conservative average bytes per file for estimation purposes.
///
/// This is a middle ground that works well for most projects:
/// - Small files (configs, headers): 500–1000 bytes
/// - Medium files (source code): 1500–3000 bytes
/// - Large files: Will exceed estimate, but buffer helps
const AVG_BYTES_PER_FILE: usize = 1536;

/// Estimate output size based on file count
///
/// Uses a conservative fixed estimate per file that maintains monotonicity
/// and prevents discontinuities at arbitrary boundaries.
///
/// # Performance
///
/// - O(1) computation time (simple multiplication)
/// - Returns conservative estimate (adds 30% buffer for markdown overhead)
///
/// # Arguments
///
/// * `total_files` - Total number of files to be processed
///
/// # Returns
///
/// Estimated total output size in bytes. Guaranteed to be:
/// - Zero for zero files
/// - Monotonically increasing (more files = larger estimate)
/// - Overflow-safe (uses saturating arithmetic)
///
/// # Mathematical Properties
///
/// For all n > 0:
/// - `estimate(0) = 0`
/// - `estimate(n) >= estimate(n-1)` (monotonicity)
/// - `estimate(n) = n * AVG_BYTES_PER_FILE * 4/3` (33% buffer)
pub const fn estimate_output_size(total_files: usize) -> usize {
    if total_files == 0 {
        return 0;
    }

    // Add 30% buffer for markdown formatting overhead
    // Formula: base + base/3 = base * (1 + 1/3) = base * 4/3
    let base_estimate = total_files.saturating_mul(AVG_BYTES_PER_FILE);
    base_estimate.saturating_add(base_estimate / 3)
}

#[cfg(test)]
mod tests {
    use super::*;
    use proptest::prelude::*;

    proptest! {
        /// Property: Estimation should never overflow (use saturating arithmetic)
        ///
        /// This test verifies that the function completes without panicking,
        /// which proves that saturating arithmetic is working correctly.
        #[test]
        fn prop_never_overflows(total_files in 0usize..1_000_000) {
            let _result = estimate_output_size(total_files);
            // If we reach here without panic, saturating arithmetic worked correctly
        }

        /// Property: More files should always result in equal or larger estimate
        /// (Strict monotonicity: estimate(n) >= estimate(n-1))
        #[test]
        fn prop_monotonic_increase(n in 1usize..10_000) {
            let current = estimate_output_size(n);
            let previous = estimate_output_size(n.saturating_sub(1));

            assert!(
                current >= previous,
                "Estimate should be monotonically increasing: estimate({n}) = {current} should be >= estimate({}) = {previous}",
                n - 1
            );
        }

        /// Property: Buffer should add exactly 33% (1/3) overhead
        /// Formula: base * (1 + 1/3) = base * 4/3
        #[test]
        fn prop_buffer_factor_correct(total_files in 1usize..10_000) {
            let estimate = estimate_output_size(total_files);

            let expected_base = total_files.saturating_mul(AVG_BYTES_PER_FILE);
            let expected_with_buffer = expected_base.saturating_add(expected_base / 3);

            assert_eq!(
                estimate, expected_with_buffer,
                "Buffer calculation mismatch for {total_files} files: got {estimate}, expected {expected_with_buffer}"
            );
        }

        /// Property: Zero files should always return zero
        #[test]
        fn prop_zero_files_zero_estimate(_dummy in 0usize..100) {
            assert_eq!(estimate_output_size(0), 0);
        }

        /// Property: Estimate should scale linearly with file count
        /// Since we use a constant per-file estimate, doubling files should double output
        #[test]
        fn prop_scales_linearly(n in 1usize..5_000) {
            let single = estimate_output_size(n);
            let doubled = estimate_output_size(n.saturating_mul(2));

            // For linear scaling: estimate(2n) should be approximately 2 * estimate(n)
            // We allow for small rounding differences due to integer division in buffer calc
            let expected_doubled = single.saturating_mul(2);
            let diff = doubled.abs_diff(expected_doubled);

            // Allow up to 1% rounding error
            let tolerance = expected_doubled / 100;
            assert!(
                diff <= tolerance,
                "Linear scaling violated: estimate({n}) = {single}, estimate({})={doubled}, expected≈{expected_doubled}, diff={diff}",
                n * 2
            );
        }

        /// Property: Estimate should be strictly positive for any non-zero file count
        #[test]
        fn prop_positive_for_nonzero(total_files in 1usize..100_000) {
            let estimate = estimate_output_size(total_files);
            assert!(
                estimate > 0,
                "Estimate should be positive for {total_files} files, got {estimate}"
            );
        }
    }

    #[test]
    fn test_single_file_exact_value() {
        // 1 file * 1536 bytes * 4/3 = 1536 + 512 = 2048
        assert_eq!(estimate_output_size(1), 2048);
    }

    #[test]
    fn test_thousand_files_exact_value() {
        // 1000 * 1536 = 1_536_000; buffer = 512_000; total = 2_048_000
        assert_eq!(estimate_output_size(1000), 2_048_000);
    }

    #[test]
    fn test_saturates_at_usize_max() {
        // Ensure we saturate instead of panicking at extreme inputs
        assert_eq!(
            estimate_output_size(usize::MAX),
            usize::MAX,
            "Should saturate to usize::MAX for extreme input"
        );
    }

    #[test]
    fn test_module_level_constant_value() {
        assert_eq!(AVG_BYTES_PER_FILE, 1536);
    }
}