bytesize/
display.rs

1use core::{fmt, write};
2
3use crate::ByteSize;
4
5/// Format / style to use when displaying a [`ByteSize`].
6#[derive(Debug, Clone, Copy)]
7pub(crate) enum Format {
8    Iec,
9    IecShort,
10    Si,
11    SiShort,
12}
13
14impl Format {
15    fn unit(self) -> u64 {
16        match self {
17            Format::Iec | Format::IecShort => crate::KIB,
18            Format::Si | Format::SiShort => crate::KB,
19        }
20    }
21
22    fn unit_base(self) -> f64 {
23        match self {
24            Format::Iec | Format::IecShort => crate::LN_KIB,
25            Format::Si | Format::SiShort => crate::LN_KB,
26        }
27    }
28
29    fn unit_prefixes(self) -> &'static [u8] {
30        match self {
31            Format::Iec | Format::IecShort => crate::UNITS_IEC.as_bytes(),
32            Format::Si | Format::SiShort => crate::UNITS_SI.as_bytes(),
33        }
34    }
35
36    fn unit_separator(self) -> &'static str {
37        match self {
38            Format::Iec | Format::Si => " ",
39            Format::IecShort | Format::SiShort => "",
40        }
41    }
42
43    fn unit_suffix(self) -> &'static str {
44        match self {
45            Format::Iec => "iB",
46            Format::Si => "B",
47            Format::IecShort | Format::SiShort => "",
48        }
49    }
50}
51
52/// Formatting display wrapper for [`ByteSize`].
53///
54/// Supports various styles, see methods. By default, the [`iec()`](Self::iec()) style is used.
55///
56/// # Examples
57///
58/// ```
59/// # use bytesize::ByteSize;
60/// assert_eq!(
61///     "1.0 MiB",
62///     ByteSize::mib(1).display().iec().to_string(),
63/// );
64///
65/// assert_eq!(
66///     "42.0k",
67///     ByteSize::kb(42).display().si_short().to_string(),
68/// );
69/// ```
70#[derive(Debug, Clone)]
71pub struct Display {
72    pub(crate) byte_size: ByteSize,
73    pub(crate) format: Format,
74}
75
76impl Display {
77    /// Format using IEC (binary) units.
78    ///
79    /// E.g., `11.8 MiB`.
80    #[must_use]
81    #[doc(alias = "binary")]
82    pub fn iec(mut self) -> Self {
83        self.format = Format::Iec;
84        self
85    }
86
87    /// Format using a short style and IEC (binary) units.
88    ///
89    /// E.g., `11.8M`.
90    ///
91    /// Designed to produce output compatible with `sort -h`.
92    #[must_use]
93    #[doc(alias = "binary")]
94    pub fn iec_short(mut self) -> Self {
95        self.format = Format::IecShort;
96        self
97    }
98
99    /// Format using SI (decimal) units.
100    ///
101    /// E.g., `12.3 MB`.
102    #[must_use]
103    #[doc(alias = "decimal")]
104    pub fn si(mut self) -> Self {
105        self.format = Format::Si;
106        self
107    }
108
109    /// Format using a short style and SI (decimal) units.
110    ///
111    /// E.g., `12.3M`.
112    #[must_use]
113    #[doc(alias = "decimal")]
114    pub fn si_short(mut self) -> Self {
115        self.format = Format::SiShort;
116        self
117    }
118}
119
120impl fmt::Display for Display {
121    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122        let bytes = self.byte_size.as_u64();
123
124        let unit = self.format.unit();
125        #[allow(unused_variables)] // used in std contexts
126        let unit_base = self.format.unit_base();
127
128        let unit_prefixes = self.format.unit_prefixes();
129        let unit_separator = self.format.unit_separator();
130        let unit_suffix = self.format.unit_suffix();
131        let precision = f.precision().unwrap_or(1);
132
133        if bytes < unit {
134            write!(f, "{bytes}{unit_separator}B")?;
135        } else {
136            let size = bytes as f64;
137
138            #[cfg(feature = "std")]
139            let exp = ideal_unit_std(size, unit_base);
140
141            #[cfg(not(feature = "std"))]
142            let exp = ideal_unit_no_std(size, unit);
143
144            let unit_prefix = unit_prefixes[exp - 1] as char;
145
146            write!(
147                f,
148                "{:.precision$}{unit_separator}{unit_prefix}{unit_suffix}",
149                (size / unit.pow(exp as u32) as f64),
150            )?;
151        }
152
153        Ok(())
154    }
155}
156
157#[allow(dead_code)] // used in no-std contexts
158fn ideal_unit_no_std(size: f64, unit: u64) -> usize {
159    assert!(size >= unit as f64, "only called when bytes >= unit");
160
161    let mut ideal_prefix = 0;
162    let mut ideal_size = size;
163
164    loop {
165        ideal_prefix += 1;
166        ideal_size /= unit as f64;
167
168        if ideal_size < unit as f64 {
169            break;
170        }
171    }
172
173    ideal_prefix
174}
175
176#[cfg(feature = "std")]
177#[allow(dead_code)] // used in std contexts
178fn ideal_unit_std(size: f64, unit_base: f64) -> usize {
179    assert!(size.ln() >= unit_base, "only called when bytes >= unit");
180
181    match (size.ln() / unit_base) as usize {
182        0 => unreachable!(),
183        e => e,
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use alloc::{format, string::ToString as _};
190
191    use super::*;
192
193    #[cfg(feature = "std")]
194    quickcheck::quickcheck! {
195        #[test]
196        fn ideal_unit_selection_std_no_std_iec(bytes: ByteSize) -> bool {
197            if bytes.0 < 1025 {
198                return true;
199            }
200
201            let size = bytes.0 as f64;
202
203            ideal_unit_std(size, crate::LN_KIB) == ideal_unit_no_std(size, crate::KIB)
204        }
205
206        #[test]
207        fn ideal_unit_selection_std_no_std_si(bytes: ByteSize) -> bool {
208            if bytes.0 < 1025 {
209                return true;
210            }
211
212            let size = bytes.0 as f64;
213
214            ideal_unit_std(size, crate::LN_KB) == ideal_unit_no_std(size, crate::KB)
215        }
216    }
217
218    #[test]
219    fn to_string_iec() {
220        let display = Display {
221            byte_size: ByteSize::gib(1),
222            format: Format::Iec,
223        };
224        assert_eq!("1.0 GiB", display.to_string());
225
226        let display = Display {
227            byte_size: ByteSize::gb(1),
228            format: Format::Iec,
229        };
230        assert_eq!("953.7 MiB", display.to_string());
231    }
232
233    #[test]
234    fn to_string_si() {
235        let display = Display {
236            byte_size: ByteSize::gib(1),
237            format: Format::Si,
238        };
239        assert_eq!("1.1 GB", display.to_string());
240
241        let display = Display {
242            byte_size: ByteSize::gb(1),
243            format: Format::Si,
244        };
245        assert_eq!("1.0 GB", display.to_string());
246    }
247
248    #[test]
249    fn to_string_short() {
250        let display = Display {
251            byte_size: ByteSize::gib(1),
252            format: Format::IecShort,
253        };
254        assert_eq!("1.0G", display.to_string());
255
256        let display = Display {
257            byte_size: ByteSize::gb(1),
258            format: Format::IecShort,
259        };
260        assert_eq!("953.7M", display.to_string());
261    }
262
263    #[track_caller]
264    fn assert_to_string(expected: &str, byte_size: ByteSize, format: Format) {
265        assert_eq!(expected, Display { byte_size, format }.to_string());
266    }
267
268    #[test]
269    fn test_to_string_as() {
270        assert_to_string("215 B", ByteSize::b(215), Format::Iec);
271        assert_to_string("215 B", ByteSize::b(215), Format::Si);
272
273        assert_to_string("1.0 KiB", ByteSize::kib(1), Format::Iec);
274        assert_to_string("1.0 kB", ByteSize::kib(1), Format::Si);
275
276        assert_to_string("293.9 KiB", ByteSize::kb(301), Format::Iec);
277        assert_to_string("301.0 kB", ByteSize::kb(301), Format::Si);
278
279        assert_to_string("1.0 MiB", ByteSize::mib(1), Format::Iec);
280        assert_to_string("1.0 MB", ByteSize::mib(1), Format::Si);
281
282        assert_to_string("1.9 GiB", ByteSize::mib(1907), Format::Iec);
283        assert_to_string("2.0 GB", ByteSize::mib(1908), Format::Si);
284
285        assert_to_string("399.6 MiB", ByteSize::mb(419), Format::Iec);
286        assert_to_string("419.0 MB", ByteSize::mb(419), Format::Si);
287
288        assert_to_string("482.4 GiB", ByteSize::gb(518), Format::Iec);
289        assert_to_string("518.0 GB", ByteSize::gb(518), Format::Si);
290
291        assert_to_string("741.2 TiB", ByteSize::tb(815), Format::Iec);
292        assert_to_string("815.0 TB", ByteSize::tb(815), Format::Si);
293
294        assert_to_string("540.9 PiB", ByteSize::pb(609), Format::Iec);
295        assert_to_string("609.0 PB", ByteSize::pb(609), Format::Si);
296    }
297
298    #[test]
299    fn precision() {
300        let size = ByteSize::mib(1908);
301        assert_eq!("1.9 GiB".to_string(), format!("{size}"));
302        assert_eq!("2 GiB".to_string(), format!("{size:.0}"));
303        assert_eq!("1.86328 GiB".to_string(), format!("{size:.5}"));
304    }
305}