diff --git a/CHANGELOG.md b/CHANGELOG.md index d0bff8c..1e2f220 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,15 @@ All significant changes to this software be documented in this file. ## Unreleased +## v0.3.0 (2026-06-28) + ### Breaking changes * Renamed the old `BSize::with` mapping API to `BSize::map`. * Renamed the generic `BSize` wrapper to `ByteSize`. `BSize` is now an alias for `ByteSize`. * Renamed the `ByteSize` trait to `BaseByteSize`. * Removed the `Displayable` trait. `BaseByteSize` now has a `to_f64` method that is the same as the `Displayable::canonicalize` method. +* Made the inner `ByteSize` field private. Use `ByteSize::b` to construct byte sizes and `ByteSize::bytes` to get the exact underlying byte count. ### New features @@ -17,6 +20,8 @@ All significant changes to this software be documented in this file. * Added `BSize8`, `BSize16`, `BSize32`, and `BSize64` aliases. * Added `BaseByteSize::to_f64` for converting supported byte size base types to approximate `f64` values. * Added `BSize::as_b` for returning the byte count as an approximate `f64`. +* Added support for formatting positive infinity with `Display::new`, which acts as an overflow marker. +* Added `ByteSize::bytes` for returning the exact byte count as the underlying integer type. ## v0.2.1 (2026-06-27) diff --git a/Cargo.lock b/Cargo.lock index 27a47e7..0ab4924 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -69,7 +69,7 @@ checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "bsize" -version = "0.3.0-rc.2" +version = "0.3.0" dependencies = [ "insta", "macroweave", diff --git a/README.md b/README.md index 0370295..b24fb35 100644 --- a/README.md +++ b/README.md @@ -89,15 +89,15 @@ The [`bytesize`](https://crates.io/crates/bytesize) crate provides a `ByteSize` I was more than happy to try `bytesize` at first. However, I found that it does not provide a way to specify the underlying integer type for the byte size. It uses `u64` internally, while most of the constants shown above are of type `usize`. This means that I have to convert between `u64` and `usize` frequently, which is not ideal. See [this issue](https://github.com/bytesize-rs/bytesize/issues/135) for more details. -What's more, to support calculations between byte size wrappers and numeric types, this crate implements `ByteSize::map` for producing a new wrapper, and exposes the `.0` field for arbitrary calculations from the underlying byte count. This avoids implementing arithmetic traits for calculations between byte size wrappers and numeric types. The latter would cause confusions like what result type should be used for `bytesize::ByteSize + u64`. However, `ByteSize` implements arithmetic traits for calculations between wrappers with the same base type, which is more intuitive and less error-prone. +What's more, to support calculations between byte size wrappers and numeric types, this crate implements `ByteSize::map` for producing a new wrapper, and exposes `ByteSize::bytes` for extracting the exact underlying byte count. This avoids implementing arithmetic traits for calculations between byte size wrappers and numeric types. The latter would cause confusions like what result type should be used for `bytesize::ByteSize + u64`. However, `ByteSize` implements arithmetic traits for calculations between wrappers with the same base type, which is more intuitive and less error-prone. ```rust let result = bytesize::ByteSize::kib(4) + 64; // Is the result type bytesize::ByteSize or u64? Why? let result = BSize64::kib(4).map(|b| b + 64); // Clearly the result type is BSize64. -let result = BSize64::kib(4).0 + 64; // Clearly the result type is u64. +let result = BSize64::kib(4).bytes() + 64; // Clearly the result type is u64. ``` -There is no `Unit` as well. To obtain a constant for a specific unit, you can use `BSize64::kib(1).0` and this can be resolved at compile time. +There is no `Unit` as well. To obtain a constant for a specific unit, you can use `BSize64::kib(1).bytes()` and this can be resolved at compile time. Finally, the following issues in `bytesize` have been resolved in this crate: diff --git a/bsize/Cargo.toml b/bsize/Cargo.toml index ffc8abe..743281f 100644 --- a/bsize/Cargo.toml +++ b/bsize/Cargo.toml @@ -14,7 +14,7 @@ [package] name = "bsize" -version = "0.3.0-rc.2" +version = "0.3.0" categories = ["development-tools", "no-std", "value-formatting"] description = "Semantic wrappers and utilities for byte size representations." diff --git a/bsize/src/display.rs b/bsize/src/display.rs index c5e3285..2ccda73 100644 --- a/bsize/src/display.rs +++ b/bsize/src/display.rs @@ -31,7 +31,7 @@ impl ByteSize { /// /// See [`Display`] for examples. pub fn display(&self) -> Display { - Display::new(self.0.to_f64()) + Display::new(self.bytes().to_f64()) } } @@ -327,9 +327,9 @@ impl Display { /// /// # Panics /// - /// Panics if the `size` is not finite or is negative. + /// Panics if the `size` is NaN or negative. pub fn new(size: f64) -> Self { - assert!(size.is_finite() && size >= 0.0); + assert!(size >= 0.0, "size must be non-negative and not NaN"); let options = DisplayOptions::BINARY; Self { size, options } } @@ -524,15 +524,15 @@ mod tests { } #[test] - #[should_panic] - fn test_new_rejects_nan_size() { - Display::new(f64::NAN); + fn test_formats_infinite_size() { + assert_snapshot!(Display::new(f64::INFINITY).binary(), @"inf EiB"); + assert_snapshot!(Display::new(f64::INFINITY).decimal(), @"inf EB"); } #[test] #[should_panic] - fn test_new_rejects_infinite_size() { - Display::new(f64::INFINITY); + fn test_new_rejects_nan_size() { + Display::new(f64::NAN); } #[test] diff --git a/bsize/src/types/mod.rs b/bsize/src/impls/mod.rs similarity index 76% rename from bsize/src/types/mod.rs rename to bsize/src/impls/mod.rs index 45a8941..588ee6f 100644 --- a/bsize/src/types/mod.rs +++ b/bsize/src/impls/mod.rs @@ -15,27 +15,9 @@ use core::any::type_name; use core::fmt; +use crate::ByteSize; use crate::traits::BaseByteSize; -/// Byte size representation. -#[derive(Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct ByteSize(pub T); - -/// Byte size representation backed by `usize`. -pub type BSize = ByteSize; - -/// Byte size representation backed by `u8`. -pub type BSize8 = ByteSize; - -/// Byte size representation backed by `u16`. -pub type BSize16 = ByteSize; - -/// Byte size representation backed by `u32`. -pub type BSize32 = ByteSize; - -/// Byte size representation backed by `u64`. -pub type BSize64 = ByteSize; - impl fmt::Debug for ByteSize { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "ByteSize<{}>({:?})", type_name::(), self.0) @@ -59,6 +41,12 @@ impl ByteSize { pub const fn b(size: T) -> Self { ByteSize(size) } + + /// Returns the exact byte count as the underlying integer type. + #[inline(always)] + pub const fn bytes(self) -> T { + self.0 + } } #[cfg(feature = "nightly")] @@ -68,12 +56,12 @@ mod stable; #[cfg(test)] mod tests { - use super::BSize; - use super::BSize8; - use super::BSize16; - use super::BSize32; - use super::BSize64; - use super::ByteSize; + use crate::BSize; + use crate::BSize8; + use crate::BSize16; + use crate::BSize32; + use crate::BSize64; + use crate::ByteSize; use crate::assert_close; #[test] @@ -101,9 +89,15 @@ mod tests { assert_eq!(BSize64::eib(2), ByteSize::::eib(2)); } + #[test] + fn returns_exact_bytes() { + const KIB: u64 = BSize64::kib(1).bytes(); + assert_eq!(KIB, 1_024); + } + #[test] fn constructs_u8_units() { - assert_eq!(BSize8::b(2).0, 2); + assert_eq!(BSize8::b(2).bytes(), 2); } #[test] @@ -122,8 +116,8 @@ mod tests { #[test] fn constructs_u16_units() { - assert_eq!(BSize16::kb(2).0, 2_000); - assert_eq!(BSize16::kib(2).0, 2_048); + assert_eq!(BSize16::kb(2).bytes(), 2_000); + assert_eq!(BSize16::kib(2).bytes(), 2_048); } #[test] @@ -144,8 +138,8 @@ mod tests { #[test] fn constructs_u32_units() { - assert_eq!(BSize32::gb(2).0, 2_000_000_000); - assert_eq!(BSize32::gib(2).0, 2_147_483_648); + assert_eq!(BSize32::gb(2).bytes(), 2_000_000_000); + assert_eq!(BSize32::gib(2).bytes(), 2_147_483_648); } #[test] @@ -166,8 +160,8 @@ mod tests { #[test] fn constructs_u64_units() { - assert_eq!(BSize64::eb(2).0, 2_000_000_000_000_000_000); - assert_eq!(BSize64::eib(2).0, 2_305_843_009_213_693_952); + assert_eq!(BSize64::eb(2).bytes(), 2_000_000_000_000_000_000); + assert_eq!(BSize64::eib(2).bytes(), 2_305_843_009_213_693_952); } #[test] @@ -178,8 +172,8 @@ mod tests { #[cfg(target_pointer_width = "16")] #[test] fn returns_usize_units() { - assert_eq!(BSize::kb(2).0, 2_000); - assert_eq!(BSize::kib(2).0, 2_048); + assert_eq!(BSize::kb(2).bytes(), 2_000); + assert_eq!(BSize::kib(2).bytes(), 2_048); assert_close(BSize::kb(2).as_kb(), 2.0); assert_close(BSize::kib(2).as_kib(), 2.0); } @@ -187,12 +181,12 @@ mod tests { #[cfg(target_pointer_width = "32")] #[test] fn returns_usize_units() { - assert_eq!(BSize::kb(2).0, 2_000); - assert_eq!(BSize::kib(2).0, 2_048); + assert_eq!(BSize::kb(2).bytes(), 2_000); + assert_eq!(BSize::kib(2).bytes(), 2_048); assert_close(BSize::kb(2).as_kb(), 2.0); assert_close(BSize::kib(2).as_kib(), 2.0); - assert_eq!(BSize::gb(2).0, 2_000_000_000); - assert_eq!(BSize::gib(2).0, 2_147_483_648); + assert_eq!(BSize::gb(2).bytes(), 2_000_000_000); + assert_eq!(BSize::gib(2).bytes(), 2_147_483_648); assert_close(BSize::gb(2).as_gb(), 2.0); assert_close(BSize::gib(2).as_gib(), 2.0); } @@ -200,16 +194,16 @@ mod tests { #[cfg(target_pointer_width = "64")] #[test] fn returns_usize_units() { - assert_eq!(BSize::kb(2).0, 2_000); - assert_eq!(BSize::kib(2).0, 2_048); + assert_eq!(BSize::kb(2).bytes(), 2_000); + assert_eq!(BSize::kib(2).bytes(), 2_048); assert_close(BSize::kb(2).as_kb(), 2.0); assert_close(BSize::kib(2).as_kib(), 2.0); - assert_eq!(BSize::gb(2).0, 2_000_000_000); - assert_eq!(BSize::gib(2).0, 2_147_483_648); + assert_eq!(BSize::gb(2).bytes(), 2_000_000_000); + assert_eq!(BSize::gib(2).bytes(), 2_147_483_648); assert_close(BSize::gb(2).as_gb(), 2.0); assert_close(BSize::gib(2).as_gib(), 2.0); - assert_eq!(BSize::eb(2).0, 2_000_000_000_000_000_000); - assert_eq!(BSize::eib(2).0, 2_305_843_009_213_693_952); + assert_eq!(BSize::eb(2).bytes(), 2_000_000_000_000_000_000); + assert_eq!(BSize::eib(2).bytes(), 2_305_843_009_213_693_952); assert_close(BSize::eb(2).as_eb(), 2.0); assert_close(BSize::eib(2).as_eib(), 2.0); } diff --git a/bsize/src/types/nightly.rs b/bsize/src/impls/nightly.rs similarity index 98% rename from bsize/src/types/nightly.rs rename to bsize/src/impls/nightly.rs index 2e733c5..cfef20a 100644 --- a/bsize/src/types/nightly.rs +++ b/bsize/src/impls/nightly.rs @@ -37,7 +37,8 @@ impl ByteSize { /// Returns byte count as bytes. /// /// The result is approximate when the byte count cannot be represented - /// exactly as `f64`. Use `.0` for the exact underlying integer value. + /// exactly as `f64`. Use [`ByteSize::bytes`] for the exact underlying + /// integer value. #[inline(always)] pub const fn as_b(&self) -> f64 where diff --git a/bsize/src/types/stable.rs b/bsize/src/impls/stable.rs similarity index 98% rename from bsize/src/types/stable.rs rename to bsize/src/impls/stable.rs index d61db25..a8b666c 100644 --- a/bsize/src/types/stable.rs +++ b/bsize/src/impls/stable.rs @@ -98,8 +98,8 @@ macroweave::repeat!(Ty in [u8, u16, u32, u64, usize] { /// Returns byte count as bytes. /// /// The result is approximate when the byte count cannot be - /// represented exactly as `f64`. Use `.0` for the exact underlying - /// integer value. + /// represented exactly as `f64`. Use [`ByteSize::bytes`] for the + /// exact underlying integer value. #[inline(always)] pub const fn as_b(&self) -> f64 { self.0 as f64 diff --git a/bsize/src/lib.rs b/bsize/src/lib.rs index b91d3ff..6246a08 100644 --- a/bsize/src/lib.rs +++ b/bsize/src/lib.rs @@ -53,7 +53,7 @@ //! assert!(BSize::kib(4) > BSize::kb(4)); //! //! let size: BSize = BSize::b(4_096); -//! assert_eq!(size.0, 4_096); +//! assert_eq!(size.bytes(), 4_096); //! ``` //! //! Parse byte sizes from strings. @@ -113,12 +113,12 @@ extern crate alloc; mod display; +mod impls; mod ops; mod parse; #[cfg(feature = "serde")] mod serde; mod traits; -mod types; pub use self::display::Display; pub use self::display::DisplayBaseUnit; @@ -134,12 +134,28 @@ pub use self::traits::KiloByteSize; pub use self::traits::MegaByteSize; pub use self::traits::PetaByteSize; pub use self::traits::TeraByteSize; -pub use self::types::BSize; -pub use self::types::BSize8; -pub use self::types::BSize16; -pub use self::types::BSize32; -pub use self::types::BSize64; -pub use self::types::ByteSize; + +/// Byte size representation. +/// +/// Use [`ByteSize::b`] to construct a value from bytes and [`ByteSize::bytes`] to get +/// the exact underlying byte count. +#[derive(Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ByteSize(T); + +/// Byte size representation backed by `usize`. +pub type BSize = ByteSize; + +/// Byte size representation backed by `u8`. +pub type BSize8 = ByteSize; + +/// Byte size representation backed by `u16`. +pub type BSize16 = ByteSize; + +/// Byte size representation backed by `u32`. +pub type BSize32 = ByteSize; + +/// Byte size representation backed by `u64`. +pub type BSize64 = ByteSize; #[cfg(test)] fn assert_close(actual: f64, expected: f64) { @@ -161,7 +177,7 @@ mod property_tests { impl quickcheck::Arbitrary for ByteSize { fn arbitrary(g: &mut quickcheck::Gen) -> Self { - Self(u64::arbitrary(g)) + ByteSize::b(u64::arbitrary(g)) } } diff --git a/bsize/src/ops/mod.rs b/bsize/src/ops/mod.rs index 66bf07d..4c11810 100644 --- a/bsize/src/ops/mod.rs +++ b/bsize/src/ops/mod.rs @@ -27,33 +27,33 @@ mod tests { #[test] fn adds_byte_sizes() { - assert_eq!((BSize8::b(3) + BSize8::b(5)).0, 8); - assert_eq!((BSize16::b(3) + BSize16::b(5)).0, 8); - assert_eq!((BSize32::b(3) + BSize32::b(5)).0, 8); - assert_eq!((BSize64::b(3) + BSize64::b(5)).0, 8); - assert_eq!((BSize::b(3) + BSize::b(5)).0, 8); + assert_eq!((BSize8::b(3) + BSize8::b(5)).bytes(), 8); + assert_eq!((BSize16::b(3) + BSize16::b(5)).bytes(), 8); + assert_eq!((BSize32::b(3) + BSize32::b(5)).bytes(), 8); + assert_eq!((BSize64::b(3) + BSize64::b(5)).bytes(), 8); + assert_eq!((BSize::b(3) + BSize::b(5)).bytes(), 8); } #[test] fn add_assigns_byte_sizes() { let mut size = BSize::b(3); size += BSize::b(5); - assert_eq!(size.0, 8); + assert_eq!(size.bytes(), 8); } #[test] fn subtracts_byte_sizes() { - assert_eq!((BSize8::b(8) - BSize8::b(5)).0, 3); - assert_eq!((BSize16::b(8) - BSize16::b(5)).0, 3); - assert_eq!((BSize32::b(8) - BSize32::b(5)).0, 3); - assert_eq!((BSize64::b(8) - BSize64::b(5)).0, 3); - assert_eq!((BSize::b(8) - BSize::b(5)).0, 3); + assert_eq!((BSize8::b(8) - BSize8::b(5)).bytes(), 3); + assert_eq!((BSize16::b(8) - BSize16::b(5)).bytes(), 3); + assert_eq!((BSize32::b(8) - BSize32::b(5)).bytes(), 3); + assert_eq!((BSize64::b(8) - BSize64::b(5)).bytes(), 3); + assert_eq!((BSize::b(8) - BSize::b(5)).bytes(), 3); } #[test] fn sub_assigns_byte_sizes() { let mut size = BSize::b(8); size -= BSize::b(5); - assert_eq!(size.0, 3); + assert_eq!(size.bytes(), 3); } } diff --git a/bsize/src/ops/nightly.rs b/bsize/src/ops/nightly.rs index 6a7697d..3a1ecca 100644 --- a/bsize/src/ops/nightly.rs +++ b/bsize/src/ops/nightly.rs @@ -14,8 +14,8 @@ use core::ops; +use crate::ByteSize; use crate::traits::BaseByteSize; -use crate::types::ByteSize; const impl ops::Add> for ByteSize where diff --git a/bsize/src/ops/stable.rs b/bsize/src/ops/stable.rs index aa1a6c4..fd09eec 100644 --- a/bsize/src/ops/stable.rs +++ b/bsize/src/ops/stable.rs @@ -14,7 +14,7 @@ use core::ops; -use crate::types::ByteSize; +use crate::ByteSize; macroweave::repeat!(Ty in [u8, u16, u32, u64, usize] { impl ops::Add> for ByteSize { diff --git a/bsize/src/parse.rs b/bsize/src/parse.rs index 8795309..5fb2abb 100644 --- a/bsize/src/parse.rs +++ b/bsize/src/parse.rs @@ -58,7 +58,7 @@ where T: BaseByteSize + TryFrom, { T::try_from(size) - .map(ByteSize) + .map(ByteSize::b) .map_err(|_| ParseError::Overflow) } @@ -205,7 +205,7 @@ mod tests { fn assert_parse_ok(input: &str, expected: u64) { let actual = ByteSize::::from_str(input).unwrap(); - let expected = ByteSize::(expected); + let expected = ByteSize::::b(expected); assert_eq!(actual, expected, "input: {input:?}"); let round_trip = actual.to_string().parse::>().unwrap(); diff --git a/bsize/src/serde.rs b/bsize/src/serde.rs index ba53d67..79d6b6e 100644 --- a/bsize/src/serde.rs +++ b/bsize/src/serde.rs @@ -29,7 +29,7 @@ macro_rules! impl_serialize { if ser.is_human_readable() { ser.collect_str(self) } else { - self.0.serialize(ser) + self.bytes().serialize(ser) } } } @@ -59,7 +59,7 @@ macro_rules! impl_deserialize { fn visit_i64(self, value: i64) -> Result { if let Ok(val) = u64::try_from(value) { if val <= <$ty>::MAX as u64 { - Ok(ByteSize(val as $ty)) + Ok(ByteSize::b(val as $ty)) } else { Err(E::invalid_value( de::Unexpected::Signed(value), @@ -76,7 +76,7 @@ macro_rules! impl_deserialize { fn visit_u64(self, value: u64) -> Result { if value <= <$ty>::MAX as u64 { - Ok(ByteSize(value as $ty)) + Ok(ByteSize::b(value as $ty)) } else { Err(E::invalid_value( de::Unexpected::Unsigned(value), @@ -158,6 +158,6 @@ mod tests { assert_eq!(json, "\"1048576 B\""); let deserialized = serde_json::from_str::(&json).unwrap(); - assert_eq!(deserialized.0, 1048576); + assert_eq!(deserialized.bytes(), 1048576); } } diff --git a/xtask/src/main.rs b/xtask/src/main.rs index f186321..4096d39 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -31,7 +31,6 @@ struct Command { impl Command { fn run(self) { match self.sub { - SubCommand::Build(cmd) => cmd.run(), SubCommand::Lint(cmd) => cmd.run(), SubCommand::Test(cmd) => cmd.run(), } @@ -40,33 +39,12 @@ impl Command { #[derive(Subcommand)] enum SubCommand { - #[clap(about = "Compile all workspace targets.")] - Build(CommandBuild), #[clap(about = "Run workspace quality checks.")] Lint(CommandLint), #[clap(about = "Run workspace unit tests.")] Test(CommandTest), } -#[derive(Parser)] -struct CommandBuild { - #[arg(long, help = "Assert that `Cargo.lock` will remain unchanged.")] - locked: bool, - - #[arg( - long, - value_delimiter = ',', - help = "Enable package features for the build." - )] - features: Vec, -} - -impl CommandBuild { - fn run(self) { - run_command(make_build_cmd(self.locked, &self.features)); - } -} - #[derive(Parser)] struct CommandTest { #[arg(long, help = "Run tests serially and do not capture output.")] @@ -93,7 +71,10 @@ struct CommandLint { impl CommandLint { fn run(self) { - run_command(make_clippy_cmd(self.fix)); + run_command(make_clippy_cmd(self.fix, &[])); + run_command(make_clippy_cmd(self.fix, &["serde"])); + run_command(make_clippy_cmd(self.fix, &["nightly"])); + run_command(make_clippy_cmd(self.fix, &["nightly", "serde"])); run_command(make_format_cmd(self.fix)); run_command(make_taplo_cmd(self.fix)); run_command(make_typos_cmd()); @@ -128,28 +109,6 @@ fn run_command(mut cmd: StdCommand) { assert!(status.success(), "command failed: {status}"); } -fn make_build_cmd(locked: bool, features: &[String]) -> StdCommand { - let mut cmd = find_command("cargo"); - if features.iter().any(|feature| feature == "nightly") { - cmd.arg("+nightly"); - } - cmd.args([ - "build", - "--workspace", - "--tests", - "--examples", - "--benches", - "--bins", - ]); - if !features.is_empty() { - cmd.arg("--features").arg(features.join(",")); - } - if locked { - cmd.arg("--locked"); - } - cmd -} - fn make_test_cmd(no_capture: bool, features: &[&str]) -> StdCommand { let mut cmd = find_command("cargo"); cmd.args(["test", "--workspace", "--no-default-features"]); @@ -171,16 +130,18 @@ fn make_format_cmd(fix: bool) -> StdCommand { cmd } -fn make_clippy_cmd(fix: bool) -> StdCommand { +fn make_clippy_cmd(fix: bool, features: &[&str]) -> StdCommand { let mut cmd = find_command("cargo"); cmd.args([ "+nightly", "clippy", "--tests", - "--all-features", "--all-targets", "--workspace", ]); + if !features.is_empty() { + cmd.arg("--features").arg(features.join(",")); + } if fix { cmd.args(["--allow-staged", "--allow-dirty", "--fix"]); } else {