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}