nu_protocol/value/
filesize.rs

1use crate::{FromValue, IntoValue, ShellError, Span, Type, Value};
2use num_format::{Locale, WriteFormatted};
3use serde::{Deserialize, Serialize};
4use std::{
5    char,
6    fmt::{self, Write},
7    iter::Sum,
8    ops::{Add, Mul, Neg, Sub},
9    str::FromStr,
10};
11use thiserror::Error;
12
13/// A signed number of bytes.
14///
15/// [`Filesize`] is a wrapper around [`i64`]. Whereas [`i64`] is a dimensionless value, [`Filesize`] represents a
16/// numerical value with a dimensional unit (byte).
17///
18/// A [`Filesize`] can be created from an [`i64`] using [`Filesize::new`] or the `From` or `Into` trait implementations.
19/// To get the underlying [`i64`] value, use [`Filesize::get`] or the `From` or `Into` trait implementations.
20#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
21#[repr(transparent)]
22#[serde(transparent)]
23pub struct Filesize(i64);
24
25impl Filesize {
26    /// A [`Filesize`] of 0 bytes.
27    pub const ZERO: Self = Self(0);
28
29    /// The smallest possible [`Filesize`] value.
30    pub const MIN: Self = Self(i64::MIN);
31
32    /// The largest possible [`Filesize`] value.
33    pub const MAX: Self = Self(i64::MAX);
34
35    /// Create a new [`Filesize`] from a [`i64`] number of bytes.
36    pub const fn new(bytes: i64) -> Self {
37        Self(bytes)
38    }
39
40    /// Creates a [`Filesize`] from a signed multiple of a [`FilesizeUnit`].
41    ///
42    /// If the resulting number of bytes calculated by `value * unit.as_bytes()` overflows an
43    /// [`i64`], then `None` is returned.
44    pub const fn from_unit(value: i64, unit: FilesizeUnit) -> Option<Self> {
45        if let Some(bytes) = value.checked_mul(unit.as_bytes() as i64) {
46            Some(Self(bytes))
47        } else {
48            None
49        }
50    }
51
52    /// Returns the underlying [`i64`] number of bytes in a [`Filesize`].
53    pub const fn get(&self) -> i64 {
54        self.0
55    }
56
57    /// Returns true if a [`Filesize`] is positive and false if it is zero or negative.
58    pub const fn is_positive(self) -> bool {
59        self.0.is_positive()
60    }
61
62    /// Returns true if a [`Filesize`] is negative and false if it is zero or positive.
63    pub const fn is_negative(self) -> bool {
64        self.0.is_negative()
65    }
66
67    /// Returns a [`Filesize`] representing the sign of `self`.
68    /// - 0 if the file size is zero
69    /// - 1 if the file size is positive
70    /// - -1 if the file size is negative
71    pub const fn signum(self) -> Self {
72        Self(self.0.signum())
73    }
74
75    /// Returns the largest [`FilesizeUnit`] with a metric prefix that is smaller than or equal to `self`.
76    ///
77    /// # Examples
78    /// ```
79    /// # use nu_protocol::{Filesize, FilesizeUnit};
80    ///
81    /// let filesize = Filesize::from(FilesizeUnit::KB);
82    /// assert_eq!(filesize.largest_metric_unit(), FilesizeUnit::KB);
83    ///
84    /// let filesize = Filesize::new(FilesizeUnit::KB.as_bytes() as i64 - 1);
85    /// assert_eq!(filesize.largest_metric_unit(), FilesizeUnit::B);
86    ///
87    /// let filesize = Filesize::from(FilesizeUnit::KiB);
88    /// assert_eq!(filesize.largest_metric_unit(), FilesizeUnit::KB);
89    /// ```
90    pub const fn largest_metric_unit(&self) -> FilesizeUnit {
91        const KB: u64 = FilesizeUnit::KB.as_bytes();
92        const MB: u64 = FilesizeUnit::MB.as_bytes();
93        const GB: u64 = FilesizeUnit::GB.as_bytes();
94        const TB: u64 = FilesizeUnit::TB.as_bytes();
95        const PB: u64 = FilesizeUnit::PB.as_bytes();
96        const EB: u64 = FilesizeUnit::EB.as_bytes();
97
98        match self.0.unsigned_abs() {
99            0..KB => FilesizeUnit::B,
100            KB..MB => FilesizeUnit::KB,
101            MB..GB => FilesizeUnit::MB,
102            GB..TB => FilesizeUnit::GB,
103            TB..PB => FilesizeUnit::TB,
104            PB..EB => FilesizeUnit::PB,
105            EB.. => FilesizeUnit::EB,
106        }
107    }
108
109    /// Returns the largest [`FilesizeUnit`] with a binary prefix that is smaller than or equal to `self`.
110    ///
111    /// # Examples
112    /// ```
113    /// # use nu_protocol::{Filesize, FilesizeUnit};
114    ///
115    /// let filesize = Filesize::from(FilesizeUnit::KiB);
116    /// assert_eq!(filesize.largest_binary_unit(), FilesizeUnit::KiB);
117    ///
118    /// let filesize = Filesize::new(FilesizeUnit::KiB.as_bytes() as i64 - 1);
119    /// assert_eq!(filesize.largest_binary_unit(), FilesizeUnit::B);
120    ///
121    /// let filesize = Filesize::from(FilesizeUnit::MB);
122    /// assert_eq!(filesize.largest_binary_unit(), FilesizeUnit::KiB);
123    /// ```
124    pub const fn largest_binary_unit(&self) -> FilesizeUnit {
125        const KIB: u64 = FilesizeUnit::KiB.as_bytes();
126        const MIB: u64 = FilesizeUnit::MiB.as_bytes();
127        const GIB: u64 = FilesizeUnit::GiB.as_bytes();
128        const TIB: u64 = FilesizeUnit::TiB.as_bytes();
129        const PIB: u64 = FilesizeUnit::PiB.as_bytes();
130        const EIB: u64 = FilesizeUnit::EiB.as_bytes();
131
132        match self.0.unsigned_abs() {
133            0..KIB => FilesizeUnit::B,
134            KIB..MIB => FilesizeUnit::KiB,
135            MIB..GIB => FilesizeUnit::MiB,
136            GIB..TIB => FilesizeUnit::GiB,
137            TIB..PIB => FilesizeUnit::TiB,
138            PIB..EIB => FilesizeUnit::PiB,
139            EIB.. => FilesizeUnit::EiB,
140        }
141    }
142}
143
144impl From<i64> for Filesize {
145    fn from(value: i64) -> Self {
146        Self(value)
147    }
148}
149
150impl From<Filesize> for i64 {
151    fn from(filesize: Filesize) -> Self {
152        filesize.0
153    }
154}
155
156macro_rules! impl_from {
157    ($($ty:ty),* $(,)?) => {
158        $(
159            impl From<$ty> for Filesize {
160                #[inline]
161                fn from(value: $ty) -> Self {
162                    Self(value.into())
163                }
164            }
165
166            impl TryFrom<Filesize> for $ty {
167                type Error = <i64 as TryInto<$ty>>::Error;
168
169                #[inline]
170                fn try_from(filesize: Filesize) -> Result<Self, Self::Error> {
171                    filesize.0.try_into()
172                }
173            }
174        )*
175    };
176}
177
178impl_from!(u8, i8, u16, i16, u32, i32);
179
180macro_rules! impl_try_from {
181    ($($ty:ty),* $(,)?) => {
182        $(
183            impl TryFrom<$ty> for Filesize {
184                type Error = <$ty as TryInto<i64>>::Error;
185
186                #[inline]
187                fn try_from(value: $ty) -> Result<Self, Self::Error> {
188                    value.try_into().map(Self)
189                }
190            }
191
192            impl TryFrom<Filesize> for $ty {
193                type Error = <i64 as TryInto<$ty>>::Error;
194
195                #[inline]
196                fn try_from(filesize: Filesize) -> Result<Self, Self::Error> {
197                    filesize.0.try_into()
198                }
199            }
200        )*
201    };
202}
203
204impl_try_from!(u64, usize, isize);
205
206/// The error type returned when a checked conversion from a floating point type fails.
207#[derive(Debug, Copy, Clone, PartialEq, Eq, Error)]
208pub struct TryFromFloatError(());
209
210impl fmt::Display for TryFromFloatError {
211    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
212        write!(fmt, "out of range float type conversion attempted")
213    }
214}
215
216impl TryFrom<f64> for Filesize {
217    type Error = TryFromFloatError;
218
219    #[inline]
220    fn try_from(value: f64) -> Result<Self, Self::Error> {
221        if i64::MIN as f64 <= value && value <= i64::MAX as f64 {
222            Ok(Self(value as i64))
223        } else {
224            Err(TryFromFloatError(()))
225        }
226    }
227}
228
229impl TryFrom<f32> for Filesize {
230    type Error = TryFromFloatError;
231
232    #[inline]
233    fn try_from(value: f32) -> Result<Self, Self::Error> {
234        if i64::MIN as f32 <= value && value <= i64::MAX as f32 {
235            Ok(Self(value as i64))
236        } else {
237            Err(TryFromFloatError(()))
238        }
239    }
240}
241
242impl FromValue for Filesize {
243    fn from_value(value: Value) -> Result<Self, ShellError> {
244        value.as_filesize()
245    }
246
247    fn expected_type() -> Type {
248        Type::Filesize
249    }
250}
251
252impl IntoValue for Filesize {
253    fn into_value(self, span: Span) -> Value {
254        Value::filesize(self.0, span)
255    }
256}
257
258impl Add for Filesize {
259    type Output = Option<Self>;
260
261    fn add(self, rhs: Self) -> Self::Output {
262        self.0.checked_add(rhs.0).map(Self)
263    }
264}
265
266impl Sub for Filesize {
267    type Output = Option<Self>;
268
269    fn sub(self, rhs: Self) -> Self::Output {
270        self.0.checked_sub(rhs.0).map(Self)
271    }
272}
273
274impl Mul<i64> for Filesize {
275    type Output = Option<Self>;
276
277    fn mul(self, rhs: i64) -> Self::Output {
278        self.0.checked_mul(rhs).map(Self)
279    }
280}
281
282impl Mul<Filesize> for i64 {
283    type Output = Option<Filesize>;
284
285    fn mul(self, rhs: Filesize) -> Self::Output {
286        self.checked_mul(rhs.0).map(Filesize::new)
287    }
288}
289
290impl Mul<f64> for Filesize {
291    type Output = Option<Self>;
292
293    fn mul(self, rhs: f64) -> Self::Output {
294        let bytes = ((self.0 as f64) * rhs).round();
295        if i64::MIN as f64 <= bytes && bytes <= i64::MAX as f64 {
296            Some(Self(bytes as i64))
297        } else {
298            None
299        }
300    }
301}
302
303impl Mul<Filesize> for f64 {
304    type Output = Option<Filesize>;
305
306    fn mul(self, rhs: Filesize) -> Self::Output {
307        let bytes = (self * (rhs.0 as f64)).round();
308        if i64::MIN as f64 <= bytes && bytes <= i64::MAX as f64 {
309            Some(Filesize(bytes as i64))
310        } else {
311            None
312        }
313    }
314}
315
316impl Neg for Filesize {
317    type Output = Option<Self>;
318
319    fn neg(self) -> Self::Output {
320        self.0.checked_neg().map(Self)
321    }
322}
323
324impl Sum<Filesize> for Option<Filesize> {
325    fn sum<I: Iterator<Item = Filesize>>(iter: I) -> Self {
326        let mut sum = Filesize::ZERO;
327        for filesize in iter {
328            sum = (sum + filesize)?;
329        }
330        Some(sum)
331    }
332}
333
334impl fmt::Display for Filesize {
335    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
336        write!(f, "{}", FilesizeFormatter::new().format(*self))
337    }
338}
339
340/// All the possible filesize units for a [`Filesize`].
341///
342/// This type contains both units with metric (SI) prefixes which are powers of 10 (e.g., kB = 1000 bytes)
343/// and units with binary prefixes which are powers of 2 (e.g., KiB = 1024 bytes).
344///
345/// The number of bytes in a [`FilesizeUnit`] can be obtained using [`as_bytes`](Self::as_bytes).
346#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
347pub enum FilesizeUnit {
348    /// One byte
349    B,
350    /// Kilobyte = 1000 bytes
351    KB,
352    /// Megabyte = 10<sup>6</sup> bytes
353    MB,
354    /// Gigabyte = 10<sup>9</sup> bytes
355    GB,
356    /// Terabyte = 10<sup>12</sup> bytes
357    TB,
358    /// Petabyte = 10<sup>15</sup> bytes
359    PB,
360    /// Exabyte = 10<sup>18</sup> bytes
361    EB,
362    /// Kibibyte = 1024 bytes
363    KiB,
364    /// Mebibyte = 2<sup>20</sup> bytes
365    MiB,
366    /// Gibibyte = 2<sup>30</sup> bytes
367    GiB,
368    /// Tebibyte = 2<sup>40</sup> bytes
369    TiB,
370    /// Pebibyte = 2<sup>50</sup> bytes
371    PiB,
372    /// Exbibyte = 2<sup>60</sup> bytes
373    EiB,
374}
375
376impl FilesizeUnit {
377    /// Returns the number of bytes in a [`FilesizeUnit`].
378    pub const fn as_bytes(&self) -> u64 {
379        match self {
380            Self::B => 1,
381            Self::KB => 10_u64.pow(3),
382            Self::MB => 10_u64.pow(6),
383            Self::GB => 10_u64.pow(9),
384            Self::TB => 10_u64.pow(12),
385            Self::PB => 10_u64.pow(15),
386            Self::EB => 10_u64.pow(18),
387            Self::KiB => 2_u64.pow(10),
388            Self::MiB => 2_u64.pow(20),
389            Self::GiB => 2_u64.pow(30),
390            Self::TiB => 2_u64.pow(40),
391            Self::PiB => 2_u64.pow(50),
392            Self::EiB => 2_u64.pow(60),
393        }
394    }
395
396    /// Convert a [`FilesizeUnit`] to a [`Filesize`].
397    ///
398    /// To create a [`Filesize`] from a multiple of a [`FilesizeUnit`] use [`Filesize::from_unit`].
399    pub const fn as_filesize(&self) -> Filesize {
400        Filesize::new(self.as_bytes() as i64)
401    }
402
403    /// Returns the symbol [`str`] for a [`FilesizeUnit`].
404    ///
405    /// The symbol is exactly the same as the enum case name in Rust code except for
406    /// [`FilesizeUnit::KB`] which is `kB`.
407    ///
408    /// The returned string is the same exact string needed for a successful call to
409    /// [`parse`](str::parse) for a [`FilesizeUnit`].
410    ///
411    /// # Examples
412    /// ```
413    /// # use nu_protocol::FilesizeUnit;
414    /// assert_eq!(FilesizeUnit::B.as_str(), "B");
415    /// assert_eq!(FilesizeUnit::KB.as_str(), "kB");
416    /// assert_eq!(FilesizeUnit::KiB.as_str(), "KiB");
417    /// assert_eq!(FilesizeUnit::KB.as_str().parse(), Ok(FilesizeUnit::KB));
418    /// ```
419    pub const fn as_str(&self) -> &'static str {
420        match self {
421            Self::B => "B",
422            Self::KB => "kB",
423            Self::MB => "MB",
424            Self::GB => "GB",
425            Self::TB => "TB",
426            Self::PB => "PB",
427            Self::EB => "EB",
428            Self::KiB => "KiB",
429            Self::MiB => "MiB",
430            Self::GiB => "GiB",
431            Self::TiB => "TiB",
432            Self::PiB => "PiB",
433            Self::EiB => "EiB",
434        }
435    }
436
437    /// Returns `true` if a [`FilesizeUnit`] has a metric (SI) prefix (a power of 10).
438    ///
439    /// Note that this returns `true` for [`FilesizeUnit::B`] as well.
440    pub const fn is_metric(&self) -> bool {
441        match self {
442            Self::B | Self::KB | Self::MB | Self::GB | Self::TB | Self::PB | Self::EB => true,
443            Self::KiB | Self::MiB | Self::GiB | Self::TiB | Self::PiB | Self::EiB => false,
444        }
445    }
446
447    /// Returns `true` if a [`FilesizeUnit`] has a binary prefix (a power of 2).
448    ///
449    /// Note that this returns `true` for [`FilesizeUnit::B`] as well.
450    pub const fn is_binary(&self) -> bool {
451        match self {
452            Self::KB | Self::MB | Self::GB | Self::TB | Self::PB | Self::EB => false,
453            Self::B | Self::KiB | Self::MiB | Self::GiB | Self::TiB | Self::PiB | Self::EiB => true,
454        }
455    }
456}
457
458impl From<FilesizeUnit> for Filesize {
459    fn from(unit: FilesizeUnit) -> Self {
460        unit.as_filesize()
461    }
462}
463
464impl fmt::Display for FilesizeUnit {
465    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
466        f.write_str(self.as_str())
467    }
468}
469
470/// The error returned when failing to parse a [`FilesizeUnit`].
471///
472/// This occurs when the string being parsed does not exactly match the name of one of the
473/// enum cases in [`FilesizeUnit`].
474#[derive(Debug, Copy, Clone, PartialEq, Eq, Error)]
475pub struct ParseFilesizeUnitError(());
476
477impl fmt::Display for ParseFilesizeUnitError {
478    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
479        write!(fmt, "invalid file size unit")
480    }
481}
482
483impl FromStr for FilesizeUnit {
484    type Err = ParseFilesizeUnitError;
485
486    fn from_str(s: &str) -> Result<Self, Self::Err> {
487        Ok(match s {
488            "B" => Self::B,
489            "kB" => Self::KB,
490            "MB" => Self::MB,
491            "GB" => Self::GB,
492            "TB" => Self::TB,
493            "PB" => Self::PB,
494            "EB" => Self::EB,
495            "KiB" => Self::KiB,
496            "MiB" => Self::MiB,
497            "GiB" => Self::GiB,
498            "TiB" => Self::TiB,
499            "PiB" => Self::PiB,
500            "EiB" => Self::EiB,
501            _ => return Err(ParseFilesizeUnitError(())),
502        })
503    }
504}
505
506/// The different file size unit display formats for a [`FilesizeFormatter`].
507///
508/// To see more information about each possible format, see the documentation for each of the enum
509/// cases of [`FilesizeUnitFormat`].
510#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
511pub enum FilesizeUnitFormat {
512    /// [`Metric`](Self::Metric) will make a [`FilesizeFormatter`] use the
513    /// [`largest_metric_unit`](Filesize::largest_metric_unit) of a [`Filesize`] when formatting it.
514    Metric,
515    /// [`Binary`](Self::Binary) will make a [`FilesizeFormatter`] use the
516    /// [`largest_binary_unit`](Filesize::largest_binary_unit) of a [`Filesize`] when formatting it.
517    Binary,
518    /// [`FilesizeUnitFormat::Unit`] will make a [`FilesizeFormatter`] use the provided
519    /// [`FilesizeUnit`] when formatting all [`Filesize`]s.
520    Unit(FilesizeUnit),
521}
522
523impl FilesizeUnitFormat {
524    /// Returns a string representation of a [`FilesizeUnitFormat`].
525    ///
526    /// The returned string is the same exact string needed for a successful call to
527    /// [`parse`](str::parse) for a [`FilesizeUnitFormat`].
528    ///
529    /// # Examples
530    /// ```
531    /// # use nu_protocol::{FilesizeUnit, FilesizeUnitFormat};
532    /// assert_eq!(FilesizeUnitFormat::Metric.as_str(), "metric");
533    /// assert_eq!(FilesizeUnitFormat::Binary.as_str(), "binary");
534    /// assert_eq!(FilesizeUnitFormat::Unit(FilesizeUnit::KB).as_str(), "kB");
535    /// assert_eq!(FilesizeUnitFormat::Metric.as_str().parse(), Ok(FilesizeUnitFormat::Metric));
536    /// ```
537    pub const fn as_str(&self) -> &'static str {
538        match self {
539            Self::Metric => "metric",
540            Self::Binary => "binary",
541            Self::Unit(unit) => unit.as_str(),
542        }
543    }
544
545    /// Returns `true` for [`DisplayFilesizeUnit::Metric`] or if the underlying [`FilesizeUnit`]
546    /// is metric according to [`FilesizeUnit::is_metric`].
547    ///
548    /// Note that this returns `true` for [`FilesizeUnit::B`] as well.
549    pub const fn is_metric(&self) -> bool {
550        match self {
551            Self::Metric => true,
552            Self::Binary => false,
553            Self::Unit(unit) => unit.is_metric(),
554        }
555    }
556
557    /// Returns `true` for [`DisplayFilesizeUnit::Binary`] or if the underlying [`FilesizeUnit`]
558    /// is binary according to [`FilesizeUnit::is_binary`].
559    ///
560    /// Note that this returns `true` for [`FilesizeUnit::B`] as well.
561    pub const fn is_binary(&self) -> bool {
562        match self {
563            Self::Metric => false,
564            Self::Binary => true,
565            Self::Unit(unit) => unit.is_binary(),
566        }
567    }
568}
569
570impl From<FilesizeUnit> for FilesizeUnitFormat {
571    fn from(unit: FilesizeUnit) -> Self {
572        Self::Unit(unit)
573    }
574}
575
576impl fmt::Display for FilesizeUnitFormat {
577    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
578        f.write_str(self.as_str())
579    }
580}
581
582/// The error returned when failing to parse a [`DisplayFilesizeUnit`].
583///
584/// This occurs when the string being parsed does not exactly match any of:
585/// - `metric`
586/// - `binary`
587/// - The name of any of the enum cases in [`FilesizeUnit`]. The exception is [`FilesizeUnit::KB`] which must be `kB`.
588#[derive(Debug, Copy, Clone, PartialEq, Eq, Error)]
589pub struct ParseFilesizeUnitFormatError(());
590
591impl fmt::Display for ParseFilesizeUnitFormatError {
592    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
593        write!(fmt, "invalid file size unit format")
594    }
595}
596
597impl FromStr for FilesizeUnitFormat {
598    type Err = ParseFilesizeUnitFormatError;
599
600    fn from_str(s: &str) -> Result<Self, Self::Err> {
601        Ok(match s {
602            "metric" => Self::Metric,
603            "binary" => Self::Binary,
604            s => Self::Unit(s.parse().map_err(|_| ParseFilesizeUnitFormatError(()))?),
605        })
606    }
607}
608
609/// A configurable formatter for [`Filesize`]s.
610///
611/// [`FilesizeFormatter`] is a builder struct that you can modify via the following methods:
612/// - [`unit`](Self::unit)
613/// - [`show_unit`](Self::show_unit)
614/// - [`precision`](Self::precision)
615/// - [`locale`](Self::locale)
616///
617/// For more information, see the documentation for each of those methods.
618///
619/// # Examples
620/// ```
621/// # use nu_protocol::{Filesize, FilesizeFormatter, FilesizeUnit};
622/// # use num_format::Locale;
623/// let filesize = Filesize::from_unit(4, FilesizeUnit::KiB).unwrap();
624/// let formatter = FilesizeFormatter::new();
625///
626/// assert_eq!(formatter.unit(FilesizeUnit::B).format(filesize).to_string(), "4096 B");
627/// assert_eq!(formatter.unit(FilesizeUnit::KiB).format(filesize).to_string(), "4 KiB");
628/// assert_eq!(formatter.precision(2).format(filesize).to_string(), "4.09 kB");
629/// assert_eq!(
630///     formatter
631///         .unit(FilesizeUnit::B)
632///         .locale(Locale::en)
633///         .format(filesize)
634///         .to_string(),
635///     "4,096 B",
636/// );
637/// ```
638#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
639pub struct FilesizeFormatter {
640    unit: FilesizeUnitFormat,
641    show_unit: bool,
642    precision: Option<usize>,
643    locale: Locale,
644}
645
646impl FilesizeFormatter {
647    /// Create a new, default [`FilesizeFormatter`].
648    ///
649    /// The default formatter has:
650    /// - a [`unit`](Self::unit) of [`FilesizeUnitFormat::Metric`].
651    /// - a [`show_unit`](Self::show_unit) of `true`.
652    /// - a [`precision`](Self::precision) of `None`.
653    /// - a [`locale`](Self::locale) of [`Locale::en_US_POSIX`]
654    ///   (a very plain format with no thousands separators).
655    pub fn new() -> Self {
656        FilesizeFormatter {
657            unit: FilesizeUnitFormat::Metric,
658            show_unit: true,
659            precision: None,
660            locale: Locale::en_US_POSIX,
661        }
662    }
663
664    /// Set the [`FilesizeUnitFormat`] used by the formatter.
665    ///
666    /// A [`FilesizeUnit`] or a [`FilesizeUnitFormat`] can be provided to this method.
667    /// [`FilesizeUnitFormat::Metric`] and [`FilesizeUnitFormat::Binary`] will use a unit of an
668    /// appropriate scale for each [`Filesize`], whereas providing a [`FilesizeUnit`] will use that
669    /// unit to format all [`Filesize`]s.
670    ///
671    /// # Examples
672    /// ```
673    /// # use nu_protocol::{Filesize, FilesizeFormatter, FilesizeUnit, FilesizeUnitFormat};
674    /// let formatter = FilesizeFormatter::new().precision(1);
675    ///
676    /// let filesize = Filesize::from_unit(4, FilesizeUnit::KiB).unwrap();
677    /// assert_eq!(formatter.unit(FilesizeUnit::B).format(filesize).to_string(), "4096 B");
678    /// assert_eq!(formatter.unit(FilesizeUnitFormat::Binary).format(filesize).to_string(), "4.0 KiB");
679    ///
680    /// let filesize = Filesize::from_unit(4, FilesizeUnit::MiB).unwrap();
681    /// assert_eq!(formatter.unit(FilesizeUnitFormat::Metric).format(filesize).to_string(), "4.1 MB");
682    /// assert_eq!(formatter.unit(FilesizeUnitFormat::Binary).format(filesize).to_string(), "4.0 MiB");
683    /// ```
684    pub fn unit(mut self, unit: impl Into<FilesizeUnitFormat>) -> Self {
685        self.unit = unit.into();
686        self
687    }
688
689    /// Sets whether to show or omit the file size unit in the formatted output.
690    ///
691    /// This setting can be used to disable the unit formatting from [`FilesizeFormatter`]
692    /// and instead provide your own.
693    ///
694    /// Note that the [`FilesizeUnitFormat`] provided to [`unit`](Self::unit) is still used to
695    /// format the numeric portion of a [`Filesize`]. So, setting `show_unit` to `false` is only
696    /// recommended for [`FilesizeUnitFormat::Unit`], since this will keep the unit the same
697    /// for all [`Filesize`]s. [`FilesizeUnitFormat::Metric`] and [`FilesizeUnitFormat::Binary`],
698    /// on the other hand, will adapt the unit to match the magnitude of each formatted [`Filesize`].
699    ///
700    /// # Examples
701    /// ```
702    /// # use nu_protocol::{Filesize, FilesizeFormatter, FilesizeUnit};
703    /// let filesize = Filesize::from_unit(4, FilesizeUnit::KiB).unwrap();
704    /// let formatter = FilesizeFormatter::new().show_unit(false);
705    ///
706    /// assert_eq!(formatter.unit(FilesizeUnit::B).format(filesize).to_string(), "4096");
707    /// assert_eq!(format!("{} KB", formatter.unit(FilesizeUnit::KiB).format(filesize)), "4 KB");
708    /// ```
709    pub fn show_unit(self, show_unit: bool) -> Self {
710        Self { show_unit, ..self }
711    }
712
713    /// Set the number of digits to display after the decimal place.
714    ///
715    /// Note that digits after the decimal place will never be shown if:
716    /// - [`unit`](Self::unit) is [`FilesizeUnit::B`],
717    /// - [`unit`](Self::unit) is [`FilesizeUnitFormat::Metric`] and the number of bytes
718    ///   is less than [`FilesizeUnit::KB`]
719    /// - [`unit`](Self::unit) is [`FilesizeUnitFormat::Binary`] and the number of bytes
720    ///   is less than [`FilesizeUnit::KiB`].
721    ///
722    /// Additionally, the precision specified in the format string
723    /// (i.e., [`std::fmt::Formatter::precision`]) will take precedence if is specified.
724    /// If the format string precision and the [`FilesizeFormatter`]'s precision are both `None`,
725    /// then all digits after the decimal place, if any, are shown.
726    ///
727    /// # Examples
728    /// ```
729    /// # use nu_protocol::{Filesize, FilesizeFormatter, FilesizeUnit, FilesizeUnitFormat};
730    /// let filesize = Filesize::from_unit(4, FilesizeUnit::KiB).unwrap();
731    /// let formatter = FilesizeFormatter::new();
732    ///
733    /// assert_eq!(formatter.precision(2).format(filesize).to_string(), "4.09 kB");
734    /// assert_eq!(formatter.precision(0).format(filesize).to_string(), "4 kB");
735    /// assert_eq!(formatter.precision(None).format(filesize).to_string(), "4.096 kB");
736    /// assert_eq!(
737    ///     formatter
738    ///         .precision(None)
739    ///         .unit(FilesizeUnit::KiB)
740    ///         .format(filesize)
741    ///         .to_string(),
742    ///     "4 KiB",
743    /// );
744    /// assert_eq!(
745    ///     formatter
746    ///         .unit(FilesizeUnit::B)
747    ///         .precision(2)
748    ///         .format(filesize)
749    ///         .to_string(),
750    ///     "4096 B",
751    /// );
752    /// assert_eq!(format!("{:.2}", formatter.precision(0).format(filesize)), "4.09 kB");
753    /// ```
754    pub fn precision(mut self, precision: impl Into<Option<usize>>) -> Self {
755        self.precision = precision.into();
756        self
757    }
758
759    /// Set the [`Locale`] to use when formatting the numeric portion of a [`Filesize`].
760    ///
761    /// The [`Locale`] determines the decimal place character, minus sign character,
762    /// digit grouping method, and digit separator character.
763    ///
764    /// # Examples
765    /// ```
766    /// # use nu_protocol::{Filesize, FilesizeFormatter, FilesizeUnit, FilesizeUnitFormat};
767    /// # use num_format::Locale;
768    /// let filesize = Filesize::from_unit(-4, FilesizeUnit::MiB).unwrap();
769    /// let formatter = FilesizeFormatter::new().unit(FilesizeUnit::KB).precision(1);
770    ///
771    /// assert_eq!(formatter.format(filesize).to_string(), "-4194.3 kB");
772    /// assert_eq!(formatter.locale(Locale::en).format(filesize).to_string(), "-4,194.3 kB");
773    /// assert_eq!(formatter.locale(Locale::rm).format(filesize).to_string(), "\u{2212}4’194.3 kB");
774    /// let filesize = Filesize::from_unit(-4, FilesizeUnit::GiB).unwrap();
775    /// assert_eq!(formatter.locale(Locale::ta).format(filesize).to_string(), "-42,94,967.2 kB");
776    /// ```
777    pub fn locale(mut self, locale: Locale) -> Self {
778        self.locale = locale;
779        self
780    }
781
782    /// Format a [`Filesize`] into a [`FormattedFilesize`] which implements [`fmt::Display`].
783    ///
784    /// # Examples
785    /// ```
786    /// # use nu_protocol::{Filesize, FilesizeFormatter, FilesizeUnit};
787    /// let filesize = Filesize::from_unit(4, FilesizeUnit::KB).unwrap();
788    /// let formatter = FilesizeFormatter::new();
789    ///
790    /// assert_eq!(format!("{}", formatter.format(filesize)), "4 kB");
791    /// assert_eq!(formatter.format(filesize).to_string(), "4 kB");
792    /// ```
793    pub fn format(&self, filesize: Filesize) -> FormattedFilesize {
794        FormattedFilesize {
795            format: *self,
796            filesize,
797        }
798    }
799}
800
801impl Default for FilesizeFormatter {
802    fn default() -> Self {
803        Self::new()
804    }
805}
806
807/// The resulting struct from calling [`FilesizeFormatter::format`] on a [`Filesize`].
808///
809/// The only purpose of this struct is to implement [`fmt::Display`].
810#[derive(Debug, Clone)]
811pub struct FormattedFilesize {
812    format: FilesizeFormatter,
813    filesize: Filesize,
814}
815
816impl fmt::Display for FormattedFilesize {
817    fn fmt(&self, mut f: &mut fmt::Formatter<'_>) -> fmt::Result {
818        let Self { filesize, format } = *self;
819        let FilesizeFormatter {
820            unit,
821            show_unit,
822            precision,
823            locale,
824        } = format;
825        let unit = match unit {
826            FilesizeUnitFormat::Metric => filesize.largest_metric_unit(),
827            FilesizeUnitFormat::Binary => filesize.largest_binary_unit(),
828            FilesizeUnitFormat::Unit(unit) => unit,
829        };
830        let Filesize(filesize) = filesize;
831        let precision = f.precision().or(precision);
832
833        let bytes = unit.as_bytes() as i64;
834        let whole = filesize / bytes;
835        let fract = (filesize % bytes).unsigned_abs();
836
837        f.write_formatted(&whole, &locale)
838            .map_err(|_| std::fmt::Error)?;
839
840        if unit != FilesizeUnit::B && precision != Some(0) && !(precision.is_none() && fract == 0) {
841            f.write_str(locale.decimal())?;
842
843            let bytes = unit.as_bytes();
844            let mut fract = fract * 10;
845            let mut i = 0;
846            loop {
847                let q = fract / bytes;
848                let r = fract % bytes;
849                // Quick soundness proof:
850                // r <= bytes                by definition of remainder `%`
851                // => 10 * r <= 10 * bytes
852                // => fract <= 10 * bytes    before next iteration, fract = r * 10
853                // => fract / bytes <= 10
854                // => q <= 10                next iteration, q = fract / bytes
855                debug_assert!(q <= 10);
856                f.write_char(char::from_digit(q as u32, 10).expect("q <= 10"))?;
857                i += 1;
858                if r == 0 || precision.is_some_and(|p| i >= p) {
859                    break;
860                }
861                fract = r * 10;
862            }
863
864            if let Some(precision) = precision {
865                for _ in 0..(precision - i) {
866                    f.write_char('0')?;
867                }
868            }
869        }
870
871        if show_unit {
872            write!(f, " {unit}")?;
873        }
874
875        Ok(())
876    }
877}
878
879#[cfg(test)]
880mod tests {
881    use super::*;
882    use rstest::rstest;
883
884    #[rstest]
885    #[case(1024, FilesizeUnit::KB, "1.024 kB")]
886    #[case(1024, FilesizeUnit::B, "1024 B")]
887    #[case(1024, FilesizeUnit::KiB, "1 KiB")]
888    #[case(3_000_000, FilesizeUnit::MB, "3 MB")]
889    #[case(3_000_000, FilesizeUnit::KB, "3000 kB")]
890    fn display_unit(#[case] bytes: i64, #[case] unit: FilesizeUnit, #[case] exp: &str) {
891        assert_eq!(
892            exp,
893            FilesizeFormatter::new()
894                .unit(unit)
895                .format(Filesize::new(bytes))
896                .to_string()
897        );
898    }
899
900    #[rstest]
901    #[case(1000, "1000 B")]
902    #[case(1024, "1 KiB")]
903    #[case(1025, "1.0009765625 KiB")]
904    fn display_auto_binary(#[case] val: i64, #[case] exp: &str) {
905        assert_eq!(
906            exp,
907            FilesizeFormatter::new()
908                .unit(FilesizeUnitFormat::Binary)
909                .format(Filesize::new(val))
910                .to_string()
911        );
912    }
913
914    #[rstest]
915    #[case(999, "999 B")]
916    #[case(1000, "1 kB")]
917    #[case(1024, "1.024 kB")]
918    fn display_auto_metric(#[case] val: i64, #[case] exp: &str) {
919        assert_eq!(
920            exp,
921            FilesizeFormatter::new()
922                .unit(FilesizeUnitFormat::Metric)
923                .format(Filesize::new(val))
924                .to_string()
925        );
926    }
927}