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
36 changes: 36 additions & 0 deletions bytescale/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,42 @@ mod tests {
assert_eq!(humanbyte::parse("1.5 KiB"), Ok(1536));
}

#[test]
fn test_sum() {
let sizes = [ByteScale::kib(1), ByteScale::kib(2), ByteScale::kib(3)];
assert_eq!(sizes.iter().sum::<ByteScale>(), ByteScale::kib(6));
assert_eq!(sizes.into_iter().sum::<ByteScale>(), ByteScale::kib(6));
}

#[test]
fn test_exabytes() {
assert_eq!(ByteScale::eib(1).as_u64(), 1_152_921_504_606_846_976);
assert_eq!(ByteScale::eb(1).as_u64(), 1_000_000_000_000_000_000);
// the display/parse roundtrip works at the top of the u64 range
let max = ByteScale(u64::MAX);
assert_display!("16.0 EiB", max);
assert_eq!("15 EiB".parse::<ByteScale>().unwrap(), ByteScale::eib(15));
// 16 EiB is exactly 2^64: one past u64::MAX, so it must error
assert!("16 EiB".parse::<ByteScale>().is_err());
}

#[test]
fn test_float_accessors() {
assert_eq!(ByteScale::kib(1).as_kib(), 1.0);
assert_eq!(ByteScale::kib(1).as_kb(), 1.024);
assert_eq!(ByteScale::gib(3).as_mib(), 3072.0);
}

#[test]
fn test_display_precision() {
let x = ByteScale::kib(1) + 512u64;
assert_eq!(format!("{x:.2}"), "1.50 KiB");
assert_eq!(format!("{x:.0}"), "2 KiB");
assert_eq!(format!("{x}"), "1.5 KiB");
// precision composes with width/alignment
assert_eq!(format!("|{x:>10.2}|"), "| 1.50 KiB|");
}

#[test]
#[should_panic(expected = "byte size overflows u64")]
fn test_constructor_overflow() {
Expand Down
57 changes: 56 additions & 1 deletion humanbyte-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ fn constructor_tokens(name: &syn::Ident) -> proc_macro2::TokenStream {
("tib", "::humanbyte::TIB", "tebibytes"),
("pb", "::humanbyte::PB", "petabytes"),
("pib", "::humanbyte::PIB", "pebibytes"),
("eb", "::humanbyte::EB", "exabytes"),
("eib", "::humanbyte::EIB", "exbibytes"),
];

// Generate methods
Expand Down Expand Up @@ -71,9 +73,25 @@ fn constructor_tokens(name: &syn::Ident) -> proc_macro2::TokenStream {
}
});

// Generate float accessors (skipping bytes, which has as_u64 in the parse derive)
let accessors = units[1..].iter().map(|(unit, multiplier, description)| {
let method_name = syn::Ident::new(&format!("as_{}", unit), Span::call_site());
let multiplier_expr: syn::Expr = syn::parse_str(multiplier).unwrap();
let doc_comment = format!("Returns the size in {} as a float.", description);

quote! {
#[doc = #doc_comment]
#[inline(always)]
pub fn #method_name(&self) -> f64 {
self.0 as f64 / #multiplier_expr as f64
}
}
});

quote! {
impl #name {
#(#methods)*
#(#accessors)*
}

impl From<u64> for #name {
Expand Down Expand Up @@ -229,6 +247,18 @@ fn ops_tokens(name: &syn::Ident) -> proc_macro2::TokenStream {
}
}

impl core::iter::Sum<#name> for #name {
fn sum<I: Iterator<Item = #name>>(iter: I) -> #name {
iter.fold(#name(0), |acc, x| #name(acc.0 + x.0))
}
}

impl<'a> core::iter::Sum<&'a #name> for #name {
fn sum<I: Iterator<Item = &'a #name>>(iter: I) -> #name {
iter.fold(#name(0), |acc, x| #name(acc.0 + x.0))
}
}

impl core::ops::Add<#name> for u64 {
type Output = #name;
#[inline(always)]
Expand Down Expand Up @@ -350,7 +380,32 @@ fn display_tokens(name: &syn::Ident) -> proc_macro2::TokenStream {
quote! {
impl core::fmt::Display for #name {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.pad(&::humanbyte::to_string(self.0, ::humanbyte::Format::IEC))
use core::fmt::Write as _;

// Interpret formatter precision as decimals: "{:.2}" is "1.50 KiB".
// Width/alignment are handled manually because Formatter::pad
// would reinterpret the precision as string truncation.
let precision = f.precision().unwrap_or(1);
let s = ::humanbyte::to_string_with_precision(
self.0,
::humanbyte::Format::IEC,
precision,
);
let pad = f.width().unwrap_or(0).saturating_sub(s.len());
let (left, right) = match f.align() {
Some(core::fmt::Alignment::Right) => (pad, 0),
Some(core::fmt::Alignment::Center) => (pad / 2, pad - pad / 2),
// strings left-align by default
_ => (0, pad),
};
for _ in 0..left {
f.write_char(f.fill())?;
}
f.write_str(&s)?;
for _ in 0..right {
f.write_char(f.fill())?;
}
Ok(())
}
}

