1const BINARY_UNITS: &[&str] = &[
27 "bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB",
28];
29
30const DECIMAL_UNITS: &[&str] = &["bytes", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
35pub enum SizeUnit {
36 #[default]
38 Binary,
39 Decimal,
41}
42
43#[must_use]
61pub fn format_size(size: i64, unit: SizeUnit, precision: usize) -> String {
62 let (base, units): (f64, &[&str]) = match unit {
63 SizeUnit::Binary => (1024.0, BINARY_UNITS),
64 SizeUnit::Decimal => (1000.0, DECIMAL_UNITS),
65 };
66
67 let negative = size < 0;
68 let abs_size = size.unsigned_abs();
69
70 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
71 if abs_size < base as u64 {
72 let prefix = if negative { "-" } else { "" };
74 return format!("{prefix}{abs_size} bytes");
75 }
76
77 #[allow(clippy::cast_precision_loss)]
78 let mut value = abs_size as f64;
79 let mut unit_idx = 0;
80
81 while value >= base && unit_idx < units.len() - 1 {
82 value /= base;
83 unit_idx += 1;
84 }
85
86 let prefix = if negative { "-" } else { "" };
87 format!("{prefix}{value:.precision$} {}", units[unit_idx])
88}
89
90#[must_use]
104pub fn decimal(size: u64) -> String {
105 #[allow(clippy::cast_possible_wrap)]
106 format_size(size as i64, SizeUnit::Decimal, 1)
107}
108
109#[must_use]
121pub fn decimal_with_precision(size: u64, precision: usize) -> String {
122 #[allow(clippy::cast_possible_wrap)]
123 format_size(size as i64, SizeUnit::Decimal, precision)
124}
125
126#[must_use]
140pub fn binary(size: u64) -> String {
141 #[allow(clippy::cast_possible_wrap)]
142 format_size(size as i64, SizeUnit::Binary, 1)
143}
144
145#[must_use]
157pub fn binary_with_precision(size: u64, precision: usize) -> String {
158 #[allow(clippy::cast_possible_wrap)]
159 format_size(size as i64, SizeUnit::Binary, precision)
160}
161
162#[must_use]
179pub fn format_speed(bytes_per_second: f64, unit: SizeUnit, precision: usize) -> String {
180 if bytes_per_second.is_nan() {
182 return "NaN".to_string();
183 }
184 if bytes_per_second.is_infinite() {
185 let prefix = if bytes_per_second.is_sign_negative() {
186 "-"
187 } else {
188 ""
189 };
190 return format!("{prefix}∞");
191 }
192
193 let (base, units): (f64, &[&str]) = match unit {
194 SizeUnit::Binary => (1024.0, BINARY_UNITS),
195 SizeUnit::Decimal => (1000.0, DECIMAL_UNITS),
196 };
197
198 let negative = bytes_per_second < 0.0;
199 let mut value = bytes_per_second.abs();
200
201 if value < base {
202 let prefix = if negative { "-" } else { "" };
204 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
205 let int_value = value as u64;
206 return format!("{prefix}{int_value} bytes/s");
207 }
208
209 let mut unit_idx = 0;
210 while value >= base && unit_idx < units.len() - 1 {
211 value /= base;
212 unit_idx += 1;
213 }
214
215 let unit_str = units[unit_idx];
216 let speed_unit = if unit_str == "bytes" {
218 "bytes/s"
219 } else {
220 &format!("{unit_str}/s")
222 };
223
224 let prefix = if negative { "-" } else { "" };
225 format!("{prefix}{value:.precision$} {speed_unit}")
226}
227
228#[must_use]
239pub fn decimal_speed(bytes_per_second: f64) -> String {
240 format_speed(bytes_per_second, SizeUnit::Decimal, 1)
241}
242
243#[must_use]
254pub fn binary_speed(bytes_per_second: f64) -> String {
255 format_speed(bytes_per_second, SizeUnit::Binary, 1)
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[test]
263 fn test_decimal_bytes() {
264 assert_eq!(decimal(0), "0 bytes");
265 assert_eq!(decimal(1), "1 bytes");
266 assert_eq!(decimal(999), "999 bytes");
267 }
268
269 #[test]
270 fn test_decimal_kilobytes() {
271 assert_eq!(decimal(1_000), "1.0 kB");
272 assert_eq!(decimal(1_500), "1.5 kB");
273 assert_eq!(decimal(999_000), "999.0 kB");
274 }
275
276 #[test]
277 fn test_decimal_megabytes() {
278 assert_eq!(decimal(1_000_000), "1.0 MB");
279 assert_eq!(decimal(1_500_000), "1.5 MB");
280 assert_eq!(decimal(999_000_000), "999.0 MB");
281 }
282
283 #[test]
284 fn test_decimal_gigabytes() {
285 assert_eq!(decimal(1_000_000_000), "1.0 GB");
286 assert_eq!(decimal(1_500_000_000), "1.5 GB");
287 }
288
289 #[test]
290 fn test_decimal_terabytes() {
291 assert_eq!(decimal(1_000_000_000_000), "1.0 TB");
292 assert_eq!(decimal(1_500_000_000_000), "1.5 TB");
293 }
294
295 #[test]
296 fn test_binary_bytes() {
297 assert_eq!(binary(0), "0 bytes");
298 assert_eq!(binary(1), "1 bytes");
299 assert_eq!(binary(1023), "1023 bytes");
300 }
301
302 #[test]
303 fn test_binary_kibibytes() {
304 assert_eq!(binary(1_024), "1.0 KiB");
305 assert_eq!(binary(1_536), "1.5 KiB");
306 }
307
308 #[test]
309 fn test_binary_mebibytes() {
310 assert_eq!(binary(1_048_576), "1.0 MiB");
311 assert_eq!(binary(1_572_864), "1.5 MiB");
312 }
313
314 #[test]
315 fn test_binary_gibibytes() {
316 assert_eq!(binary(1_073_741_824), "1.0 GiB");
317 assert_eq!(binary(1_610_612_736), "1.5 GiB");
318 }
319
320 #[test]
321 fn test_precision() {
322 assert_eq!(decimal_with_precision(1_234_567, 0), "1 MB");
323 assert_eq!(decimal_with_precision(1_234_567, 1), "1.2 MB");
324 assert_eq!(decimal_with_precision(1_234_567, 2), "1.23 MB");
325 assert_eq!(decimal_with_precision(1_234_567, 3), "1.235 MB");
326 }
327
328 #[test]
329 fn test_negative_size() {
330 assert_eq!(format_size(-1_000, SizeUnit::Decimal, 1), "-1.0 kB");
331 assert_eq!(format_size(-1_500_000, SizeUnit::Decimal, 1), "-1.5 MB");
332 }
333
334 #[test]
335 fn test_decimal_speed() {
336 assert_eq!(decimal_speed(500.0), "500 bytes/s");
337 assert_eq!(decimal_speed(1_500_000.0), "1.5 MB/s");
338 assert_eq!(decimal_speed(1_000_000_000.0), "1.0 GB/s");
339 }
340
341 #[test]
342 fn test_binary_speed() {
343 assert_eq!(binary_speed(512.0), "512 bytes/s");
344 assert_eq!(binary_speed(1_048_576.0), "1.0 MiB/s");
345 assert_eq!(binary_speed(1_073_741_824.0), "1.0 GiB/s");
346 }
347
348 #[test]
349 fn test_speed_precision() {
350 assert_eq!(format_speed(1_234_567.0, SizeUnit::Decimal, 2), "1.23 MB/s");
351 assert_eq!(format_speed(1_234_567.0, SizeUnit::Binary, 2), "1.18 MiB/s");
352 }
353
354 #[test]
355 fn test_large_sizes() {
356 assert_eq!(decimal(1_000_000_000_000_000), "1.0 PB");
358 assert_eq!(binary(1_125_899_906_842_624), "1.0 PiB");
359
360 assert_eq!(decimal(1_000_000_000_000_000_000), "1.0 EB");
362 assert_eq!(binary(1_152_921_504_606_846_976), "1.0 EiB");
363 }
364
365 #[test]
366 fn test_speed_nan_handling() {
367 assert_eq!(format_speed(f64::NAN, SizeUnit::Decimal, 1), "NaN");
368 }
369
370 #[test]
371 fn test_speed_infinity_handling() {
372 assert_eq!(format_speed(f64::INFINITY, SizeUnit::Decimal, 1), "∞");
373 assert_eq!(format_speed(f64::NEG_INFINITY, SizeUnit::Decimal, 1), "-∞");
374 }
375}