Skip to main content

bytesize/
display.rs

1use core::{fmt, write};
2
3use crate::ByteSize;
4
5const BITS_PER_BYTE: u64 = 8;
6
7/// Format / style to use when displaying a [`ByteSize`].
8#[derive(Debug, Clone, Copy)]
9pub(crate) enum Format {
10    Iec,
11    IecShort,
12    Si,
13    SiShort,
14    IecBits,
15    SiBits,
16}
17
18impl Format {
19    fn unit(self) -> u64 {
20        match self {
21            Format::Iec | Format::IecShort => crate::KIB,
22            Format::Si | Format::SiShort => crate::KB,
23            Format::IecBits => crate::KIB,
24            Format::SiBits => crate::KB,
25        }
26    }
27
28    fn unit_base(self) -> f64 {
29        match self {
30            Format::Iec | Format::IecShort => crate::LN_KIB,
31            Format::Si | Format::SiShort => crate::LN_KB,
32            Format::IecBits => crate::LN_KIB,
33            Format::SiBits => crate::LN_KB,
34        }
35    }
36
37    fn unit_prefixes(self) -> &'static [u8] {
38        match self {
39            Format::Iec | Format::IecShort | Format::IecBits => crate::UNITS_IEC.as_bytes(),
40            Format::Si | Format::SiShort | Self::SiBits => crate::UNITS_SI.as_bytes(),
41        }
42    }
43
44    fn unit_separator(self) -> &'static str {
45        match self {
46            Format::Iec | Format::Si | Format::IecBits | Format::SiBits => " ",
47            Format::IecShort | Format::SiShort => "",
48        }
49    }
50
51    fn unit_suffix(self) -> &'static str {
52        match self {
53            Format::Iec => "iB",
54            Format::Si => "B",
55            Format::IecShort | Format::SiShort => "",
56            Format::IecBits => "ib",
57            Format::SiBits => "b",
58        }
59    }
60
61    fn is_bits(self) -> bool {
62        matches!(self, Format::IecBits | Format::SiBits)
63    }
64}
65
66/// Formatting display wrapper for [`ByteSize`].
67///
68/// Supports various styles, see methods. By default, the [`iec()`](Self::iec()) style is used.
69///
70/// # Examples
71///
72/// ```
73/// # use bytesize::ByteSize;
74/// assert_eq!(
75///     "1.0 MiB",
76///     ByteSize::mib(1).display().iec().to_string(),
77/// );
78///
79/// assert_eq!(
80///     "42.0k",
81///     ByteSize::kb(42).display().si_short().to_string(),
82/// );
83/// ```
84#[derive(Debug, Clone)]
85pub struct Display {
86    pub(crate) byte_size: ByteSize,
87    pub(crate) format: Format,
88}
89
90impl Display {
91    /// Format using IEC (binary) units.
92    ///
93    /// E.g., `11.8 MiB`.
94    #[must_use]
95    #[doc(alias = "binary")]
96    pub fn iec(mut self) -> Self {
97        self.format = Format::Iec;
98        self
99    }
100
101    /// Format using a short style and IEC (binary) units.
102    ///
103    /// E.g., `11.8M`.
104    ///
105    /// Designed to produce output compatible with `sort -h`.
106    #[must_use]
107    #[doc(alias = "binary")]
108    pub fn iec_short(mut self) -> Self {
109        self.format = Format::IecShort;
110        self
111    }
112
113    /// Format using SI (decimal) units.
114    ///
115    /// E.g., `12.3 MB`.
116    #[must_use]
117    #[doc(alias = "decimal")]
118    pub fn si(mut self) -> Self {
119        self.format = Format::Si;
120        self
121    }
122
123    /// Format using a short style and SI (decimal) units.
124    ///
125    /// E.g., `12.3M`.
126    #[must_use]
127    #[doc(alias = "decimal")]
128    pub fn si_short(mut self) -> Self {
129        self.format = Format::SiShort;
130        self
131    }
132
133    /// Format as equivalent number of bits using IEC (binary) units.
134    ///
135    /// E.g., `12.3 Mib`.
136    #[must_use]
137    pub fn iec_bits(mut self) -> Self {
138        self.format = Format::IecBits;
139        self
140    }
141
142    /// Format as equivalent number of bits using SI (decimal) units.
143    ///
144    /// E.g., `12.3 Mb`.
145    #[must_use]
146    pub fn si_bits(mut self) -> Self {
147        self.format = Format::SiBits;
148        self
149    }
150}
151
152impl fmt::Display for Display {
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        let bytes = self.byte_size.as_u64();
155
156        let is_bits = self.format.is_bits();
157
158        let unit = self.format.unit();
159        #[allow(unused_variables)] // used in std contexts
160        let unit_base = self.format.unit_base();
161
162        let unit_prefixes = self.format.unit_prefixes();
163        let unit_separator = self.format.unit_separator();
164        let unit_suffix = self.format.unit_suffix();
165        let precision = f.precision().unwrap_or(1);
166
167        let threshold = if is_bits {
168            unit.div_ceil(BITS_PER_BYTE)
169        } else {
170            unit
171        };
172
173        if bytes < threshold {
174            let value = if is_bits {
175                bytes * BITS_PER_BYTE
176            } else {
177                bytes
178            };
179
180            if is_bits {
181                write!(f, "{value}{unit_separator}b")?;
182            } else {
183                write!(f, "{value}{unit_separator}B")?;
184            }
185        } else {
186            let size = bytes as f64 * if is_bits { BITS_PER_BYTE as f64 } else { 1.0 };
187
188            #[cfg(feature = "std")]
189            let exp = ideal_unit_std(size, unit, unit_base);
190
191            #[cfg(not(feature = "std"))]
192            let exp = ideal_unit_no_std(size, unit);
193
194            let unit_prefix = unit_prefixes[exp - 1] as char;
195
196            write!(
197                f,
198                "{:.precision$}{unit_separator}{unit_prefix}{unit_suffix}",
199                (size / unit.pow(exp as u32) as f64),
200            )?;
201        }
202
203        Ok(())
204    }
205}
206
207#[allow(dead_code)] // used in no-std contexts
208fn ideal_unit_no_std(size: f64, unit: u64) -> usize {
209    assert!(size >= unit as f64, "only called when bytes >= unit");
210
211    let mut ideal_prefix = 0;
212    let mut ideal_size = size;
213
214    loop {
215        ideal_prefix += 1;
216        ideal_size /= unit as f64;
217
218        if ideal_size < unit as f64 {
219            break;
220        }
221    }
222
223    ideal_prefix
224}
225
226#[cfg(feature = "std")]
227#[allow(dead_code)] // used in std contexts
228fn ideal_unit_std(size: f64, unit: u64, unit_base: f64) -> usize {
229    assert!(size >= unit as f64, "only called when bytes >= unit");
230
231    // `ln()` is a fast approximation, but it's not precise enough to trust at power-of-`unit`
232    // boundaries: `f64::ln` can round such that `size.ln() / unit_base` lands one exponent above
233    // or below the correct value (see #142), which previously could underflow `exp - 1` and panic,
234    // or silently pick the wrong unit prefix. Nudge the approximation to the exact boundary using
235    // integer-exact `powi` checks, matching the (slower but exact) loop in `ideal_unit_no_std`.
236    let unit = unit as f64;
237    let mut exp = ((size.ln() / unit_base) as isize).max(1);
238
239    while exp > 1 && size / unit.powi(exp as i32 - 1) < unit {
240        exp -= 1;
241    }
242    while size / unit.powi(exp as i32) >= unit {
243        exp += 1;
244    }
245
246    exp as usize
247}
248
249#[cfg(test)]
250mod tests {
251    use alloc::{format, string::ToString as _};
252    use core::fmt::Write as _;
253
254    use super::*;
255
256    #[cfg(feature = "std")]
257    quickcheck::quickcheck! {
258        #[test]
259        fn ideal_unit_selection_std_no_std_iec(bytes: ByteSize) -> bool {
260            if bytes.0 < 1025 {
261                return true;
262            }
263
264            let size = bytes.0 as f64;
265
266            ideal_unit_std(size, crate::KIB, crate::LN_KIB) == ideal_unit_no_std(size, crate::KIB)
267        }
268
269        #[test]
270        fn ideal_unit_selection_std_no_std_si(bytes: ByteSize) -> bool {
271            if bytes.0 < 1025 {
272                return true;
273            }
274
275            let size = bytes.0 as f64;
276
277            ideal_unit_std(size, crate::KB, crate::LN_KB) == ideal_unit_no_std(size, crate::KB)
278        }
279
280        #[test]
281        fn ideal_unit_selection_std_no_std_iec_bits(bytes: ByteSize) -> bool {
282            if bytes.0 < 128 {
283                return true;
284            }
285
286            let size = bytes.0 as f64 * BITS_PER_BYTE as f64;
287
288            ideal_unit_std(size, crate::KIB, crate::LN_KIB)
289                == ideal_unit_no_std(size, crate::KIB)
290        }
291
292        #[test]
293        fn ideal_unit_selection_std_no_std_si_bits(bytes: ByteSize) -> bool {
294            if bytes.0 < 125 {
295                return true;
296            }
297
298            let size = bytes.0 as f64 * BITS_PER_BYTE as f64;
299
300            ideal_unit_std(size, crate::KB, crate::LN_KB) == ideal_unit_no_std(size, crate::KB)
301        }
302    }
303
304    // Regression test for #142 / the `std` vs `no_std` display divergence: `f64::ln()` isn't
305    // precise enough to trust right at a power-of-`unit` boundary, so `ideal_unit_std` used to
306    // disagree with the exact, loop-based `ideal_unit_no_std` for sizes just below 1024^5 bytes
307    // (and the equivalent 1000^5 boundary for SI units) — previously "1.0 PiB" under the `std`
308    // feature vs "1024.0 TiB" without it, for the exact same byte count.
309    #[cfg(feature = "std")]
310    #[test]
311    fn ideal_unit_std_matches_no_std_near_pebi_boundary() {
312        for bytes in [
313            1_125_899_906_842_621u64, // 1024^5 - 3
314            1_125_899_906_842_622,    // 1024^5 - 2
315            1_125_899_906_842_623,    // 1024^5 - 1
316            1_125_899_906_842_624,    // 1024^5 exactly
317        ] {
318            let size = bytes as f64;
319            assert_eq!(
320                ideal_unit_std(size, crate::KIB, crate::LN_KIB),
321                ideal_unit_no_std(size, crate::KIB),
322                "mismatch at {bytes} bytes (IEC)",
323            );
324        }
325
326        for bytes in [
327            999_999_999_999_996u64, // 1000^5 - 4
328            999_999_999_999_997,    // 1000^5 - 3
329            999_999_999_999_998,    // 1000^5 - 2
330            999_999_999_999_999,    // 1000^5 - 1
331            1_000_000_000_000_000,  // 1000^5 exactly
332        ] {
333            let size = bytes as f64;
334            assert_eq!(
335                ideal_unit_std(size, crate::KB, crate::LN_KB),
336                ideal_unit_no_std(size, crate::KB),
337                "mismatch at {bytes} bytes (SI)",
338            );
339        }
340    }
341
342    #[test]
343    fn display_matches_just_below_pebi_boundary() {
344        assert_eq!(
345            "1024.0 TiB",
346            Display {
347                byte_size: ByteSize(1_125_899_906_842_623),
348                format: Format::Iec,
349            }
350            .to_string()
351        );
352        assert_eq!(
353            "1.0 PiB",
354            Display {
355                byte_size: ByteSize(1_125_899_906_842_624),
356                format: Format::Iec,
357            }
358            .to_string()
359        );
360    }
361
362    #[track_caller]
363    fn assert_to_string(expected: &str, byte_size: ByteSize, format: Format) {
364        assert_eq!(expected, Display { byte_size, format }.to_string());
365    }
366
367    #[test]
368    fn to_string_iec() {
369        let display = Display {
370            byte_size: ByteSize::gib(1),
371            format: Format::Iec,
372        };
373        assert_eq!("1.0 GiB", display.to_string());
374
375        let display = Display {
376            byte_size: ByteSize::gb(1),
377            format: Format::Iec,
378        };
379        assert_eq!("953.7 MiB", display.to_string());
380    }
381
382    #[test]
383    fn to_string_si() {
384        let display = Display {
385            byte_size: ByteSize::gib(1),
386            format: Format::Si,
387        };
388        assert_eq!("1.1 GB", display.to_string());
389
390        let display = Display {
391            byte_size: ByteSize::gb(1),
392            format: Format::Si,
393        };
394        assert_eq!("1.0 GB", display.to_string());
395    }
396
397    #[test]
398    fn to_string_short() {
399        let display = Display {
400            byte_size: ByteSize::gib(1),
401            format: Format::IecShort,
402        };
403        assert_eq!("1.0G", display.to_string());
404
405        let display = Display {
406            byte_size: ByteSize::gb(1),
407            format: Format::IecShort,
408        };
409        assert_eq!("953.7M", display.to_string());
410    }
411
412    #[test]
413    fn test_to_string_as() {
414        assert_to_string("215 B", ByteSize::b(215), Format::Iec);
415        assert_to_string("215 B", ByteSize::b(215), Format::Si);
416
417        assert_to_string("1.0 KiB", ByteSize::kib(1), Format::Iec);
418        assert_to_string("1.0 kB", ByteSize::kib(1), Format::Si);
419
420        assert_to_string("293.9 KiB", ByteSize::kb(301), Format::Iec);
421        assert_to_string("301.0 kB", ByteSize::kb(301), Format::Si);
422
423        assert_to_string("1.0 MiB", ByteSize::mib(1), Format::Iec);
424        assert_to_string("1.0 MB", ByteSize::mib(1), Format::Si);
425
426        assert_to_string("1.9 GiB", ByteSize::mib(1907), Format::Iec);
427        assert_to_string("2.0 GB", ByteSize::mib(1908), Format::Si);
428
429        assert_to_string("399.6 MiB", ByteSize::mb(419), Format::Iec);
430        assert_to_string("419.0 MB", ByteSize::mb(419), Format::Si);
431
432        assert_to_string("482.4 GiB", ByteSize::gb(518), Format::Iec);
433        assert_to_string("518.0 GB", ByteSize::gb(518), Format::Si);
434
435        assert_to_string("741.2 TiB", ByteSize::tb(815), Format::Iec);
436        assert_to_string("815.0 TB", ByteSize::tb(815), Format::Si);
437
438        assert_to_string("540.9 PiB", ByteSize::pb(609), Format::Iec);
439        assert_to_string("609.0 PB", ByteSize::pb(609), Format::Si);
440    }
441
442    #[test]
443    fn to_string_bits() {
444        assert_to_string("1016 b", ByteSize(127), Format::IecBits);
445        assert_to_string("1.0 Kib", ByteSize(128), Format::IecBits);
446        assert_to_string("992 b", ByteSize(124), Format::SiBits);
447        assert_to_string("1.0 kb", ByteSize(125), Format::SiBits);
448
449        assert_to_string("7.8 Kib", ByteSize::kb(1), Format::IecBits);
450        assert_to_string("8.0 kb", ByteSize::kb(1), Format::SiBits);
451        assert_to_string("128.0 Eib", ByteSize(u64::MAX), Format::IecBits);
452        assert_to_string("147.6 Eb", ByteSize(u64::MAX), Format::SiBits);
453    }
454
455    #[test]
456    fn display_bits_public_api() {
457        assert_eq!("1.0 Kib", ByteSize(128).display().iec_bits().to_string());
458        assert_eq!("1.0 kb", ByteSize(125).display().si_bits().to_string());
459    }
460
461    #[test]
462    fn display_propagates_write_errors() {
463        struct FailingWriter;
464
465        impl fmt::Write for FailingWriter {
466            fn write_str(&mut self, _: &str) -> fmt::Result {
467                Err(fmt::Error)
468            }
469        }
470
471        let mut writer = FailingWriter;
472
473        assert_eq!(
474            Err(fmt::Error),
475            write!(
476                writer,
477                "{}",
478                Display {
479                    byte_size: ByteSize(127),
480                    format: Format::IecBits,
481                },
482            ),
483        );
484        assert_eq!(
485            Err(fmt::Error),
486            write!(
487                writer,
488                "{}",
489                Display {
490                    byte_size: ByteSize(1),
491                    format: Format::Iec,
492                },
493            ),
494        );
495    }
496
497    #[test]
498    fn precision() {
499        let size = ByteSize::mib(1908);
500        assert_eq!("1.9 GiB".to_string(), format!("{size}"));
501        assert_eq!("2 GiB".to_string(), format!("{size:.0}"));
502        assert_eq!("1.86328 GiB".to_string(), format!("{size:.5}"));
503    }
504}