1const STEP: u64 = 1000;
6const UNITS: &[&str] = &["KB", "MB", "GB", "TB", "PB", "EB"];
7const TENTHS: u64 = 10;
9
10#[must_use]
13pub fn format_bytes(bytes: i64) -> String {
14 let sign = if bytes < 0 { "-" } else { "" };
15 let size = bytes.unsigned_abs();
16 if size < STEP {
17 let noun = if size == 1 { "byte" } else { "bytes" };
18 return format!("{sign}{size} {noun}");
19 }
20 let mut divisor = STEP;
21 let mut unit = 0;
22 let mut tenths = rounded_tenths(size, divisor);
25 while tenths >= STEP * TENTHS && unit + 1 < UNITS.len() {
26 divisor *= STEP;
27 unit += 1;
28 tenths = rounded_tenths(size, divisor);
29 }
30 format!(
31 "{sign}{}.{} {}",
32 tenths / TENTHS,
33 tenths % TENTHS,
34 UNITS[unit]
35 )
36}
37
38fn rounded_tenths(size: u64, divisor: u64) -> u64 {
39 let tenths =
40 (u128::from(size) * u128::from(TENTHS) + u128::from(divisor) / 2) / u128::from(divisor);
41 u64::try_from(tenths).expect("a size in tenths of a kilobyte or more fits a u64")
42}
43
44#[cfg(test)]
45mod tests {
46 use super::*;
47
48 #[test]
49 fn counts_bytes_then_larger_units() {
50 assert_eq!(format_bytes(0), "0 bytes");
51 assert_eq!(format_bytes(1), "1 byte");
52 assert_eq!(format_bytes(999), "999 bytes");
53 assert_eq!(format_bytes(1000), "1.0 KB");
54 assert_eq!(format_bytes(1_540), "1.5 KB");
55 assert_eq!(format_bytes(1_550), "1.6 KB");
56 assert_eq!(format_bytes(15_200_000), "15.2 MB");
57 assert_eq!(format_bytes(2_000_000_000), "2.0 GB");
58 }
59
60 #[test]
61 fn rounding_up_moves_to_the_next_unit() {
62 assert_eq!(format_bytes(999_949), "999.9 KB");
63 assert_eq!(format_bytes(999_950), "1.0 MB");
64 }
65
66 #[test]
67 fn handles_the_ends_of_the_int_range() {
68 assert_eq!(format_bytes(-1_500), "-1.5 KB");
69 assert_eq!(format_bytes(i64::MAX), "9.2 EB");
70 assert_eq!(format_bytes(i64::MIN), "-9.2 EB");
71 }
72}