1
use std::fmt;
2

            
3
/// Formats bytes into human-readable format using decimal units (GB, MB, KB, bytes)
4
///
5
/// Note: This uses decimal units (1 KB = 1000 bytes) rather than binary units (1 KiB = 1024 bytes).
6
pub struct BytesFormatter(pub usize);
7

            
8
impl fmt::Display for BytesFormatter {
9
1009
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10
        const KB: usize = 1_000;
11
        const MB: usize = 1_000_000;
12
        const GB: usize = 1_000_000_000;
13

            
14
        // Choose appropriate unit based on size for best readability
15
1009
        if self.0 >= GB {
16
911
            write!(f, "{:.2} GB", self.0 as f64 / GB as f64)
17
98
        } else if self.0 >= MB {
18
92
            write!(f, "{:.2} MB", self.0 as f64 / MB as f64)
19
6
        } else if self.0 >= KB {
20
3
            write!(f, "{:.2} KB", self.0 as f64 / KB as f64)
21
        } else {
22
3
            write!(f, "{} bytes", self.0)
23
        }
24
1009
    }
25
}
26

            
27
pub struct LargeFormatter<T: ToString>(pub T);
28

            
29
impl<T: ToString> fmt::Display for LargeFormatter<T> {
30
1007
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31
1007
        let num_str = self.0.to_string();
32

            
33
        // Handle an optional leading '-' sign separately so comma positions are
34
        // calculated only over the digit characters.
35
1007
        let (sign, digits) = if let Some(stripped) = num_str.strip_prefix('-') {
36
475
            ("-", stripped)
37
        } else {
38
532
            ("", num_str.as_str())
39
        };
40

            
41
1007
        if !sign.is_empty() {
42
475
            write!(f, "{sign}")?;
43
532
        }
44

            
45
1007
        let len = digits.len();
46
18921
        for (i, ch) in digits.chars().enumerate() {
47
18921
            if i > 0 && (len - i).is_multiple_of(3) {
48
5901
                write!(f, ",")?;
49
13020
            }
50
18921
            write!(f, "{ch}")?;
51
        }
52

            
53
1007
        Ok(())
54
1007
    }
55
}
56

            
57
#[cfg(test)]
58
mod tests {
59
    use super::BytesFormatter;
60
    use super::LargeFormatter;
61

            
62
    use rand::RngExt;
63

            
64
    use merc_utilities::random_test;
65

            
66
    #[test]
67
1
    fn test_bytes_formatter_units() {
68
1
        assert_eq!(format!("{}", BytesFormatter(0)), "0 bytes");
69
1
        assert_eq!(format!("{}", BytesFormatter(1)), "1 bytes");
70
1
        assert_eq!(format!("{}", BytesFormatter(999)), "999 bytes");
71
        // Exact unit boundaries.
72
1
        assert_eq!(format!("{}", BytesFormatter(1_000)), "1.00 KB");
73
1
        assert_eq!(format!("{}", BytesFormatter(1_000_000)), "1.00 MB");
74
1
        assert_eq!(format!("{}", BytesFormatter(1_000_000_000)), "1.00 GB");
75
        // Fractional values within a unit.
76
1
        assert_eq!(format!("{}", BytesFormatter(1_500)), "1.50 KB");
77
1
        assert_eq!(format!("{}", BytesFormatter(2_500_000_000)), "2.50 GB");
78
        // Just below the next boundary rounds up to the boundary in the lower unit.
79
1
        assert_eq!(format!("{}", BytesFormatter(999_999)), "1000.00 KB");
80
1
    }
81

            
82
    /// The chosen unit must always match the magnitude of the value.
83
    #[test]
84
1
    fn test_bytes_formatter_random_unit_selection() {
85
1000
        random_test(1000, |rng| {
86
1000
            let value: usize = rng.random_range(0..10_000_000_000);
87
1000
            let formatted = format!("{}", BytesFormatter(value));
88

            
89
1000
            let expected_suffix = if value >= 1_000_000_000 {
90
909
                "GB"
91
91
            } else if value >= 1_000_000 {
92
91
                "MB"
93
            } else if value >= 1_000 {
94
                "KB"
95
            } else {
96
                "bytes"
97
            };
98
1000
            assert!(
99
1000
                formatted.ends_with(expected_suffix),
100
                "{value} formatted as {formatted:?}, expected unit {expected_suffix}"
101
            );
102
1000
        });
103
1
    }
104

            
105
    /// Inserting comma separators must never corrupt the digits: stripping the
106
    /// commas back out has to reproduce the plain decimal representation.
107
    #[test]
108
1
    fn test_large_formatter_random_preserves_value() {
109
1000
        random_test(1000, |rng| {
110
1000
            let value: i64 = rng.random();
111
1000
            let formatted = format!("{}", LargeFormatter(value));
112
1000
            assert_eq!(formatted.replace(',', ""), value.to_string());
113
1000
        });
114
1
    }
115

            
116
    #[test]
117
1
    fn test_large_formatter_small_numbers() {
118
1
        assert_eq!(format!("{}", LargeFormatter(0)), "0");
119
1
        assert_eq!(format!("{}", LargeFormatter(123)), "123");
120
1
    }
121

            
122
    #[test]
123
1
    fn test_large_formatter_millions() {
124
1
        assert_eq!(format!("{}", LargeFormatter(1234567)), "1,234,567");
125
1
        assert_eq!(format!("{}", LargeFormatter(12345678)), "12,345,678");
126
1
    }
127

            
128
    #[test]
129
1
    fn test_large_formatter_negative() {
130
1
        assert_eq!(format!("{}", LargeFormatter(-123456i64)), "-123,456");
131
1
        assert_eq!(format!("{}", LargeFormatter(-1234567i64)), "-1,234,567");
132
1
        assert_eq!(format!("{}", LargeFormatter(-123456789i64)), "-123,456,789");
133
1
    }
134
}