Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- Add display styles for IEC and SI bit units.

## 2.5.0

- Honor precision when a width is set with formatting args.
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ use bytesize::ByteSize;
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());
assert_eq!("4.0 Kib", ByteSize::b(512).display().iec_bits().to_string());
assert_eq!("4.1 kb", ByteSize::b(512).display().si_bits().to_string());
```

Arithmetic operations are supported.
Expand Down
151 changes: 140 additions & 11 deletions src/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,48 @@ use core::{fmt, write};

use crate::ByteSize;

const BITS_PER_BYTE: u64 = 8;

/// Format / style to use when displaying a [`ByteSize`].
#[derive(Debug, Clone, Copy)]
pub(crate) enum Format {
Iec,
IecShort,
Si,
SiShort,
IecBits,
SiBits,
}

impl Format {
fn unit(self) -> u64 {
match self {
Format::Iec | Format::IecShort => crate::KIB,
Format::Si | Format::SiShort => crate::KB,
Format::IecBits => crate::KIB,
Format::SiBits => crate::KB,
}
}

fn unit_base(self) -> f64 {
match self {
Format::Iec | Format::IecShort => crate::LN_KIB,
Format::Si | Format::SiShort => crate::LN_KB,
Format::IecBits => crate::LN_KIB,
Format::SiBits => 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(),
Format::Iec | Format::IecShort | Format::IecBits => crate::UNITS_IEC.as_bytes(),
Format::Si | Format::SiShort | Self::SiBits => crate::UNITS_SI.as_bytes(),
}
}

fn unit_separator(self) -> &'static str {
match self {
Format::Iec | Format::Si => " ",
Format::Iec | Format::Si | Format::IecBits | Format::SiBits => " ",
Format::IecShort | Format::SiShort => "",
}
}
Expand All @@ -45,8 +53,14 @@ impl Format {
Format::Iec => "iB",
Format::Si => "B",
Format::IecShort | Format::SiShort => "",
Format::IecBits => "ib",
Format::SiBits => "b",
}
}

fn is_bits(self) -> bool {
matches!(self, Format::IecBits | Format::SiBits)
}
}

/// Formatting display wrapper for [`ByteSize`].
Expand Down Expand Up @@ -115,12 +129,32 @@ impl Display {
self.format = Format::SiShort;
self
}

/// Format as equivalent number of bits using IEC (binary) units.
///
/// E.g., `12.3 Mib`.
#[must_use]
pub fn iec_bits(mut self) -> Self {
self.format = Format::IecBits;
self
}

/// Format as equivalent number of bits using SI (decimal) units.
///
/// E.g., `12.3 Mb`.
#[must_use]
pub fn si_bits(mut self) -> Self {
self.format = Format::SiBits;
self
}
}

impl fmt::Display for Display {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let bytes = self.byte_size.as_u64();

let is_bits = self.format.is_bits();

let unit = self.format.unit();
#[allow(unused_variables)] // used in std contexts
let unit_base = self.format.unit_base();
Expand All @@ -130,10 +164,26 @@ impl fmt::Display for Display {
let unit_suffix = self.format.unit_suffix();
let precision = f.precision().unwrap_or(1);

if bytes < unit {
write!(f, "{bytes}{unit_separator}B")?;
let threshold = if is_bits {
unit.div_ceil(BITS_PER_BYTE)
} else {
let size = bytes as f64;
unit
};

if bytes < threshold {
let value = if is_bits {
bytes * BITS_PER_BYTE
} else {
bytes
};

if is_bits {
write!(f, "{value}{unit_separator}b")?;
} else {
write!(f, "{value}{unit_separator}B")?;
}
} else {
let size = bytes as f64 * if is_bits { BITS_PER_BYTE as f64 } else { 1.0 };

#[cfg(feature = "std")]
let exp = ideal_unit_std(size, unit, unit_base);
Expand Down Expand Up @@ -199,6 +249,7 @@ fn ideal_unit_std(size: f64, unit: u64, unit_base: f64) -> usize {
#[cfg(test)]
mod tests {
use alloc::{format, string::ToString as _};
use core::fmt::Write as _;

use super::*;

Expand All @@ -225,6 +276,29 @@ mod tests {

ideal_unit_std(size, crate::KB, crate::LN_KB) == ideal_unit_no_std(size, crate::KB)
}

#[test]
fn ideal_unit_selection_std_no_std_iec_bits(bytes: ByteSize) -> bool {
if bytes.0 < 128 {
return true;
}

let size = bytes.0 as f64 * BITS_PER_BYTE as f64;

ideal_unit_std(size, crate::KIB, crate::LN_KIB)
== ideal_unit_no_std(size, crate::KIB)
}

#[test]
fn ideal_unit_selection_std_no_std_si_bits(bytes: ByteSize) -> bool {
if bytes.0 < 125 {
return true;
}

let size = bytes.0 as f64 * BITS_PER_BYTE as f64;

ideal_unit_std(size, crate::KB, crate::LN_KB) == ideal_unit_no_std(size, crate::KB)
}
}

// Regression test for #142 / the `std` vs `no_std` display divergence: `f64::ln()` isn't
Expand Down Expand Up @@ -285,6 +359,11 @@ mod tests {
);
}

#[track_caller]
fn assert_to_string(expected: &str, byte_size: ByteSize, format: Format) {
assert_eq!(expected, Display { byte_size, format }.to_string());
}

#[test]
fn to_string_iec() {
let display = Display {
Expand Down Expand Up @@ -330,11 +409,6 @@ mod tests {
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);
Expand Down Expand Up @@ -365,6 +439,61 @@ mod tests {
assert_to_string("609.0 PB", ByteSize::pb(609), Format::Si);
}

#[test]
fn to_string_bits() {
assert_to_string("1016 b", ByteSize(127), Format::IecBits);
assert_to_string("1.0 Kib", ByteSize(128), Format::IecBits);
assert_to_string("992 b", ByteSize(124), Format::SiBits);
assert_to_string("1.0 kb", ByteSize(125), Format::SiBits);

assert_to_string("7.8 Kib", ByteSize::kb(1), Format::IecBits);
assert_to_string("8.0 kb", ByteSize::kb(1), Format::SiBits);
assert_to_string("128.0 Eib", ByteSize(u64::MAX), Format::IecBits);
assert_to_string("147.6 Eb", ByteSize(u64::MAX), Format::SiBits);
}

#[test]
fn display_bits_public_api() {
assert_eq!("1.0 Kib", ByteSize(128).display().iec_bits().to_string());
assert_eq!("1.0 kb", ByteSize(125).display().si_bits().to_string());
}

#[test]
fn display_propagates_write_errors() {
struct FailingWriter;

impl fmt::Write for FailingWriter {
fn write_str(&mut self, _: &str) -> fmt::Result {
Err(fmt::Error)
}
}

let mut writer = FailingWriter;

assert_eq!(
Err(fmt::Error),
write!(
writer,
"{}",
Display {
byte_size: ByteSize(127),
format: Format::IecBits,
},
),
);
assert_eq!(
Err(fmt::Error),
write!(
writer,
"{}",
Display {
byte_size: ByteSize(1),
format: Format::Iec,
},
),
);
}

#[test]
fn precision() {
let size = ByteSize::mib(1908);
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
//! 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());
//! assert_eq!("4.0 Kib", ByteSize::b(512).display().iec_bits().to_string());
//! assert_eq!("4.1 kb", ByteSize::b(512).display().si_bits().to_string());
//! ```
//!
//! Arithmetic operations are supported.
Expand Down
Loading