Skip to main content

nntp_proxy/
formatting.rs

1//! Log formatting utilities
2
3use std::fmt;
4
5#[allow(clippy::cast_precision_loss)] // Human-readable byte formatting is approximate by design.
6const fn bytes_as_f64_for_display(bytes: u64) -> f64 {
7    // Human-readable log output is approximate by design; exact byte counts are
8    // preserved in the integer path below and in metrics storage.
9    bytes as f64
10}
11
12/// Format bytes in human-readable format using IEC units (KiB, MiB, GiB).
13#[inline]
14#[must_use]
15pub fn format_bytes(bytes: u64) -> String {
16    let mut output = String::new();
17    format_bytes_into(&mut output, bytes).expect("writing to String cannot fail");
18    output
19}
20
21/// Write bytes in human-readable format using IEC units (KiB, MiB, GiB).
22#[inline]
23pub fn format_bytes_into(output: &mut impl fmt::Write, bytes: u64) -> fmt::Result {
24    const KIB: u64 = 1024;
25    const MIB: u64 = KIB * 1024;
26    const GIB: u64 = MIB * 1024;
27    const TIB: u64 = GIB * 1024;
28    const PIB: u64 = TIB * 1024;
29    const KIB_F64: f64 = 1024.0;
30    const MIB_F64: f64 = KIB_F64 * 1024.0;
31    const GIB_F64: f64 = MIB_F64 * 1024.0;
32    const TIB_F64: f64 = GIB_F64 * 1024.0;
33    const PIB_F64: f64 = TIB_F64 * 1024.0;
34
35    if bytes >= PIB {
36        write!(
37            output,
38            "{:.2} PiB",
39            bytes_as_f64_for_display(bytes) / PIB_F64
40        )
41    } else if bytes >= TIB {
42        write!(
43            output,
44            "{:.2} TiB",
45            bytes_as_f64_for_display(bytes) / TIB_F64
46        )
47    } else if bytes >= GIB {
48        write!(
49            output,
50            "{:.2} GiB",
51            bytes_as_f64_for_display(bytes) / GIB_F64
52        )
53    } else if bytes >= MIB {
54        write!(
55            output,
56            "{:.2} MiB",
57            bytes_as_f64_for_display(bytes) / MIB_F64
58        )
59    } else if bytes >= KIB {
60        write!(
61            output,
62            "{:.2} KiB",
63            bytes_as_f64_for_display(bytes) / KIB_F64
64        )
65    } else {
66        write!(output, "{bytes} B")
67    }
68}
69
70/// Format large counts with compact human suffixes for readable TUI output.
71#[inline]
72#[must_use]
73pub fn format_count(count: u64) -> String {
74    const UNITS: &[(u64, &str)] = &[
75        (1_000_000_000_000, "T"),
76        (1_000_000_000, "B"),
77        (1_000_000, "M"),
78        (1_000, "K"),
79    ];
80
81    #[allow(clippy::cast_precision_loss)] // Presentation-only rounding for compact labels.
82    fn compact(value: u64, divisor: u64, suffix: &str) -> String {
83        let scaled = value as f64 / divisor as f64;
84        format!("{scaled:.1}{suffix}")
85    }
86
87    for (idx, (divisor, suffix)) in UNITS.iter().enumerate() {
88        if count >= *divisor {
89            let formatted = compact(count, *divisor, suffix);
90            if formatted.starts_with("1000.0") && idx > 0 {
91                let (next_divisor, next_suffix) = UNITS[idx - 1];
92                return compact(count, next_divisor, next_suffix);
93            }
94            return formatted;
95        }
96    }
97
98    count.to_string()
99}
100
101/// Shorten UUID to first 8 characters for log readability
102#[inline]
103#[must_use]
104pub fn short_id(uuid: &uuid::Uuid) -> String {
105    uuid.to_string()[..8].to_string()
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn test_format_bytes() {
114        assert_eq!(format_bytes(0), "0 B");
115        assert_eq!(format_bytes(512), "512 B");
116        assert_eq!(format_bytes(1024), "1.00 KiB");
117        assert_eq!(format_bytes(1536), "1.50 KiB");
118        assert_eq!(format_bytes(1_048_576), "1.00 MiB");
119        assert_eq!(format_bytes(29_312_178), "27.95 MiB");
120        assert_eq!(format_bytes(1_073_741_824), "1.00 GiB");
121    }
122
123    #[test]
124    fn format_bytes_into_matches_owned_formatter() {
125        let cases = [
126            0,
127            512,
128            1024,
129            1536,
130            1_048_576,
131            29_312_178,
132            1_073_741_824,
133            1_099_511_627_776,
134            1_125_899_906_842_624,
135        ];
136
137        for bytes in cases {
138            let mut output = String::new();
139            format_bytes_into(&mut output, bytes).unwrap();
140            assert_eq!(output, format_bytes(bytes));
141        }
142    }
143
144    #[test]
145    fn test_format_count() {
146        assert_eq!(format_count(0), "0");
147        assert_eq!(format_count(999), "999");
148        assert_eq!(format_count(1_000), "1.0K");
149        assert_eq!(format_count(999_999), "1.0M");
150        assert_eq!(format_count(12_345), "12.3K");
151        assert_eq!(format_count(1_234_567), "1.2M");
152        assert_eq!(format_count(1_234_567_890), "1.2B");
153        assert_eq!(format_count(9_876_543_210_987), "9.9T");
154    }
155
156    #[test]
157    fn test_short_id() {
158        let uuid = uuid::Uuid::parse_str("34925aee-7f65-4670-9adc-d2e95ac97b26").unwrap();
159        assert_eq!(short_id(&uuid), "34925aee");
160    }
161}