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
11 changes: 7 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ All significant changes to this software be documented in this file.

### Breaking changes

* Renamed `BSize::with` to `BSize::map`. A new `BSize::with` method has been added that accepts a closure returning arbitrary type, allowing for mapping a `BSize` to any other type. This follows `LocalKey::with`'s naming conventions.
* Removed the `Displayable` trait. `ByteSize` now has a `to_f64` method that is the same as the `Displayable::canonicalize` method.
* Renamed the old `BSize::with` mapping API to `BSize::map`.
* Renamed the generic `BSize<T>` wrapper to `ByteSize<T>`. `BSize` is now an alias for `ByteSize<usize>`.
* 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.

### New features

* Added `nightly` feature for using `BSize` with nightly-only features like `const_ops` and `const_trait_impl`.
* Added `ByteSize::to_f64` for converting supported byte size underlying types to approximate `f64` values.
* Added `nightly` feature for using `ByteSize` with nightly-only features like `const_ops` and `const_trait_impl`.
* 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`.

## v0.2.1 (2026-06-27)
Expand Down
27 changes: 13 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ This crate provides multiple semantic wrappers and utilities for byte size repre
## Features

* `#![no_std]`-capable, no heap allocation, and no runtime dependencies by default.
* `BSize` wrappers over `u8`, `u16`, `u32`, `u64`, and `usize` for representing byte sizes with different underlying types.
* `FromStr` impl for `BSize`, allowing for parsing string size representations like "1.5 KiB" and "521 TB".
* `Display` impl for `BSize`, allowing for formatting byte sizes as human-readable strings in both binary (e.g., "1.5 MiB") and decimal (e.g., "1.5 MB") styles.
* `ByteSize<T>` wrappers over supported unsigned integer base types, with `BSize` as the `usize` alias and `BSize8`, `BSize16`, `BSize32`, and `BSize64` aliases for fixed-width base types.
* `FromStr` impl for `ByteSize`, allowing for parsing string size representations like "1.5 KiB" and "521 TB".
* `Display` impl for `ByteSize`, allowing for formatting byte sizes as human-readable strings in both binary (e.g., "1.5 MiB") and decimal (e.g., "1.5 MB") styles.
* Optional `serde` support for binary and human-readable format.
* Optional `nightly` support for generic const unit constructors, allowing calls like `BSize::kib(16_u64)`.
* Optional `nightly` support for generic const unit constructors, allowing calls like `ByteSize::kib(16_u64)`.

## Documentation

Expand Down Expand Up @@ -59,10 +59,10 @@ const RESULT_SIZE_LIMIT: usize = 8 * 1024 * 1024 * 1024; // 8 GiB
I want them to be:

```rust
const BASE_BLOB_INDEX_SIZE: BSize<usize> = BSize::kib(4);
const BASE_BLOCK_SIZE: BSize<usize> = BSize::mib(16);
const RESERVED_MEMORY: BSize<usize> = BSize::mib(256);
const RESULT_SIZE_LIMIT: BSize<usize> = BSize::gib(8);
const BASE_BLOB_INDEX_SIZE: BSize = BSize::kib(4);
const BASE_BLOCK_SIZE: BSize = BSize::mib(16);
const RESERVED_MEMORY: BSize = BSize::mib(256);
const RESULT_SIZE_LIMIT: BSize = BSize::gib(8);
```

So you don't have to multiply the numbers by hand and rely on comments to indicate the units. This also makes it easier to change the units later if needed.
Expand All @@ -85,16 +85,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 `BSize` and numeric types, this crate implements `BSize::map` for producing a new `BSize`, and `BSize::with` for producing an arbitrary result from the underlying byte count. This avoids implementing arithmetic traits for calculations between `BSize` and numeric types. The latter would cause confusions like what result type should be used for `ByteSize + u64`. However, `BSize` implements arithmetic traits for calculations between `BSize` and `BSize`, 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 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.

```rust
let result = ByteSize::kib(4) + 64; // Is the result type ByteSize or u64? Why?
let result = BSize::<u64>::kib(4).map(|b| b + 64); // Clearly the result type is BSize.
let result = BSize::<u64>::kib(4).0 + 64; // Clearly the result type is u64.
let result = BSize::<u64>::kib(4).with(|b| b + 64); // when .0 is cumbersome sometimes, this is more convenient.
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.
```

There is no `Unit` as well. To obtain a constant for a specific unit, you can use `BSize::<u64>::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).0` and this can be resolved at compile time.

Finally, the following issues in `bytesize` have been resolved in this crate:

Expand Down
31 changes: 11 additions & 20 deletions bsize/src/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,18 @@
use core::fmt;
use core::fmt::Write as _;

use crate::BSize;
use crate::BaseByteSize;
use crate::ByteSize;

