1use num_traits::{AsPrimitive, Zero};
2use std::sync::OnceLock;
3
4static NUM_HUMANIZER: OnceLock<Humanizer> = OnceLock::new();
5static BINARY_HUMANIZER: OnceLock<Humanizer> = OnceLock::new();
6static SI_HUMANIZER: OnceLock<Humanizer> = OnceLock::new();
7
8fn num_humanizer() -> &'static Humanizer {
9 NUM_HUMANIZER.get_or_init(|| {
10 Humanizer::new(&["", "K", "M", "B", "T", "Qa", "Qd"])
11 .with_division_factor(1000.0)
12 .with_space_before_unit(true)
13 })
14}
15
16fn binary_humanizer() -> &'static Humanizer {
17 BINARY_HUMANIZER.get_or_init(|| {
18 Humanizer::new(&["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"])
19 .with_division_factor(1024.0)
20 .with_space_before_unit(true)
21 })
22}
23
24fn si_humanizer() -> &'static Humanizer {
25 SI_HUMANIZER.get_or_init(|| {
26 Humanizer::new(&["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"])
27 .with_division_factor(1000.0)
28 .with_space_before_unit(true)
29 })
30}
31
32#[derive(Clone, Debug)]
34pub struct Humanizer {
35 units: Vec<String>,
36 space_before_unit: bool,
37 division_factor: f64,
38}
39
40impl Humanizer {
41 pub fn new(units: &[&str]) -> Self {
57 assert!(!units.is_empty(), "Units slice must not be empty");
58
59 Self {
60 units: units.iter().map(std::string::ToString::to_string).collect(),
61 space_before_unit: true,
62 division_factor: 1000.0,
63 }
64 }
65
66 #[must_use]
69 pub fn with_space_before_unit(mut self, space_before_unit: bool) -> Self {
70 self.space_before_unit = space_before_unit;
71 self
72 }
73
74 #[must_use]
81 pub fn with_division_factor<F>(mut self, factor: F) -> Self
82 where
83 F: Into<f64>,
84 {
85 self.division_factor = factor.into();
86 assert!(
87 self.division_factor >= 0.0,
88 "Division factor must be greater than 0"
89 );
90 self
91 }
92
93 fn calculate_parts<U>(&self, value: U) -> (f64, usize)
100 where
101 U: Zero + AsPrimitive<f64> + PartialEq + Copy,
102 {
103 if value == U::zero() {
104 return (0.0, 0);
105 }
106
107 let mut num_value = value.as_();
108 let mut index = 0;
109 let max_index = self.units.len() - 1;
110
111 while num_value.abs() >= self.division_factor && index < max_index {
112 num_value /= self.division_factor;
113 index += 1;
114 }
115
116 (num_value, index)
117 }
118
119 pub fn format<U>(&self, value: U) -> String
138 where
139 U: Zero + AsPrimitive<f64> + PartialEq,
140 {
141 let (num_value, index) = self.calculate_parts(value);
142 let unit = &self.units[index];
143 let space = if self.space_before_unit && !unit.is_empty() {
144 " "
145 } else {
146 ""
147 };
148
149 if index == 0 && num_value == 0.0 {
150 return format!("0{space}{unit}");
151 }
152
153 let abs_val = num_value.abs();
154 let precision = if abs_val < 10.0 {
155 2
156 } else {
157 usize::from(abs_val < 100.0)
158 };
159
160 format!("{num_value:.precision$}{space}{unit}")
161 }
162
163 pub fn format_as_parts<U>(&self, value: U) -> (f64, &str)
183 where
184 U: Zero + AsPrimitive<f64> + PartialEq + Copy,
185 {
186 let (num_value, index) = self.calculate_parts(value);
187 (num_value, &self.units[index])
188 }
189}
190
191#[must_use]
201pub fn human_bytes<U>(bytes: U) -> String
202where
203 U: Zero + AsPrimitive<f64> + PartialEq,
204{
205 binary_humanizer().format(bytes)
206}
207
208#[must_use]
218pub fn human_bytes_as_parts<U>(bytes: U) -> (f64, &'static str)
219where
220 U: Zero + AsPrimitive<f64> + PartialEq,
221{
222 binary_humanizer().format_as_parts(bytes)
223}
224
225#[must_use]
235pub fn human_bytes_si<U>(bytes: U) -> String
236where
237 U: Zero + AsPrimitive<f64> + PartialEq,
238{
239 si_humanizer().format(bytes)
240}
241
242#[must_use]
252pub fn human_bytes_si_as_parts<U>(bytes: U) -> (f64, &'static str)
253where
254 U: Zero + AsPrimitive<f64> + PartialEq,
255{
256 si_humanizer().format_as_parts(bytes)
257}
258
259#[must_use]
269pub fn human_number<U>(number: U) -> String
270where
271 U: Zero + AsPrimitive<f64> + PartialEq,
272{
273 num_humanizer().format(number)
274}
275
276pub fn human_number_as_parts<U>(number: U) -> (f64, &'static str)
286where
287 U: Zero + AsPrimitive<f64> + PartialEq,
288{
289 num_humanizer().format_as_parts(number)
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 #[test]
297 fn test_humanizer() {
298 let humanizer = Humanizer::new(&["", "k", "m", "b", "t"]).with_space_before_unit(false);
299
300 assert_eq!(humanizer.format(0), "0");
301 assert_eq!(humanizer.format(889), "889");
302 assert_eq!(humanizer.format(123_456_789), "123m");
303 assert_eq!(humanizer.format(1_234_567_890), "1.23b");
304 assert_eq!(humanizer.format(12_345_678_901u64), "12.3b");
305 assert_eq!(humanizer.format(123_456_789_012u64), "123b");
306 assert_eq!(humanizer.format(123_456_789_012_345u64), "123t");
307
308 let humanizer2 = humanizer
309 .with_space_before_unit(true)
310 .with_division_factor(500);
311 assert_eq!(humanizer2.format(0), "0");
312 assert_eq!(humanizer2.format(889), "1.78 k");
313 assert_eq!(humanizer2.format(123_456_789), "494 m");
314 assert_eq!(humanizer2.format(1_234_567_890), "9.88 b");
315 assert_eq!(humanizer2.format(12_345_678_901u64), "98.8 b");
316 assert_eq!(humanizer2.format(123_456_789_012u64), "1.98 t");
317 assert_eq!(humanizer2.format(123_456_789_012_345u64), "1975 t");
318
319 let humanizer3 = Humanizer::new(&["", "k", "m", "b", "t", "qa"]);
320 assert_eq!(humanizer3.format_as_parts(0), (0.0, ""));
321 assert_eq!(humanizer3.format_as_parts(635), (635.0, ""));
322 assert_eq!(humanizer3.format_as_parts(12_345), (12.345, "k"));
323 assert_eq!(humanizer3.format_as_parts(1_234_567), (1.234_567, "m"));
324 assert_eq!(humanizer3.format_as_parts(123_456_789), (123.456_789, "m"));
325 assert_eq!(
326 humanizer3.format_as_parts(12_345_678_901u64),
327 (12.345_678_901_000_001, "b")
328 );
329 assert_eq!(
330 humanizer3.format_as_parts(123_456_789_012u64),
331 (123.456_789_011_999_99, "b")
332 );
333 assert_eq!(
334 humanizer3.format_as_parts(123_456_789_012_345u64),
335 (123.456_789_012_345, "t")
336 );
337 assert_eq!(
338 humanizer3.format_as_parts(123_456_789_012_345_678u64),
339 (123.456_789_012_345_7, "qa")
340 );
341 }
342
343 #[test]
344 #[should_panic(expected = "Units slice must not be empty")]
345 fn test_humanizer_new_empty_units() {
346 let _ = Humanizer::new(&[]);
347 }
348
349 #[test]
350 fn test_human_bytes() {
351 assert_eq!(human_bytes(0), "0 B");
352 assert_eq!(human_bytes(635), "635 B");
353 assert_eq!(human_bytes(12_345), "12.1 KiB");
354 assert_eq!(human_bytes(1_234_567), "1.18 MiB");
355 assert_eq!(human_bytes(123_456_789), "118 MiB");
356 assert_eq!(human_bytes(12_345_678_901u64), "11.5 GiB");
357 assert_eq!(human_bytes(123_456_789_012u64), "115 GiB");
358 assert_eq!(human_bytes(123_456_789_012_345u64), "112 TiB");
359 assert_eq!(human_bytes(123_456_789_012_345_678u64), "110 PiB");
360 }
361
362 #[test]
363 fn test_human_bytes_as_parts() {
364 assert_eq!(human_bytes_as_parts(0), (0.0, "B"));
365 assert_eq!(human_bytes_as_parts(635), (635.0, "B"));
366 assert_eq!(human_bytes_as_parts(12_345), (12.055_664_062_5, "KiB"));
367 assert_eq!(
368 human_bytes_as_parts(1_234_567),
369 (1.177_374_839_782_714_8, "MiB")
370 );
371 assert_eq!(
372 human_bytes_as_parts(123_456_789),
373 (117.737_568_855_285_64, "MiB")
374 );
375 assert_eq!(
376 human_bytes_as_parts(12_345_678_901u64),
377 (11.497_809_459_455_311, "GiB")
378 );
379 assert_eq!(
380 human_bytes_as_parts(123_456_789_012u64),
381 (114.978_094_596_415_76, "GiB")
382 );
383 assert_eq!(
384 human_bytes_as_parts(123_456_789_012_345u64),
385 (112.283_295_504_626_04, "TiB")
386 );
387 assert_eq!(
388 human_bytes_as_parts(123_456_789_012_345_678u64),
389 (109.651_655_766_236_97, "PiB")
390 );
391 }
392
393 #[test]
394 fn test_human_bytes_si() {
395 assert_eq!(human_bytes_si(0), "0 B");
396 assert_eq!(human_bytes_si(635), "635 B");
397 assert_eq!(human_bytes_si(12_345), "12.3 KB");
398 assert_eq!(human_bytes_si(1_234_567), "1.23 MB");
399 assert_eq!(human_bytes_si(123_456_789), "123 MB");
400 assert_eq!(human_bytes_si(12_345_678_901u64), "12.3 GB");
401 assert_eq!(human_bytes_si(123_456_789_012u64), "123 GB");
402 assert_eq!(human_bytes_si(123_456_789_012_345u64), "123 TB");
403 assert_eq!(human_bytes_si(123_456_789_012_345_678u64), "123 PB");
404 }
405
406 #[test]
407 fn test_human_bytes_si_as_parts() {
408 assert_eq!(human_bytes_si_as_parts(0), (0.0, "B"));
409 assert_eq!(human_bytes_si_as_parts(635), (635.0, "B"));
410 assert_eq!(human_bytes_si_as_parts(12_345), (12.345, "KB"));
411 assert_eq!(human_bytes_si_as_parts(1_234_567), (1.234_567, "MB"));
412 assert_eq!(human_bytes_si_as_parts(123_456_789), (123.456_789, "MB"));
413 assert_eq!(
414 human_bytes_si_as_parts(12_345_678_901u64),
415 (12.345_678_901_000_001, "GB")
416 );
417 assert_eq!(
418 human_bytes_si_as_parts(123_456_789_012u64),
419 (123.456_789_011_999_99, "GB")
420 );
421 assert_eq!(
422 human_bytes_si_as_parts(123_456_789_012_345u64),
423 (123.456_789_012_345, "TB")
424 );
425 assert_eq!(
426 human_bytes_si_as_parts(123_456_789_012_345_678u64),
427 (123.456_789_012_345_7, "PB")
428 );
429 }
430
431 #[test]
432 fn test_human_number() {
433 assert_eq!(human_number(0), "0");
434 assert_eq!(human_number(635), "635");
435 assert_eq!(human_number(12_345), "12.3 K");
436 assert_eq!(human_number(1_234_567), "1.23 M");
437 assert_eq!(human_number(123_456_789), "123 M");
438 assert_eq!(human_number(12_345_678_901u64), "12.3 B");
439 assert_eq!(human_number(123_456_789_012u64), "123 B");
440 assert_eq!(human_number(123_456_789_012_345u64), "123 T");
441 assert_eq!(human_number(123_456_789_012_345_678u64), "123 Qa");
442 }
443
444 #[test]
445 fn test_human_number_as_parts() {
446 assert_eq!(human_number_as_parts(0), (0.0, ""));
447 assert_eq!(human_number_as_parts(635), (635.0, ""));
448 assert_eq!(human_number_as_parts(12_345), (12.345, "K"));
449 assert_eq!(human_number_as_parts(1_234_567), (1.234_567, "M"));
450 assert_eq!(human_number_as_parts(123_456_789), (123.456_789, "M"));
451 assert_eq!(
452 human_number_as_parts(12_345_678_901u64),
453 (12.345_678_901_000_001, "B")
454 );
455 assert_eq!(
456 human_number_as_parts(123_456_789_012u64),
457 (123.456_789_011_999_99, "B")
458 );
459 assert_eq!(
460 human_number_as_parts(123_456_789_012_345u64),
461 (123.456_789_012_345, "T")
462 );
463 assert_eq!(
464 human_number_as_parts(123_456_789_012_345_678u64),
465 (123.456_789_012_345_7, "Qa")
466 );
467 }
468}