Skip to main content

bsize/
display.rs

1// Copyright 2026 FastLabs Developers
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use core::fmt;
16use core::fmt::Write as _;
17
18use crate::BaseByteSize;
19use crate::ByteSize;
20
21/// Create a [`Display`] instance for displaying a byte size.
22///
23/// See [`Display`] for examples. Use [`Display::new`] when the byte count is already represented
24/// as an `f64`.
25pub fn display(size: impl BaseByteSize) -> Display {
26    Display::new(size.to_f64())
27}
28
29impl<T: BaseByteSize> ByteSize<T> {
30    /// Returns a [`Display`] wrapper.
31    ///
32    /// See [`Display`] for examples.
33    pub fn display(&self) -> Display {
34        Display::new(self.bytes().to_f64())
35    }
36}
37
38/// Display wrapper for formatting byte sizes as human-readable strings.
39///
40/// You may create this wrapper with [`Display::new`], [`display`], or [`ByteSize::display`], then
41/// pass custom [`DisplayOptions`] with [`Display::options`].
42///
43/// # Examples
44///
45/// Display with the [`DisplayOptions::BINARY`] and [`DisplayOptions::DECIMAL`] presets.
46///
47/// ```
48/// use bsize::BSize64;
49///
50/// assert_eq!(
51///     "41.0 KiB",
52///     BSize64::kb(42).display().to_string(), // default to binary
53/// );
54///
55/// assert_eq!("1.0 MiB", BSize64::mib(1).display().binary().to_string(),);
56///
57/// assert_eq!("42.0 kB", BSize64::kb(42).display().decimal().to_string(),);
58/// ```
59///
60/// The free [`display`] function accepts any supported integer byte size.
61///
62/// ```
63/// assert_eq!("1.5 KiB", bsize::display(1536u64).to_string());
64/// ```
65///
66/// Use [`Display::new`] when the byte count is already represented as an `f64`.
67///
68/// ```
69/// use bsize::Display;
70///
71/// assert_eq!("1.5 KiB", Display::new(1536.5).to_string());
72/// assert_eq!("1.54 kB", format!("{:.2}", Display::new(1536.5).decimal()));
73/// ```
74///
75/// Use standard formatter precision to control the number of fractional digits.
76///
77/// ```
78/// use bsize::BSize64;
79///
80/// assert_eq!("1.54 KiB", format!("{:.2}", BSize64::b(1575).display()));
81/// assert_eq!("1.575 KiB", format!("{:.3}", bsize::display(1613u64)));
82/// ```
83///
84/// Standard formatter width, fill, and alignment options are supported.
85///
86/// ```
87/// let size = bsize::display(1536u64);
88///
89/// assert_eq!("1.5 KiB   ", format!("{size:10}"));
90/// assert_eq!("   1.5 KiB", format!("{size:>10}"));
91/// assert_eq!(" 1.5 KiB  ", format!("{size:^10}"));
92/// assert_eq!("*1.5 KiB**", format!("{size:*^10}"));
93/// assert_eq!("**1.50 KiB", format!("{size:*>10.2}"));
94/// ```
95///
96/// Use [`DisplayOptions`] to choose a fixed scale or show values as bits.
97///
98/// ```
99/// use bsize::DisplayBaseUnit;
100/// use bsize::DisplayScale;
101///
102/// let as_kibits = bsize::display(1536u64).options(|opts| {
103///     opts.base_unit(DisplayBaseUnit::Bit)
104///         .scale(DisplayScale::Kilo)
105/// });
106///
107/// assert_eq!("12.0 Kibit", as_kibits.to_string());
108/// ```
109///
110/// Decimal units use a base of 1000 and SI prefixes.
111///
112/// ```
113/// use bsize::DisplayScale;
114/// use bsize::DisplayUnitSystem;
115///
116/// let display = bsize::display(1_500_000u64).options(|opts| {
117///     opts.unit_system(DisplayUnitSystem::Decimal)
118///         .scale(DisplayScale::Mega)
119/// });
120///
121/// assert_eq!("1.500 MB", format!("{display:.3}"));
122/// ```
123#[derive(Debug, Clone)]
124pub struct Display {
125    size: f64,
126    options: DisplayOptions,
127}
128
129/// Formatting options for [`Display`].
130///
131/// See [`Display`] for examples.
132#[derive(Debug, Clone, Copy)]
133pub struct DisplayOptions {
134    base_unit: DisplayBaseUnit,
135    scale: DisplayScale,
136    unit_system: DisplayUnitSystem,
137}
138
139impl DisplayOptions {
140    /// The default binary display options.
141    ///
142    /// See [`Display`] for examples.
143    pub const BINARY: Self = Self {
144        base_unit: DisplayBaseUnit::Byte,
145        scale: DisplayScale::Auto,
146        unit_system: DisplayUnitSystem::Binary,
147    };
148
149    /// Decimal display options.
150    ///
151    /// See [`Display`] for examples.
152    pub const DECIMAL: Self = Self {
153        base_unit: DisplayBaseUnit::Byte,
154        scale: DisplayScale::Auto,
155        unit_system: DisplayUnitSystem::Decimal,
156    };
157
158    /// Construct a new instance with the `BINARY` preset.
159    ///
160    /// See [`Display`] for examples.
161    #[inline(always)]
162    pub const fn new() -> Self {
163        DisplayOptions::BINARY
164    }
165
166    /// Set the base unit used for display.
167    ///
168    /// See [`Display`] for examples.
169    #[inline(always)]
170    pub const fn base_unit(mut self, base_unit: DisplayBaseUnit) -> Self {
171        self.base_unit = base_unit;
172        self
173    }
174
175    /// Set the display scale.
176    ///
177    /// See [`Display`] for examples.
178    #[inline(always)]
179    pub const fn scale(mut self, scale: DisplayScale) -> Self {
180        self.scale = scale;
181        self
182    }
183
184    /// Set the unit system used for display.
185    ///
186    /// See [`Display`] for examples.
187    #[inline(always)]
188    pub const fn unit_system(mut self, unit_system: DisplayUnitSystem) -> Self {
189        self.unit_system = unit_system;
190        self
191    }
192}
193
194impl Default for DisplayOptions {
195    /// Same as [`DisplayOptions::new`].
196    fn default() -> Self {
197        Self::new()
198    }
199}
200
201/// Base unit used by [`DisplayOptions`].
202///
203/// See [`Display`] for examples.
204#[derive(Debug, Clone, Copy, PartialEq, Eq)]
205#[non_exhaustive]
206pub enum DisplayBaseUnit {
207    /// Format values as bits.
208    ///
209    /// The byte count is converted to bits for display.
210    ///
211    /// # Examples
212    ///
213    /// ```
214    /// use bsize::DisplayBaseUnit;
215    ///
216    /// let display = bsize::display(1usize).options(|opts| opts.base_unit(DisplayBaseUnit::Bit));
217    ///
218    /// assert_eq!("8 bit", display.to_string());
219    /// ```
220    Bit,
221    /// Format values as bytes.
222    Byte,
223}
224
225/// Scale used by [`DisplayOptions`].
226///
227/// See [`Display`] for examples.
228#[derive(Debug, Clone, Copy, PartialEq, Eq)]
229#[non_exhaustive]
230pub enum DisplayScale {
231    /// Select the display scale automatically.
232    Auto,
233    /// Format values in the base unit without a prefix.
234    Base,
235    /// Format values in kilo units.
236    Kilo,
237    /// Format values in mega units.
238    Mega,
239    /// Format values in giga units.
240    Giga,
241    /// Format values in tera units.
242    Tera,
243    /// Format values in peta units.
244    Peta,
245    /// Format values in exa units.
246    Exa,
247}
248
249/// Unit system used by [`DisplayOptions`].
250///
251/// See [`Display`] for examples.
252#[derive(Debug, Clone, Copy, PartialEq, Eq)]
253#[non_exhaustive]
254pub enum DisplayUnitSystem {
255    /// Use the binary unit system, with a base of 1024.
256    Binary,
257    /// Use the decimal unit system, with a base of 1000.
258    Decimal,
259}
260
261impl Display {
262    /// Set the display option to the [`DisplayOptions::BINARY`] preset.
263    ///
264    /// See [`Display`] for examples.
265    pub fn binary(mut self) -> Self {
266        self.options = DisplayOptions::BINARY;
267        self
268    }
269
270    /// Set the display option to the [`DisplayOptions::DECIMAL`] preset.
271    ///
272    /// See [`Display`] for examples.
273    pub fn decimal(mut self) -> Self {
274        self.options = DisplayOptions::DECIMAL;
275        self
276    }
277
278    /// Set the options for display.
279    ///
280    /// The provided closure receives the current options, so customizations can build on the
281    /// default binary preset or preconfigured options.
282    ///
283    /// # Examples
284    ///
285    /// ```
286    /// use bsize::DisplayScale;
287    ///
288    /// let display = bsize::display(1536u64)
289    ///     .decimal()
290    ///     .options(|opts| opts.scale(DisplayScale::Kilo));
291    ///
292    /// assert_eq!("1.5 kB", display.to_string());
293    /// ```
294    ///
295    /// Use `|_| options` when the current options should be replaced as a whole.
296    ///
297    /// ```
298    /// use bsize::DisplayBaseUnit;
299    /// use bsize::DisplayOptions;
300    /// use bsize::DisplayScale;
301    ///
302    /// let network_units = DisplayOptions::DECIMAL
303    ///     .base_unit(DisplayBaseUnit::Bit)
304    ///     .scale(DisplayScale::Mega);
305    ///
306    /// let display = bsize::display(125_000u64).options(|_| network_units);
307    ///
308    /// assert_eq!("1.0 Mbit", display.to_string());
309    /// ```
310    pub fn options(mut self, f: impl FnOnce(DisplayOptions) -> DisplayOptions) -> Self {
311        self.options = f(self.options);
312        self
313    }
314
315    /// Create a [`Display`] instance from a byte count.
316    ///
317    /// This constructor is useful when the byte count is already represented as an `f64`. For
318    /// supported integer byte counts, use [`display`] or [`ByteSize::display`].
319    ///
320    /// # Examples
321    ///
322    /// ```
323    /// use bsize::Display;
324    ///
325    /// assert_eq!("2.5 KiB", Display::new(2560.0).to_string());
326    /// ```
327    ///
328    /// # Panics
329    ///
330    /// Panics if the `size` is NaN or negative.
331    pub fn new(size: f64) -> Self {
332        assert!(size >= 0.0, "size must be non-negative and not NaN");
333        let options = DisplayOptions::BINARY;
334        Self { size, options }
335    }
336}
337
338impl fmt::Display for Display {
339    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
340        let value = match self.options.base_unit {
341            DisplayBaseUnit::Bit => self.size * 8.0,
342            DisplayBaseUnit::Byte => self.size,
343        };
344        let divisor = match self.options.unit_system {
345            DisplayUnitSystem::Binary => 1024.0,
346            DisplayUnitSystem::Decimal => 1000.0,
347        };
348        let (value, exponent) = scaled_value(value, divisor, self.options.scale);
349        let precision = f.precision();
350
351        let Some(width) = f.width() else {
352            // fast path for no padding
353            return write_display(f, value, exponent, self.options, precision);
354        };
355
356        let mut counter = WidthCounter { width: 0 };
357        write_display(&mut counter, value, exponent, self.options, precision)?;
358
359        let padding = width.saturating_sub(counter.width);
360        let (left_padding, right_padding) = match f.align().unwrap_or(fmt::Alignment::Left) {
361            fmt::Alignment::Left => (0, padding),
362            fmt::Alignment::Right => (padding, 0),
363            fmt::Alignment::Center => (padding / 2, padding - padding / 2),
364        };
365
366        let fill = f.fill();
367        for _ in 0..left_padding {
368            f.write_char(fill)?;
369        }
370        write_display(f, value, exponent, self.options, precision)?;
371        for _ in 0..right_padding {
372            f.write_char(fill)?;
373        }
374        Ok(())
375    }
376}
377
378fn write_display(
379    f: &mut impl fmt::Write,
380    value: f64,
381    exponent: usize,
382    options: DisplayOptions,
383    precision: Option<usize>,
384) -> fmt::Result {
385    if let Some(precision) = precision {
386        write!(f, "{value:.precision$}")?;
387    } else if exponent == 0 {
388        write!(f, "{value}")?;
389    } else {
390        write!(f, "{value:.1}")?;
391    }
392
393    let unit_separator = " ";
394    f.write_str(unit_separator)?;
395
396    if exponent == 0 {
397        f.write_str(match options.base_unit {
398            DisplayBaseUnit::Bit => "bit",
399            DisplayBaseUnit::Byte => "B",
400        })
401    } else {
402        // Unit system references
403        // * https://en.wikipedia.org/wiki/Kilobyte
404        // * https://en.wikipedia.org/wiki/Bit#Multiple_bits
405        let unit_prefixes = match options.unit_system {
406            DisplayUnitSystem::Binary => b"KMGTPE",
407            DisplayUnitSystem::Decimal => b"kMGTPE",
408        };
409        let unit_suffix = match (options.unit_system, options.base_unit) {
410            (DisplayUnitSystem::Binary, DisplayBaseUnit::Bit) => "ibit",
411            (DisplayUnitSystem::Binary, DisplayBaseUnit::Byte) => "iB",
412            (DisplayUnitSystem::Decimal, DisplayBaseUnit::Bit) => "bit",
413            (DisplayUnitSystem::Decimal, DisplayBaseUnit::Byte) => "B",
414        };
415        let unit_prefix = unit_prefixes[exponent - 1] as char;
416        write!(f, "{unit_prefix}{unit_suffix}")
417    }
418}
419
420struct WidthCounter {
421    width: usize,
422}
423
424impl fmt::Write for WidthCounter {
425    fn write_str(&mut self, s: &str) -> fmt::Result {
426        // HACK - all display character is ASCII
427        self.width += s.len();
428        Ok(())
429    }
430
431    fn write_char(&mut self, _: char) -> fmt::Result {
432        self.width += 1;
433        Ok(())
434    }
435}
436
437fn scaled_value(mut value: f64, divisor: f64, scale: DisplayScale) -> (f64, usize) {
438    const MAX_EXPONENT: usize = 6;
439
440    let exponent = match scale {
441        DisplayScale::Auto => {
442            let mut exponent = 0;
443            while value >= divisor && exponent < MAX_EXPONENT {
444                value /= divisor;
445                exponent += 1;
446            }
447            return (value, exponent);
448        }
449        DisplayScale::Base => 0,
450        DisplayScale::Kilo => 1,
451        DisplayScale::Mega => 2,
452        DisplayScale::Giga => 3,
453        DisplayScale::Tera => 4,
454        DisplayScale::Peta => 5,
455        DisplayScale::Exa => 6,
456    };
457
458    for _ in 0..exponent {
459        value /= divisor;
460    }
461
462    (value, exponent)
463}
464
465#[cfg(test)]
466mod tests {
467    use alloc::format;
468
469    use insta::assert_snapshot;
470
471    use super::*;
472
473    #[test]
474    fn test_formatting_snapshots() {
475        use DisplayUnitSystem::*;
476
477        fn display(size: u64, system: DisplayUnitSystem) -> Display {
478            super::display(size).options(|opts| opts.unit_system(system))
479        }
480
481        assert_snapshot!(display(0, Binary), @"0 B");
482        assert_snapshot!(display(0, Decimal), @"0 B");
483        assert_snapshot!(display(1, Binary), @"1 B");
484        assert_snapshot!(display(1, Decimal), @"1 B");
485        assert_snapshot!(display(500, Binary), @"500 B");
486        assert_snapshot!(display(500, Decimal), @"500 B");
487        assert_snapshot!(display(999, Binary), @"999 B");
488        assert_snapshot!(display(999, Decimal), @"999 B");
489        assert_snapshot!(display(1000, Binary), @"1000 B");
490        assert_snapshot!(display(1000, Decimal), @"1.0 kB");
491        assert_snapshot!(display(1023, Binary), @"1023 B");
492        assert_snapshot!(display(1023, Decimal), @"1.0 kB");
493        assert_snapshot!(display(1024, Binary), @"1.0 KiB");
494        assert_snapshot!(display(1024, Decimal), @"1.0 kB");
495        assert_snapshot!(display(1025, Binary), @"1.0 KiB");
496        assert_snapshot!(display(1025, Decimal), @"1.0 kB");
497        assert_snapshot!(display(1500, Binary), @"1.5 KiB");
498        assert_snapshot!(display(1500, Decimal), @"1.5 kB");
499        assert_snapshot!(display(2048, Binary), @"2.0 KiB");
500        assert_snapshot!(display(2048, Decimal), @"2.0 kB");
501        assert_snapshot!(display(1_000_000, Binary), @"976.6 KiB");
502        assert_snapshot!(display(1_000_000, Decimal), @"1.0 MB");
503        assert_snapshot!(display(1_048_576, Binary), @"1.0 MiB");
504        assert_snapshot!(display(1_048_576, Decimal), @"1.0 MB");
505        assert_snapshot!(display(987_654_321, Binary), @"941.9 MiB");
506        assert_snapshot!(display(987_654_321, Decimal), @"987.7 MB");
507        assert_snapshot!(display(1_099_511_627_776, Binary), @"1.0 TiB");
508        assert_snapshot!(display(1_099_511_627_776, Decimal), @"1.1 TB");
509        assert_snapshot!(display(1_125_899_906_842_624, Binary), @"1.0 PiB");
510        assert_snapshot!(display(1_125_899_906_842_624, Decimal), @"1.1 PB");
511        assert_snapshot!(display(1_152_921_504_606_846_976, Binary), @"1.0 EiB");
512        assert_snapshot!(display(1_152_921_504_606_846_976, Decimal), @"1.2 EB");
513        assert_snapshot!(display(u64::MAX - 1, Binary), @"16.0 EiB");
514        assert_snapshot!(display(u64::MAX - 1, Decimal), @"18.4 EB");
515        assert_snapshot!(display(u64::MAX, Binary), @"16.0 EiB");
516        assert_snapshot!(display(u64::MAX, Decimal), @"18.4 EB");
517    }
518
519    #[test]
520    fn test_formats_fractional_sizes() {
521        assert_snapshot!(Display::new(42.5).binary(), @"42.5 B");
522        assert_snapshot!(Display::new(1000.5).decimal(), @"1.0 kB");
523        assert_snapshot!(format!("{:.2}", Display::new(2500.5).decimal()), @"2.50 kB");
524    }
525
526    #[test]
527    fn test_formats_infinite_size() {
528        assert_snapshot!(Display::new(f64::INFINITY).binary(), @"inf EiB");
529        assert_snapshot!(Display::new(f64::INFINITY).decimal(), @"inf EB");
530    }
531
532    #[test]
533    #[should_panic]
534    fn test_new_rejects_nan_size() {
535        Display::new(f64::NAN);
536    }
537
538    #[test]
539    #[should_panic]
540    fn test_new_rejects_negative_size() {
541        Display::new(-1.0);
542    }
543
544    #[test]
545    fn test_formats_default_binary() {
546        assert_snapshot!(display(999u64), @"999 B");
547        assert_snapshot!(display(1000u64), @"1000 B");
548    }
549
550    #[test]
551    fn test_formats_scales() {
552        assert_snapshot!(
553            display(1536u64).options(|opts| opts.scale(DisplayScale::Base)),
554            @"1536 B"
555        );
556        assert_snapshot!(
557            display(1536u64).options(|opts| opts.scale(DisplayScale::Kilo)),
558            @"1.5 KiB"
559        );
560        assert_snapshot!(
561            format!(
562                "{:.3}",
563                display(1536u64).options(|opts| opts
564                    .unit_system(DisplayUnitSystem::Decimal)
565                    .scale(DisplayScale::Mega))
566            ),
567            @"0.002 MB"
568        );
569    }
570
571    #[test]
572    fn test_formats_bits() {
573        assert_snapshot!(
574            display(1u64).options(|opts| opts.base_unit(DisplayBaseUnit::Bit)),
575            @"8 bit"
576        );
577        assert_snapshot!(
578            display(125u64).options(|opts| opts.base_unit(DisplayBaseUnit::Bit)),
579            @"1000 bit"
580        );
581        assert_snapshot!(
582            display(125u64).options(|opts| opts
583                .base_unit(DisplayBaseUnit::Bit)
584                .unit_system(DisplayUnitSystem::Decimal)),
585            @"1.0 kbit"
586        );
587        assert_snapshot!(
588            display(128u64).options(|opts| opts.base_unit(DisplayBaseUnit::Bit)),
589            @"1.0 Kibit"
590        );
591    }
592
593    #[test]
594    fn test_formats_with_display_options() {
595        assert_snapshot!(
596            display(1536u64).options(|opts| opts
597                .base_unit(DisplayBaseUnit::Bit)
598                .scale(DisplayScale::Kilo)),
599            @"12.0 Kibit"
600        );
601    }
602
603    #[test]
604    fn test_formats_with_width_fill_and_alignment() {
605        assert_snapshot!(format!("{:10}", display(1536u64)), @"1.5 KiB   ");
606        assert_snapshot!(format!("{:<10}", display(1536u64)), @"1.5 KiB   ");
607        assert_snapshot!(format!("{:>10}", display(1536u64)), @"   1.5 KiB");
608        assert_snapshot!(format!("{:^10}", display(1536u64)), @" 1.5 KiB  ");
609        assert_snapshot!(format!("{:*^10}", display(1536u64)), @"*1.5 KiB**");
610        assert_snapshot!(format!("{:*>10.2}", display(1536u64)), @"**1.50 KiB");
611    }
612}