/// Create a [`Display`] instance for displaying a byte size.
///
/// See [`Display`] for examples. Use [`Display::new`] when the byte count is already represented
/// as an `f64`.
pub fn display(size: impl ByteSize) -> Display {
pub fn display(size: impl BaseByteSize) -> Display {
Display::new(size.to_f64())
}

impl<T: ByteSize> BSize<T> {
impl<T: BaseByteSize> ByteSize<T> {
/// Returns a [`Display`] wrapper.
///
/// See [`Display`] for examples.
Expand All @@ -37,30 +37,24 @@ impl<T: ByteSize> BSize<T> {

/// Display wrapper for formatting byte sizes as human-readable strings.
///
/// You may create this wrapper with [`Display::new`], [`display`], or [`BSize::display`], then
/// You may create this wrapper with [`Display::new`], [`display`], or [`ByteSize::display`], then
/// pass custom [`DisplayOptions`] with [`Display::options`].
///
/// # Examples
///
/// Display with the [`DisplayOptions::BINARY`] and [`DisplayOptions::DECIMAL`] presets.
///
/// ```
/// use bsize::BSize;
/// use bsize::BSize64;
///
/// assert_eq!(
/// "41.0 KiB",
/// BSize::<u64>::kb(42).display().to_string(), // default to binary
/// BSize64::kb(42).display().to_string(), // default to binary
/// );
///
/// assert_eq!(
/// "1.0 MiB",
/// BSize::<u64>::mib(1).display().binary().to_string(),
/// );
/// assert_eq!("1.0 MiB", BSize64::mib(1).display().binary().to_string(),);
///
/// assert_eq!(
/// "42.0 kB",
/// BSize::<u64>::kb(42).display().decimal().to_string(),
/// );
/// assert_eq!("42.0 kB", BSize64::kb(42).display().decimal().to_string(),);
/// ```
///
/// The free [`display`] function accepts any supported integer byte size.
Expand All @@ -81,12 +75,9 @@ impl<T: ByteSize> BSize<T> {
/// Use standard formatter precision to control the number of fractional digits.
///
/// ```
/// use bsize::BSize;
/// use bsize::BSize64;
///
/// assert_eq!(
/// "1.54 KiB",
/// format!("{:.2}", BSize::<u64>::b(1575).display())
/// );
/// assert_eq!("1.54 KiB", format!("{:.2}", BSize64::b(1575).display()));
/// assert_eq!("1.575 KiB", format!("{:.3}", bsize::display(1613u64)));
/// ```
///
Expand Down Expand Up @@ -324,7 +315,7 @@ impl Display {
/// Create a [`Display`] instance from a byte count.
///
/// This constructor is useful when the byte count is already represented as an `f64`. For
/// supported integer byte counts, use [`display`] or [`BSize::display`].
/// supported integer byte counts, use [`display`] or [`ByteSize::display`].
///
/// # Examples
///
Expand Down
59 changes: 31 additions & 28 deletions bsize/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,16 @@
//! # Features
//!
//! * `#![no_std]`-capable, no heap allocation, and no runtime dependencies by default.
//! * [`BSize`] wrappers over `u8`, `u16`, `u32`, `u64`, and `usize` for representing byte sizes
//! with different underlying types.
//! * `FromStr` impl for `BSize`, allowing for parsing string size representations like "1.5 KiB"
//! * Generic [`ByteSize`] wrappers over supported unsigned integer base types, with [`BSize`] as
//! the `usize` alias and [`BSize8`], [`BSize16`], [`BSize32`], and [`BSize64`] as shorter aliases
//! for fixed-width base types.
//! * `FromStr` impl for `ByteSize`, allowing for parsing string size representations like "1.5 KiB"
//! and "521 TB".
//! * [`Display`] impl for `BSize`, allowing for formatting byte sizes as human-readable strings in
//! both binary (e.g., "1.5 MiB") and decimal (e.g., "1.5 MB") styles.
//! * [`Display`] impl for `ByteSize`, allowing for formatting byte sizes as human-readable strings
//! in both binary (e.g., "1.5 MiB") and decimal (e.g., "1.5 MB") styles.
//! * Optional `serde` support for binary and human-readable format.
//! * Optional `nightly` support for generic const unit constructors, allowing calls like
//! `BSize::kib(16_u64)`.
//! `ByteSize::kib(16_u64)`.
//!
//! # Examples
//!
Expand All @@ -38,17 +39,20 @@
//! ```
//! use bsize::BSize;
//!
//! assert!(BSize::<usize>::kib(4) > BSize::<usize>::kb(4));
//! assert!(BSize::kib(4) > BSize::kb(4));
//!
//! let size: BSize = BSize::b(4_096);
//! assert_eq!(size.0, 4_096);
//! ```
//!
//! Parse byte sizes from strings.
//!
//! ```
//! use bsize::BSize;
//! use bsize::BSize64;
//!
//! let size: BSize<u64> = "1.5 MiB".parse().unwrap();
//! let size: BSize64 = "1.5 MiB".parse().unwrap();
//!
//! assert_eq!(BSize::<u64>::mib(1).map(|bytes| bytes + 512 * 1024), size);
//! assert_eq!(BSize64::mib(1).map(|bytes| bytes + 512 * 1024), size);
//! ```
//!
//! Display as human-readable string.
Expand All @@ -59,15 +63,9 @@
//! use bsize::DisplayOptions;
//! use bsize::DisplayScale;
//!
//! assert_eq!(
//! "518.0 GiB",
//! BSize::<usize>::gib(518).display().binary().to_string()
//! );
//! assert_eq!("518.0 GiB", BSize::gib(518).display().binary().to_string());
//!
//! assert_eq!(
//! "556.2 GB",
//! BSize::<usize>::gib(518).display().decimal().to_string()
//! );
//! assert_eq!("556.2 GB", BSize::gib(518).display().decimal().to_string());
//!
//! let network_units = DisplayOptions::DECIMAL
//! .base_unit(DisplayBaseUnit::Bit)
Expand All @@ -81,19 +79,19 @@
//! ```
//! use bsize::BSize;
//!
//! let plus = BSize::<usize>::mb(1) + BSize::<usize>::kb(100);
//! let plus = BSize::mb(1) + BSize::kb(100);
//! println!("{plus}");
//!
//! let minus = BSize::<usize>::tb(1) - BSize::<usize>::gb(4);
//! assert_eq!(BSize::<usize>::gb(996), minus);
//! let minus = BSize::tb(1) - BSize::gb(4);
//! assert_eq!(BSize::gb(996), minus);
//! ```
//!
//! Arithmetic operations over the underlying types are supported.
//!
//!```
//! use bsize::BSize;
//!
//! let size = BSize::<usize>::mb(1);
//! let size = BSize::mb(1);
//! let size = size.map(|b| b * 4); // 4x scale
//! println!("{size}");
//! ```
Expand All @@ -118,14 +116,19 @@ pub use self::display::DisplayScale;
pub use self::display::DisplayUnitSystem;
pub use self::display::display;
pub use self::parse::ParseError;
pub use self::traits::ByteSize;
pub use self::traits::BaseByteSize;
pub use self::traits::ExaByteSize;
pub use self::traits::GigaByteSize;
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;

#[cfg(test)]
fn assert_close(actual: f64, expected: f64) {
Expand All @@ -145,24 +148,24 @@ mod property_tests {

use super::*;

impl quickcheck::Arbitrary for BSize<u64> {
impl quickcheck::Arbitrary for ByteSize<u64> {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
Self(u64::arbitrary(g))
}
}

quickcheck::quickcheck! {
fn parsing_never_panics(size: String) -> bool {
let _ = size.parse::<BSize<u64>>();
let _ = size.parse::<ByteSize<u64>>();
true
}

fn to_string_never_blank(size: BSize<u64>) -> bool {
fn to_string_never_blank(size: ByteSize<u64>) -> bool {
!size.to_string().is_empty()
}

fn string_round_trip(size: BSize<u64>) -> bool {
size.to_string().parse::<BSize<u64>>().unwrap() == size
fn string_round_trip(size: ByteSize<u64>) -> bool {
size.to_string().parse::<ByteSize<u64>>().unwrap() == size
}
}
}
32 changes: 18 additions & 14 deletions bsize/src/ops/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,36 +20,40 @@ mod stable;
#[cfg(test)]
mod tests {
use crate::BSize;
use crate::BSize8;
use crate::BSize16;
use crate::BSize32;
use crate::BSize64;

#[test]
fn adds_byte_sizes() {
assert_eq!((BSize::<u8>(3) + BSize(5)).0, 8);
assert_eq!((BSize::<u16>(3) + BSize(5)).0, 8);
assert_eq!((BSize::<u32>(3) + BSize(5)).0, 8);
assert_eq!((BSize::<u64>(3) + BSize(5)).0, 8);
assert_eq!((BSize::<usize>(3) + BSize(5)).0, 8);
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);
}

#[test]
fn add_assigns_byte_sizes() {
let mut size = BSize::<usize>(3);
size += BSize(5);
let mut size = BSize::b(3);
size += BSize::b(5);
assert_eq!(size.0, 8);
}

#[test]
fn subtracts_byte_sizes() {
assert_eq!((BSize::<u8>(8) - BSize(5)).0, 3);
assert_eq!((BSize::<u16>(8) - BSize(5)).0, 3);
assert_eq!((BSize::<u32>(8) - BSize(5)).0, 3);
assert_eq!((BSize::<u64>(8) - BSize(5)).0, 3);
assert_eq!((BSize::<usize>(8) - BSize(5)).0, 3);
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);
}

#[test]
fn sub_assigns_byte_sizes() {
let mut size = BSize::<usize>(8);
size -= BSize(5);
let mut size = BSize::b(8);
size -= BSize::b(5);
assert_eq!(size.0, 3);
}
}
Loading