human_units/
size_format.rs

1use core::fmt::Display;
2use core::fmt::Formatter;
3use core::fmt::Write;
4
5use crate::Buffer;
6use crate::Size;
7
8/**
9Approximate size that includes unit, integral and fractional parts as fields.
10
11This type is useful when you need custom formatting of the output,
12i.e. colors, locale-specific units etc.
13*/
14pub struct FormattedSize {
15    /// Size unit.
16    pub unit: &'static str,
17    /// Integral part. Max. value is 1023.
18    pub integer: u16,
19    /// Fractional part. Max. value is 9.
20    pub fraction: u8,
21}
22
23impl Display for FormattedSize {
24    fn fmt(&self, f: &mut Formatter) -> core::fmt::Result {
25        let mut buf = Buffer::<MAX_LEN>::new();
26        buf.write_u64(self.integer as u64);
27        if self.fraction != 0 {
28            buf.write_byte(b'.');
29            buf.write_byte(b'0' + self.fraction);
30        }
31        buf.write_byte(b' ');
32        buf.write_str(self.unit)?;
33        f.write_str(unsafe { buf.as_str() })
34    }
35}
36
37const MAX_LEN: usize = 10;
38
39/**
40This trait adds [`format_size`](FormatSize::format_size) method
41to primitive [`u64`](core::u64) and [`usize`](core::usize) types.
42*/
43pub trait FormatSize {
44    /// Splits the original size into integral, fractional and adds a unit.
45    fn format_size(self) -> FormattedSize;
46}
47
48impl FormatSize for u64 {
49    fn format_size(self) -> FormattedSize {
50        let mut i = 0;
51        let mut scale = 1;
52        let mut n = self;
53        while n >= 1024 {
54            scale *= 1024;
55            n >>= 10;
56            i += 1;
57        }
58        let mut b = self & (scale - 1);
59        if b != 0 {
60            // compute the first digit of the fractional part
61            b = (b * 10_u64) >> (i * 10);
62        }
63        FormattedSize {
64            unit: UNITS[i],
65            integer: n as u16,
66            fraction: b as u8,
67        }
68    }
69}
70
71impl FormatSize for usize {
72    fn format_size(self) -> FormattedSize {
73        FormatSize::format_size(self as u64)
74    }
75}
76
77impl FormatSize for Size {
78    fn format_size(self) -> FormattedSize {
79        FormatSize::format_size(self.0)
80    }
81}
82
83const UNITS: [&str; 7] = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"];
84
85#[cfg(all(test, feature = "std"))]
86mod tests {
87    #![allow(clippy::panic)]
88    use arbitrary::Arbitrary;
89    use arbitrary::Unstructured;
90    use arbtest::arbtest;
91
92    use super::*;
93    use crate::FormatSize;
94
95    #[test]
96    fn test_format_bytes() {
97        assert_eq!("512 B", 512_u64.format_size().to_string());
98        assert_eq!("0 B", 0_u64.format_size().to_string());
99        assert_eq!("1 B", 1_u64.format_size().to_string());
100        assert_eq!("1 KiB", 1024_u64.format_size().to_string());
101        assert_eq!("512 KiB", (512_u64 * 1024).format_size().to_string());
102        assert_eq!("1023 B", 1023_u64.format_size().to_string());
103        assert_eq!("1023 KiB", (1023_u64 * 1024).format_size().to_string());
104        assert_eq!("1 MiB", (1024_u64 * 1024).format_size().to_string());
105        assert_eq!("1 GiB", (1024_u64 * 1024 * 1024).format_size().to_string());
106        assert_eq!(
107            "1023 MiB",
108            (1024_u64 * 1024 * 1023).format_size().to_string()
109        );
110        assert_eq!(
111            "3.5 GiB",
112            (1024_u64 * 1024 * 1024 * 3 + 1024_u64 * 1024 * 1024 / 2)
113                .format_size()
114                .to_string()
115        );
116        assert_eq!("3.9 GiB", (u32::MAX as u64).format_size().to_string());
117        assert_eq!("15.9 EiB", u64::MAX.format_size().to_string());
118    }
119
120    #[test]
121    fn test_format_bytes_arbitrary() {
122        arbtest(|u| {
123            let expected: u64 = u.arbitrary()?;
124            let bytes = expected.format_size();
125            let x = unit_to_factor(bytes.unit);
126            let actual = (bytes.integer as u64) * x + (bytes.fraction as u64) * x / 10;
127            assert!(
128                expected >= actual && (expected - actual) < x,
129                "expected = {expected}, actual = {actual}"
130            );
131            Ok(())
132        });
133    }
134
135    #[test]
136    fn test_shift_division() {
137        arbtest(|u| {
138            let number: u64 = u.arbitrary()?;
139            let expected = number / 1024;
140            let actual = number >> 10;
141            assert_eq!(expected, actual);
142            Ok(())
143        });
144    }
145
146    #[test]
147    fn test_shift_remainder() {
148        arbtest(|u| {
149            let number: u64 = u.arbitrary()?;
150            let expected = number % 1024;
151            let actual = number & (1024 - 1);
152            assert_eq!(expected, actual);
153            Ok(())
154        });
155    }
156
157    #[test]
158    fn test_formatted_size_io() {
159        arbtest(|u| {
160            let expected: FormattedSize = u.arbitrary()?;
161            let string = expected.to_string();
162            let mut words = string.splitn(2, ' ');
163            let number_str = words.next().unwrap();
164            let unit = words.next().unwrap().to_string();
165            let mut words = number_str.splitn(2, '.');
166            let integer: u16 = words.next().unwrap().parse().unwrap();
167            let fraction: u8 = match words.next() {
168                Some(word) => word.parse().unwrap(),
169                None => 0,
170            };
171            assert_eq!(expected.integer, integer);
172            assert_eq!(expected.fraction, fraction);
173            assert_eq!(expected.unit, unit, "expected = `{expected}`");
174            Ok(())
175        });
176    }
177
178    impl<'a> Arbitrary<'a> for FormattedSize {
179        fn arbitrary(u: &mut Unstructured<'a>) -> Result<Self, arbitrary::Error> {
180            Ok(Self {
181                unit: *u.choose(&UNITS[..])?,
182                integer: u.int_in_range(0..=MAX_INTEGER)?,
183                fraction: u.int_in_range(0..=9)?,
184            })
185        }
186    }
187
188    fn unit_to_factor(unit: &str) -> u64 {
189        match unit {
190            "B" => 1_u64,
191            "KiB" => 1024_u64,
192            "MiB" => 1024_u64.pow(2),
193            "GiB" => 1024_u64.pow(3),
194            "TiB" => 1024_u64.pow(4),
195            "PiB" => 1024_u64.pow(5),
196            "EiB" => 1024_u64.pow(6),
197            _ => panic!("unknown unit `{unit}`"),
198        }
199    }
200
201    const MAX_INTEGER: u16 = 1023;
202}