diff --git a/examples/ls.rs b/examples/ls.rs index 7b380b3..72383f2 100644 --- a/examples/ls.rs +++ b/examples/ls.rs @@ -15,7 +15,7 @@ fn main() -> io::Result<()> { if md.is_file() { let file_size = md.len(); - let file_size = bytesize::ByteSize::b(file_size); + let file_size = bytesize::ByteSize::b(file_size).display().iec_short(); println!("{file_size}\t{file_name}"); } else { diff --git a/src/display.rs b/src/display.rs new file mode 100644 index 0000000..53c8726 --- /dev/null +++ b/src/display.rs @@ -0,0 +1,236 @@ +use core::{fmt, write}; + +use crate::ByteSize; + +/// Format / style to use when displaying a [`ByteSize`]. +#[derive(Debug, Clone, Copy)] +pub(crate) enum Format { + Iec, + IecShort, + Si, + SiShort, +} + +impl Format { + fn unit(self) -> u64 { + match self { + Format::Iec | Format::IecShort => crate::KIB, + Format::Si | Format::SiShort => crate::KB, + } + } + + fn unit_base(self) -> f64 { + match self { + Format::Iec | Format::IecShort => crate::LN_KIB, + Format::Si | Format::SiShort => crate::LN_KB, + } + } + + fn unit_prefixes(self) -> &'static [u8] { + match self { + Format::Iec | Format::IecShort => crate::UNITS_IEC.as_bytes(), + Format::Si | Format::SiShort => crate::UNITS_SI.as_bytes(), + } + } + + fn unit_separator(self) -> &'static str { + match self { + Format::Iec | Format::Si => " ", + Format::IecShort | Format::SiShort => "", + } + } + + fn unit_suffix(self) -> &'static str { + match self { + Format::Iec => "iB", + Format::Si => "B", + Format::IecShort | Format::SiShort => "", + } + } +} + +/// Formatting display wrapper for [`ByteSize`]. +/// +/// Supports various styles, see methods. By default, the [`iec()`](Self::iec()) style is used. +/// +/// # Examples +/// +/// ``` +/// # use bytesize::ByteSize; +/// assert_eq!( +/// "1.0 MiB", +/// ByteSize::mib(1).display().iec().to_string(), +/// ); +/// +/// assert_eq!( +/// "42.0k", +/// ByteSize::kb(42).display().si_short().to_string(), +/// ); +/// ``` +#[derive(Debug, Clone)] +pub struct Display { + pub(crate) byte_size: ByteSize, + pub(crate) format: Format, +} + +impl Display { + /// Format using IEC (binary) units. + /// + /// E.g., `4.2 MiB`. + #[must_use] + #[doc(alias = "binary")] + pub fn iec(mut self) -> Self { + self.format = Format::Iec; + self + } + + /// Format using SI (decimal) units. + /// + /// E.g., `4.2 MB`. + /// + /// Designed to produce output compatible with `sort -h`. + #[must_use] + #[doc(alias = "binary")] + pub fn iec_short(mut self) -> Self { + self.format = Format::IecShort; + self + } + + /// Format using a short style and IEC (binary) units. + /// + /// E.g., `4.2M`. + #[must_use] + #[doc(alias = "decimal")] + pub fn si(mut self) -> Self { + self.format = Format::Si; + self + } + + /// Format using a short style and SI (decimal) units. + /// + /// E.g., `4.2M`. + #[must_use] + #[doc(alias = "decimal")] + pub fn si_short(mut self) -> Self { + self.format = Format::SiShort; + self + } +} + +impl fmt::Display for Display { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let bytes = self.byte_size.as_u64(); + + let unit = self.format.unit(); + let unit_base = self.format.unit_base(); + + let unit_prefixes = self.format.unit_prefixes(); + let unit_separator = self.format.unit_separator(); + let unit_suffix = self.format.unit_suffix(); + + if bytes < unit { + write!(f, "{bytes}{unit_separator}B")?; + } else { + let size = bytes as f64; + let exp = match (size.ln() / unit_base) as usize { + 0 => 1, + e => e, + }; + + let unit_prefix = unit_prefixes[exp - 1] as char; + + write!( + f, + "{:.1}{unit_separator}{unit_prefix}{unit_suffix}", + (size / unit.pow(exp as u32) as f64), + )?; + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn to_string_iec() { + let display = Display { + byte_size: ByteSize::gib(1), + format: Format::Iec, + }; + assert_eq!("1.0 GiB", display.to_string()); + + let display = Display { + byte_size: ByteSize::gb(1), + format: Format::Iec, + }; + assert_eq!("953.7 MiB", display.to_string()); + } + + #[test] + fn to_string_si() { + let display = Display { + byte_size: ByteSize::gib(1), + format: Format::Si, + }; + assert_eq!("1.1 GB", display.to_string()); + + let display = Display { + byte_size: ByteSize::gb(1), + format: Format::Si, + }; + assert_eq!("1.0 GB", display.to_string()); + } + + #[test] + fn to_string_short() { + let display = Display { + byte_size: ByteSize::gib(1), + format: Format::IecShort, + }; + assert_eq!("1.0G", display.to_string()); + + let display = Display { + byte_size: ByteSize::gb(1), + format: Format::IecShort, + }; + assert_eq!("953.7M", display.to_string()); + } + + #[track_caller] + fn assert_to_string(expected: &str, byte_size: ByteSize, format: Format) { + assert_eq!(expected, Display { byte_size, format }.to_string()); + } + + #[test] + fn test_to_string_as() { + assert_to_string("215 B", ByteSize::b(215), Format::Iec); + assert_to_string("215 B", ByteSize::b(215), Format::Si); + + assert_to_string("1.0 KiB", ByteSize::kib(1), Format::Iec); + assert_to_string("1.0 kB", ByteSize::kib(1), Format::Si); + + assert_to_string("293.9 KiB", ByteSize::kb(301), Format::Iec); + assert_to_string("301.0 kB", ByteSize::kb(301), Format::Si); + + assert_to_string("1.0 MiB", ByteSize::mib(1), Format::Iec); + assert_to_string("1.0 MB", ByteSize::mib(1), Format::Si); + + assert_to_string("1.9 GiB", ByteSize::mib(1907), Format::Iec); + assert_to_string("2.0 GB", ByteSize::mib(1908), Format::Si); + + assert_to_string("399.6 MiB", ByteSize::mb(419), Format::Iec); + assert_to_string("419.0 MB", ByteSize::mb(419), Format::Si); + + assert_to_string("482.4 GiB", ByteSize::gb(518), Format::Iec); + assert_to_string("518.0 GB", ByteSize::gb(518), Format::Si); + + assert_to_string("741.2 TiB", ByteSize::tb(815), Format::Iec); + assert_to_string("815.0 TB", ByteSize::tb(815), Format::Si); + + assert_to_string("540.9 PiB", ByteSize::pb(609), Format::Iec); + assert_to_string("609.0 PB", ByteSize::pb(609), Format::Si); + } +} diff --git a/src/lib.rs b/src/lib.rs index 4e0e0c1..b6fbeca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,7 +24,9 @@ //! ``` //! use bytesize::ByteSize; //! -//! assert_eq!("482.4 GiB", ByteSize::gb(518).to_string()); +//! assert_eq!("518.0 GiB", ByteSize::gib(518).display().iec().to_string()); +//! assert_eq!("556.2 GB", ByteSize::gib(518).display().si().to_string()); +//! assert_eq!("518.0G", ByteSize::gib(518).display().iec_short().to_string()); //! ``` //! //! Arithmetic operations are supported. @@ -39,16 +41,22 @@ //! assert_eq!(ByteSize::gb(996), minus); //! ``` +use std::{ + fmt, + ops::{Add, AddAssign, Mul, MulAssign, Sub, SubAssign}, +}; + #[cfg(feature = "arbitrary")] mod arbitrary; +mod display; mod parse; #[cfg(feature = "serde")] mod serde; -use std::fmt::{self, Debug, Display, Formatter}; -use std::ops::{Add, AddAssign, Mul, MulAssign, Sub, SubAssign}; +pub use crate::display::Display; +use crate::display::Format; -/// Number of bytes in 1 kilobyte. +/// Number of bytes in 1 kilobyte pub const KB: u64 = 1_000; /// Number of bytes in 1 megabyte. pub const MB: u64 = 1_000_000; @@ -86,21 +94,6 @@ const LN_KIB: f64 = 6.931_471_805_599_453; /// `ln(1000) ~= 6.908` const LN_KB: f64 = 6.907_755_278_982_137; -/// Formatting style. -#[derive(Debug, Clone, Default)] -pub enum Format { - /// IEC (binary) representation. - /// - /// E.g., "1.0 MiB" - #[default] - IEC, - - /// SI (decimal) representation. - /// - /// E.g., "1.02 MB" - SI, -} - /// Converts a quantity of kilobytes to bytes. pub fn kb(size: impl Into) -> u64 { size.into() * KB @@ -227,55 +220,32 @@ impl ByteSize { pub const fn as_u64(&self) -> u64 { self.0 } -} -/// Constructs human-readable string representation of `bytes` with given `format` style. -pub fn to_string_format(bytes: u64, format: Format) -> String { - let unit = match format { - Format::IEC => KIB, - Format::SI => KB, - }; - let unit_base = match format { - Format::IEC => LN_KIB, - Format::SI => LN_KB, - }; - - let unit_prefix = match format { - Format::IEC => UNITS_IEC.as_bytes(), - Format::SI => UNITS_SI.as_bytes(), - }; - let unit_suffix = match format { - Format::IEC => "iB", - Format::SI => "B", - }; - - if bytes < unit { - format!("{} B", bytes) - } else { - let size = bytes as f64; - let exp = match (size.ln() / unit_base) as usize { - 0 => 1, - e => e, - }; - - format!( - "{:.1} {}{}", - (size / unit.pow(exp as u32) as f64), - unit_prefix[exp - 1] as char, - unit_suffix - ) + /// Returns a formatting display wrapper. + pub fn display(&self) -> Display { + Display { + byte_size: *self, + format: Format::Iec, + } } } -impl Display for ByteSize { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - f.pad(&to_string_format(self.0, Format::IEC)) +impl fmt::Display for ByteSize { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let display = self.display(); + + if f.width().is_none() && f.precision().is_none() { + // allocation-free fast path for when no formatting options are specified + write!(f, "{display}") + } else { + f.pad(&display.to_string()) + } } } -impl Debug for ByteSize { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - ::fmt(self, f) +impl fmt::Debug for ByteSize { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} ({} bytes)", self, self.0) } } @@ -509,48 +479,8 @@ mod tests { assert_eq!("|--357 B---|", format!("|{:-^10}|", ByteSize(357))); } - #[track_caller] - fn assert_to_string(expected: &str, b: ByteSize, format: Format) { - assert_eq!(expected.to_string(), to_string_format(b.0, format)); - } - - #[test] - fn test_to_string_as() { - assert_to_string("215 B", ByteSize::b(215), Format::IEC); - assert_to_string("215 B", ByteSize::b(215), Format::SI); - - assert_to_string("1.0 KiB", ByteSize::kib(1), Format::IEC); - assert_to_string("1.0 kB", ByteSize::kib(1), Format::SI); - - assert_to_string("293.9 KiB", ByteSize::kb(301), Format::IEC); - assert_to_string("301.0 kB", ByteSize::kb(301), Format::SI); - - assert_to_string("1.0 MiB", ByteSize::mib(1), Format::IEC); - assert_to_string("1.0 MB", ByteSize::mib(1), Format::SI); - - assert_to_string("1.9 GiB", ByteSize::mib(1907), Format::IEC); - assert_to_string("2.0 GB", ByteSize::mib(1908), Format::SI); - - assert_to_string("399.6 MiB", ByteSize::mb(419), Format::IEC); - assert_to_string("419.0 MB", ByteSize::mb(419), Format::SI); - - assert_to_string("482.4 GiB", ByteSize::gb(518), Format::IEC); - assert_to_string("518.0 GB", ByteSize::gb(518), Format::SI); - - assert_to_string("741.2 TiB", ByteSize::tb(815), Format::IEC); - assert_to_string("815.0 TB", ByteSize::tb(815), Format::SI); - - assert_to_string("540.9 PiB", ByteSize::pb(609), Format::IEC); - assert_to_string("609.0 PB", ByteSize::pb(609), Format::SI); - } - #[test] fn test_default() { assert_eq!(ByteSize::b(0), ByteSize::default()); } - - #[test] - fn test_to_string() { - assert_to_string("609.0 PB", ByteSize::pb(609), Format::SI); - } } diff --git a/src/parse.rs b/src/parse.rs index 83c5c5d..dc38e65 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -181,8 +181,6 @@ impl std::str::FromStr for Unit { #[cfg(test)] mod tests { - use crate::to_string_format; - use super::*; #[test] @@ -234,9 +232,9 @@ mod tests { s.parse::().unwrap().0 } - assert_eq!(parse(&format!("{}", parse("128GB"))), 128 * Unit::GigaByte); + assert_eq!(parse(&parse("128GB").to_string()), 128 * Unit::GigaByte); assert_eq!( - parse(&to_string_format(parse("128.000 GiB"), crate::Format::IEC)), + parse(&ByteSize(parse("128.000 GiB")).to_string()), 128 * Unit::GibiByte, ); }