Skip to main content

handy/
human.rs

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/// A struct that can be used to humanize numbers with custom units.
33#[derive(Clone, Debug)]
34pub struct Humanizer {
35    units: Vec<String>,
36    space_before_unit: bool,
37    division_factor: f64,
38}
39
40impl Humanizer {
41    /// Creates a new humanizer with the given units.
42    ///
43    /// Note: the first unit is the default one so it's usually empty unless it's bytes.
44    ///
45    /// ## Arguments
46    ///
47    /// * `units` - The units to use when humanizing numbers.
48    ///
49    /// ## Returns
50    ///
51    /// A new humanizer with the given units.
52    ///
53    /// ## Panics
54    ///
55    /// Panics if `units` is empty.
56    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    /// Sets whether or not to add a space before the unit (default: `true`).
67    /// Example: `true` -> "1 MB", `false` -> "1MB".
68    #[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    /// Sets the division factor between units (default: `1000.0`).
75    /// Example: Use `1024.0` for binary prefixes (KiB, MiB, etc.).
76    ///
77    /// ## Panics
78    ///
79    /// Panics if the division factor is less than or equal to 0.
80    #[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    /// Calculates the number and index of the unit to use when humanizing a number.
94    ///
95    /// ## Returns
96    ///
97    /// * `f64` - The scaled number.
98    /// * `usize` - The index of the unit.
99    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    /// Formats a number into a human readable string using the humanizer's units.
120    ///
121    /// ## Example
122    ///
123    /// ```rust
124    /// use handy::human::Humanizer;
125    ///
126    /// let humanizer = Humanizer::new(&["", "k", "m", "b", "t"]).with_space_before_unit(false);
127    /// assert_eq!(humanizer.format(123_456_789), "123m");
128    /// ```
129    ///
130    /// ## Arguments
131    ///
132    /// * `value` - The value to format.
133    ///
134    /// ## Returns
135    ///
136    /// A human readable string using the humanizer's units.
137    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    /// Formats a number into a human readable string using the humanizer's units but returns the value and the unit.
164    ///
165    /// ## Example
166    ///
167    /// ```rust
168    /// use handy::human::Humanizer;
169    ///
170    /// let humanizer = Humanizer::new(&["", "k", "m", "b", "t"]);
171    /// assert_eq!(humanizer.format_as_parts(123_456_789), (123.456789, "m"));
172    /// ```
173    ///
174    /// ## Arguments
175    ///
176    /// * `value` - The value to format.
177    ///
178    /// ## Returns
179    ///
180    /// * `f64` - The value.
181    /// * `&str` - The unit.
182    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/// Formats bytes into a human readable string.
192///
193/// ## Examples
194///
195/// ```rust,no_run
196/// use handy::human::human_bytes;
197///
198/// assert_eq!(human_bytes(123_456_789), "118 MiB");
199/// ```
200#[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/// Formats bytes into a human readable string and its unit.
209///
210/// ## Examples
211///
212/// ```rust,no_run
213/// use handy::human::human_bytes_as_parts;
214///
215/// assert_eq!(human_bytes_as_parts(123_456_789), (118.0, "MiB"));
216/// ```
217#[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/// Formats bytes into a human readable string using SI units.
226///
227/// ## Examples
228///
229/// ```rust,no_run
230/// use handy::human::human_bytes_si;
231///
232/// assert_eq!(human_bytes_si(123_456_789), "118 MB");
233/// ```
234#[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/// Formats bytes into a human readable string and its unit using SI units.
243///
244/// ## Examples
245///
246/// ```rust,no_run
247/// use handy::human::human_bytes_si_as_parts;
248///
249/// assert_eq!(human_bytes_si_as_parts(123_456_789), (118.0, "MB"));
250/// ```
251#[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/// Formats a number into a human readable string.
260///
261/// ## Examples
262///
263/// ```rust,no_run
264/// use handy::human::human_number;
265///
266/// assert_eq!(human_number(123_456_789), "123 M");
267/// ```
268#[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
276/// Formats a number into a human readable string and its unit.
277///
278/// ## Examples
279///
280/// ```rust,no_run
281/// use handy::human::human_number_as_parts;
282///
283/// assert_eq!(human_number_as_parts(123_456_789), (123.0, "M"));
284/// ```
285pub 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}