1const IEC: [&str; 7] = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"];
9
10pub fn bytes(n: u64) -> String {
15 let mut v = n as f64;
16 let mut i = 0;
17 while v >= 1024.0 && i < IEC.len() - 1 {
18 v /= 1024.0;
19 i += 1;
20 }
21 if i == 0 {
22 format!("{n} B")
23 } else if v < 10.0 {
24 format!("{v:.2} {}", IEC[i])
25 } else {
26 format!("{v:.1} {}", IEC[i])
27 }
28}
29
30pub fn bytes_fixed(n: u64, decimals: usize) -> String {
33 const KB: f64 = 1024.0;
34 const MB: f64 = KB * 1024.0;
35 const GB: f64 = MB * 1024.0;
36 let b = n as f64;
37 if b >= GB {
38 format!("{:.decimals$} GB", b / GB)
39 } else if b >= MB {
40 format!("{:.decimals$} MB", b / MB)
41 } else if b >= KB {
42 format!("{:.decimals$} KB", b / KB)
43 } else {
44 format!("{n} B")
45 }
46}
47
48pub fn rate(bytes_per_s: f64) -> String {
50 format!("{}/s", bytes(bytes_per_s.max(0.0) as u64))
51}
52
53pub fn rate_fixed(bytes_per_s: f64, decimals: usize) -> String {
57 const KB: f64 = 1024.0;
58 const MB: f64 = KB * 1024.0;
59 if bytes_per_s >= MB {
60 format!("{:.decimals$} MB/sec", bytes_per_s / MB)
61 } else if bytes_per_s >= 1.0 {
62 format!("{:.decimals$} KB/sec", bytes_per_s / KB)
63 } else {
64 String::new()
65 }
66}
67
68pub fn duration(secs: f64) -> String {
71 if !secs.is_finite() {
72 return "?".into();
73 }
74 let s = secs.max(0.0);
75 if s < 60.0 {
76 format!("{s:.1}s")
77 } else if s < 3600.0 {
78 format!("{}m{:02}s", (s / 60.0) as u64, (s % 60.0) as u64)
79 } else {
80 format!(
81 "{}h{:02}m",
82 (s / 3600.0) as u64,
83 ((s % 3600.0) / 60.0) as u64
84 )
85 }
86}
87
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
90pub struct DurationUnits<'a> {
91 pub hour: &'a str,
92 pub minute: &'a str,
93 pub second: &'a str,
94}
95
96impl DurationUnits<'static> {
97 pub const ENGLISH: Self = DurationUnits {
99 hour: "hr",
100 minute: "min",
101 second: "sec",
102 };
103}
104
105pub fn duration_long(secs: u64) -> String {
107 duration_long_with(secs, DurationUnits::ENGLISH)
108}
109
110pub fn duration_long_with(secs: u64, units: DurationUnits<'_>) -> String {
115 if secs >= 3600 {
116 format!(
117 "{} {} {} {}",
118 secs / 3600,
119 units.hour,
120 (secs % 3600) / 60,
121 units.minute
122 )
123 } else if secs >= 60 {
124 format!(
125 "{} {} {} {}",
126 secs / 60,
127 units.minute,
128 secs % 60,
129 units.second
130 )
131 } else {
132 format!("{secs} {}", units.second)
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 const MIB: u64 = 1024 * 1024;
141 const GIB: u64 = MIB * 1024;
142
143 #[test]
144 fn iec_bytes_across_every_boundary() {
145 for (n, want) in [
146 (0, "0 B"),
147 (1, "1 B"),
148 (1023, "1023 B"),
149 (1024, "1.00 KiB"),
150 (1536, "1.50 KiB"),
151 (10 * 1024, "10.0 KiB"),
152 (MIB - 1, "1024.0 KiB"),
153 (MIB, "1.00 MiB"),
154 (GIB * 3 / 2, "1.50 GiB"),
155 (GIB * 1024 * 12, "12.0 TiB"),
156 (u64::MAX, "16.0 EiB"),
157 ] {
158 assert_eq!(bytes(n), want, "{n}");
159 }
160 }
161
162 #[test]
163 fn fixed_bytes_use_kb_mb_gb_labels_and_stop_at_gb() {
164 for (n, decimals, want) in [
165 (0, 2, "0 B"),
166 (1023, 2, "1023 B"),
167 (1024, 2, "1.00 KB"),
168 (MIB - 1, 2, "1024.00 KB"),
169 (MIB, 3, "1.000 MB"),
170 (127_565_824, 2, "121.66 MB"),
171 (GIB, 2, "1.00 GB"),
172 (u64::MAX, 1, "17179869184.0 GB"),
173 ] {
174 assert_eq!(bytes_fixed(n, decimals), want, "{n}");
175 }
176 }
177
178 #[test]
179 fn rates_carry_their_suffix() {
180 assert_eq!(rate(0.0), "0 B/s");
181 assert_eq!(rate(-5.0), "0 B/s");
182 assert_eq!(rate(1536.0), "1.50 KiB/s");
183 assert_eq!(rate(12.0 * MIB as f64), "12.0 MiB/s");
184 }
185
186 #[test]
187 fn fixed_rates_switch_unit_at_one_mib_and_hide_an_idle_transfer() {
188 assert_eq!(rate_fixed(0.0, 3), "");
189 assert_eq!(rate_fixed(0.999, 3), "");
190 assert_eq!(rate_fixed(1.0, 3), "0.001 KB/sec");
191 assert_eq!(rate_fixed(205_482.0, 3), "200.666 KB/sec");
192 assert_eq!(rate_fixed(MIB as f64 - 1.0, 3), "1023.999 KB/sec");
193 assert_eq!(rate_fixed(1_076_887.0, 3), "1.027 MB/sec");
194 }
195
196 #[test]
197 fn short_durations_keep_two_fields() {
198 for (secs, want) in [
199 (0.0, "0.0s"),
200 (-3.0, "0.0s"),
201 (45.25, "45.2s"),
202 (59.99, "60.0s"),
203 (60.0, "1m00s"),
204 (182.0, "3m02s"),
205 (3599.0, "59m59s"),
206 (3600.0, "1h00m"),
207 (4320.0, "1h12m"),
208 (f64::NAN, "?"),
209 (f64::INFINITY, "?"),
210 ] {
211 assert_eq!(duration(secs), want, "{secs}");
212 }
213 }
214
215 #[test]
216 fn long_durations_spell_their_units() {
217 for (secs, want) in [
218 (0, "0 sec"),
219 (45, "45 sec"),
220 (60, "1 min 0 sec"),
221 (182, "3 min 2 sec"),
222 (3599, "59 min 59 sec"),
223 (3600, "1 hr 0 min"),
224 (4320, "1 hr 12 min"),
225 (u64::MAX, "5124095576030431 hr 0 min"),
226 ] {
227 assert_eq!(duration_long(secs), want, "{secs}");
228 }
229 }
230
231 #[test]
232 fn a_localised_ui_supplies_its_own_labels() {
233 let units = DurationUnits {
234 hour: "Std",
235 minute: "Min",
236 second: "Sek",
237 };
238 assert_eq!(duration_long_with(4320, units), "1 Std 12 Min");
239 assert_eq!(duration_long_with(182, units), "3 Min 2 Sek");
240 assert_eq!(duration_long_with(7, units), "7 Sek");
241 }
242}