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