Expand Down
48 changes: 48 additions & 0 deletions humanbyte/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ pub const GB: u64 = 1_000_000_000;
pub const TB: u64 = 1_000_000_000_000;
/// bytes size for 1 petabyte
pub const PB: u64 = 1_000_000_000_000_000;
/// bytes size for 1 exabyte
pub const EB: u64 = 1_000_000_000_000_000_000;

/// bytes size for 1 kibibyte
pub const KIB: u64 = 1_024;
Expand All @@ -50,6 +52,8 @@ pub const GIB: u64 = 1_073_741_824;
pub const TIB: u64 = 1_099_511_627_776;
/// bytes size for 1 pebibyte
pub const PIB: u64 = 1_125_899_906_842_624;
/// bytes size for 1 exbibyte
pub const EIB: u64 = 1_152_921_504_606_846_976;

/// IEC (binary) units.
///
Expand Down Expand Up @@ -121,6 +125,16 @@ pub fn parse(value: &str) -> Result<u64, ParseError> {
Ok(v) => {
let suffix = skip_while(&value[number.len()..], char::is_whitespace);
match suffix.parse::<Unit>() {
// Use exact integer arithmetic when the number has no fractional
// part: f64 only has a 53-bit mantissa, so byte counts at or above
// 2^53 would otherwise be silently rounded (e.g.
// "9007199254740993B" to ...992).
Ok(u) if !number.contains('.') => match number.parse::<u64>() {
Ok(n) => n.checked_mul(u64::from(u)).ok_or_else(|| {
ParseError(format!("{:?} overflows a u64 byte count", value))
}),
Err(_) => Ok((v * u64::from(u) as f64) as u64),
},
Ok(u) => Ok((v * u64::from(u) as f64) as u64),
Err(error) => Err(ParseError(format!(
"couldn't parse {:?} into a known SI unit, {}",
Expand Down Expand Up @@ -171,6 +185,8 @@ where
&s[(s.len() - offset)..]
}

#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Unit {
Byte,
// power of tens
Expand All @@ -179,12 +195,14 @@ pub enum Unit {
GigaByte,
TeraByte,
PetaByte,
ExaByte,
// power of twos
KibiByte,
MebiByte,
GibiByte,
TebiByte,
PebiByte,
ExbiByte,
}

impl From<Unit> for u64 {
Expand All @@ -197,12 +215,14 @@ impl From<Unit> for u64 {
Unit::GigaByte => GB,
Unit::TeraByte => TB,
Unit::PetaByte => PB,
Unit::ExaByte => EB,
// power of twos
Unit::KibiByte => KIB,
Unit::MebiByte => MIB,
Unit::GibiByte => GIB,
Unit::TebiByte => TIB,
Unit::PebiByte => PIB,
Unit::ExbiByte => EIB,
}
}
}
Expand All @@ -219,12 +239,14 @@ impl FromStr for Unit {
"g" | "gb" => Ok(Self::GigaByte),
"t" | "tb" => Ok(Self::TeraByte),
"p" | "pb" => Ok(Self::PetaByte),
"e" | "eb" => Ok(Self::ExaByte),
// power of twos
"ki" | "kib" => Ok(Self::KibiByte),
"mi" | "mib" => Ok(Self::MebiByte),
"gi" | "gib" => Ok(Self::GibiByte),
"ti" | "tib" => Ok(Self::TebiByte),
"pi" | "pib" => Ok(Self::PebiByte),
"ei" | "eib" => Ok(Self::ExbiByte),
_ => Err(ParseError(format!("couldn't parse unit of {:?}", unit))),
}
}
Expand Down Expand Up @@ -420,10 +442,36 @@ mod tests {
assert_eq!(parse("1.5KiB"), Ok(1536));
assert_eq!(parse("2 mb"), Ok(2_000_000));
assert_eq!(parse("3 g"), Ok(3_000_000_000));
assert_eq!(parse("1 EiB"), Ok(EIB));
assert_eq!(parse("2 eb"), Ok(2 * EB));
assert!(parse("").is_err());
assert!(parse("1.5 XB").is_err());
}

#[test]
fn test_parse_integer_exactness() {
// 2^53 + 1 is not representable as f64; the suffix path must not
// round it (upstream bytesize#171)
assert_eq!(parse("9007199254740993B"), Ok(9_007_199_254_740_993));
assert_eq!(parse("9007199254740993 B"), Ok(9_007_199_254_740_993));
assert_eq!(parse("18446744073709551615B"), Ok(u64::MAX));
// integer overflow errors instead of silently wrapping/saturating
assert!(parse("20000 PB").is_err());
// fractional values still take the f64 path
assert_eq!(parse("1.5 KiB"), Ok(1536));
}

#[test]
fn test_display_parse_roundtrip() {
// everything we can display must parse back (u64::MAX shows as EiB)
for bytes in [0, 1, 999, 1024, KIB, MIB, GIB, TIB, PIB, EIB, u64::MAX] {
for format in [Format::IEC, Format::SI] {
let s = to_string(bytes, format);
assert!(parse(&s).is_ok(), "couldn't parse back {s:?}");
}
}
}

#[test]
fn test_precision() {
assert_eq!(to_string_with_precision(TIB, Format::IEC, 2), "1.00 TiB");
Expand Down
Loading