Skip to main content

pingap_util/
format.rs

1// Copyright 2024-2025 Tree xie.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::fmt::Write;
16
17/// A powerful macro to format a value with units, handling integer and fractional parts.
18///
19/// It takes a writer, a value, and a series of thresholds with their corresponding units and divisors.
20macro_rules! format_with_units {
21    (
22        $writer:expr,
23        $value:expr,
24        $base_unit:expr,
25        $( ($threshold:expr, $unit:expr, $divisor:expr) ),*
26    ) => {
27        let value = $value; // Use the value as an integer.
28        let mut handled = false;
29
30        // Iterate through thresholds, largest unit first.
31        $(
32            if !handled && value >= $threshold {
33                // 1. Calculate the whole and fractional parts using integer math.
34                let whole_part = value / $divisor;
35                let remainder = value % $divisor;
36
37                // 2. Calculate the first decimal digit.
38                // We multiply by 10 before dividing to get the digit.
39                // E.g., for 1234 bytes -> 1234 % 1024 = 210. (210 * 10) / 1024 = 2.
40                let decimal_digit = (remainder * 10) / $divisor;
41
42                // 3. Write directly to the writer, avoiding intermediate strings.
43                let _ = write!($writer, "{}", whole_part);
44                if decimal_digit > 0 {
45                    // Only write the decimal part if it's not zero.
46                    // This naturally handles the "strip .0" logic.
47                    let _ = write!($writer, ".{}", decimal_digit);
48                }
49                let _ = write!($writer, "{}", $unit);
50
51                handled = true;
52            }
53        )*
54
55        // Fallback for the base unit.
56        if !handled {
57            let _ = write!($writer, "{}{}", value, $base_unit);
58        }
59    };
60}
61
62/// Formats a byte size into a human-readable string (B, KB, MB, GB).
63pub fn format_byte_size(buf: &mut impl Write, size: usize) {
64    const KB: usize = 1_000;
65    const MB: usize = 1_000 * KB;
66    const GB: usize = 1_000 * MB;
67    format_with_units!(
68        buf,
69        size,
70        "B",
71        (GB, "GB", GB),
72        (MB, "MB", MB),
73        (KB, "KB", KB)
74    );
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use pretty_assertions::assert_eq;
81
82    fn formatted_byte_size(size: usize) -> String {
83        let mut s = String::new();
84        format_byte_size(&mut s, size);
85        s
86    }
87
88    #[test]
89    fn test_format_byte_size() {
90        assert_eq!(formatted_byte_size(512), "512B");
91        assert_eq!(formatted_byte_size(999), "999B");
92        assert_eq!(formatted_byte_size(1000), "1KB");
93        assert_eq!(formatted_byte_size(1024), "1KB");
94        assert_eq!(formatted_byte_size(1124), "1.1KB");
95        assert_eq!(formatted_byte_size(1220 * 1000), "1.2MB");
96    }
97}