1#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
5pub enum UnitSystem {
6 #[default]
8 Iec,
9 Si,
11}
12
13const IEC_SUFFIXES: &[(u64, &str)] = &[
14 (1, "B"),
15 (1 << 10, "KiB"),
16 (1 << 20, "MiB"),
17 (1 << 30, "GiB"),
18 (1u64 << 40, "TiB"),
19];
20
21const SI_SUFFIXES: &[(u64, &str)] = &[
22 (1, "B"),
23 (1_000, "kB"),
24 (1_000_000, "MB"),
25 (1_000_000_000, "GB"),
26 (1_000_000_000_000, "TB"),
27];
28
29impl UnitSystem {
30 fn suffixes(self) -> &'static [(u64, &'static str)] {
31 match self {
32 UnitSystem::Iec => IEC_SUFFIXES,
33 UnitSystem::Si => SI_SUFFIXES,
34 }
35 }
36
37 #[must_use]
41 pub fn format_bytes(self, value: u64) -> String {
42 let table = self.suffixes();
43 let mut best = &table[0];
44 for entry in table {
45 if value >= entry.0 {
46 best = entry;
47 }
48 }
49 let scaled = value as f64 / best.0 as f64;
50 if best.0 == 1 {
51 format!("{}{}", value, best.1)
52 } else if scaled >= 100.0 {
53 format!("{scaled:.0}{}", best.1)
54 } else if scaled >= 10.0 {
55 format!("{scaled:.1}{}", best.1)
56 } else {
57 format!("{scaled:.2}{}", best.1)
58 }
59 }
60
61 #[must_use]
64 pub fn format_rate(self, bytes_per_sec: f64) -> String {
65 let v = bytes_per_sec.max(0.0) as u64;
66 format!("{}/s", self.format_bytes(v))
67 }
68}
69
70pub fn parse_size(s: &str, unit_system: UnitSystem) -> Option<u64> {
78 let s = s.trim();
79 if s.is_empty() {
80 return None;
81 }
82 let table = unit_system.suffixes();
83 let (num_part, mult): (&str, u64) = if let Some(pos) = s.find(|c: char| c.is_ascii_alphabetic())
85 {
86 let (num, suffix) = s.split_at(pos);
87 let m = match suffix.chars().next()?.to_ascii_uppercase() {
88 'B' => 1,
89 'K' => table[1].0,
90 'M' => table[2].0,
91 'G' => table[3].0,
92 'T' => table[4].0,
93 _ => return None,
94 };
95 (num, m)
96 } else {
97 (s, 1)
98 };
99 let n: f64 = num_part.trim().parse().ok()?;
100 if n < 0.0 || !n.is_finite() {
101 return None;
102 }
103 Some((n * mult as f64) as u64)
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[test]
111 fn format_iec_bytes() {
112 assert_eq!(UnitSystem::Iec.format_bytes(0), "0B");
113 assert_eq!(UnitSystem::Iec.format_bytes(512), "512B");
114 assert_eq!(UnitSystem::Iec.format_bytes(1024), "1.00KiB");
115 assert_eq!(UnitSystem::Iec.format_bytes(1_048_576), "1.00MiB");
116 assert_eq!(UnitSystem::Iec.format_bytes(47_185_920), "45.0MiB");
117 }
118
119 #[test]
120 fn format_si_bytes() {
121 assert_eq!(UnitSystem::Si.format_bytes(1000), "1.00kB");
122 assert_eq!(UnitSystem::Si.format_bytes(1_000_000), "1.00MB");
123 }
124
125 #[test]
126 fn parse_iec_default() {
127 assert_eq!(parse_size("1024", UnitSystem::Iec), Some(1024));
128 assert_eq!(parse_size("1K", UnitSystem::Iec), Some(1024));
129 assert_eq!(parse_size("1M", UnitSystem::Iec), Some(1_048_576));
130 assert_eq!(parse_size("1.5M", UnitSystem::Iec), Some(1_572_864));
131 }
132
133 #[test]
134 fn parse_si() {
135 assert_eq!(parse_size("1k", UnitSystem::Si), Some(1000));
136 assert_eq!(parse_size("1M", UnitSystem::Si), Some(1_000_000));
137 }
138
139 #[test]
140 fn parse_rejects_negative() {
141 assert_eq!(parse_size("-5M", UnitSystem::Iec), None);
142 }
143
144 #[test]
145 fn parse_rejects_garbage() {
146 assert_eq!(parse_size("abc", UnitSystem::Iec), None);
147 assert_eq!(parse_size("", UnitSystem::Iec), None);
148 }
149
150 #[test]
151 fn format_rate_appends_per_second() {
152 assert_eq!(UnitSystem::Iec.format_rate(1024.0), "1.00KiB/s");
153 }
154}