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}