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}