diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index fce2a78f..7d455ebb 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -33,7 +33,7 @@ jobs: - name: Upload coverage to Codecov uses: codecov/codecov-action@v4.5.0 with: - files: codecov.json fail_ci_if_error: true - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + files: codecov.json + slug: bytesize-rs/bytesize + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index f1731b49..83fe80ed 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -Cargo.lock target .idea .vscode diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ddd15862..00000000 --- a/.travis.yml +++ /dev/null @@ -1,7 +0,0 @@ -language: rust -before_script: - - rustup component add rustfmt-preview -script: - - cargo fmt --all -- --check - - cargo build --verbose --all - - cargo test --verbose --all diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cc2adba..b1378a30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog ## Unreleased + - Use SI format by default with `Display`. - Use "KiB" for SI unit. +- Implement `Sub` for `ByteSize`. +- Implement `Sub>` for `ByteSize`. +- Implement `SubAssign` for `ByteSize`. +- Implement `SubAssign>` for `ByteSize`. +- Reject parsing non-unit characters after whitespace. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..5c3c438f --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,192 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "bytesize" +version = "1.3.0" +dependencies = [ + "arbitrary", + "serde", + "serde_json", + "toml", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "indexmap" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "proc-macro2" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" + +[[package]] +name = "serde" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.138" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "syn" +version = "2.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "toml" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "unicode-ident" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" + +[[package]] +name = "winnow" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86e376c75f4f43f44db463cf729e0d3acbf954d13e22c51e26e4c264b4ab545f" +dependencies = [ + "memchr", +] diff --git a/Cargo.toml b/Cargo.toml index d6c45fbd..fc54e5ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,11 +5,16 @@ version = "1.3.0" authors = ["Hyunsik Choi ", "MrCroxx "] keywords = ["byte", "byte-size", "utility", "human-readable", "format"] categories = ["development-tools", "filesystem"] -repository = "https://github.com/hyunsik/bytesize" +repository = "https://github.com/bytesize-rs/bytesize" license = "Apache-2.0" edition = "2021" rust-version = "1.65" +[features] +default = [] +arbitrary = ["dep:arbitrary"] +serde = ["dep:serde"] + [dependencies] arbitrary = { version = "1", features = ["derive"], optional = true } serde = { version = "1", optional = true } @@ -18,8 +23,3 @@ serde = { version = "1", optional = true } serde = { version = "1", features = ["derive"] } serde_json = "1" toml = "0.8" - -[features] -arbitrary = ["dep:arbitrary"] -default = [] -serde = ["dep:serde"] diff --git a/README.md b/README.md index 8199bc9a..2f457de4 100644 --- a/README.md +++ b/README.md @@ -1,101 +1,53 @@ ## ByteSize -[![CI](https://github.com/hyunsik/bytesize/actions/workflows/ci.yml/badge.svg)](https://github.com/hyunsik/bytesize/actions/workflows/ci.yml) + + +[![CI](https://github.com/bytesize-rs/bytesize/actions/workflows/ci.yml/badge.svg)](https://github.com/bytesize-rs/bytesize/actions/workflows/ci.yml) [![Crates.io Version](https://img.shields.io/crates/v/bytesize.svg)](https://crates.io/crates/bytesize) -`ByteSize` is a utility for human-readable byte count representations. + + + + +`ByteSize` is a semantic wrapper for byte count representations. Features: - Pre-defined constants for various size units (e.g., B, Kb, Kib, Mb, Mib, Gb, Gib, ... PB). - `ByteSize` type which presents size units convertible to different size units. - Arithmetic operations for `ByteSize`. -- FromStr impl for `ByteSize`, allowing to parse from string size representations like 1.5KiB and 521TiB. +- `FromStr` impl for `ByteSize`, allowing for parsing string size representations like "1.5KiB" and "521TiB". - Serde support for binary and human-readable deserializers like JSON. -[API Documentation](https://docs.rs/bytesize) +### Examples -## Example - -### Human readable representations (SI unit and Binary unit) +Construction using SI or IEC helpers. ```rust -fn assert_display(expected: &str, b: ByteSize) { - assert_eq!(expected, format!("{}", b)); -} - -#[test] -fn test_display() { - assert_display("215 B", ByteSize::b(215)); - assert_display("1.0 KiB", ByteSize::kib(1)); - assert_display("301.0 KiB", ByteSize::kib(301)); - assert_display("419.0 MiB", ByteSize::mib(419)); - assert_display("518.0 GiB", ByteSize::gib(518)); - assert_display("815.0 TiB", ByteSize::tib(815)); - assert_display("609.0 PiB", ByteSize::pib(609)); -} - -#[test] -fn test_display_alignment() { - assert_eq!("|357 B |", format!("|{:10}|", ByteSize(357))); - assert_eq!("| 357 B|", format!("|{:>10}|", ByteSize(357))); - assert_eq!("|357 B |", format!("|{:<10}|", ByteSize(357))); - assert_eq!("| 357 B |", format!("|{:^10}|", ByteSize(357))); - - assert_eq!("|-----357 B|", format!("|{:->10}|", ByteSize(357))); - assert_eq!("|357 B-----|", format!("|{:-<10}|", ByteSize(357))); - assert_eq!("|--357 B---|", format!("|{:-^10}|", ByteSize(357))); -} - -fn assert_to_string(expected: &str, b: ByteSize, si: bool) { - assert_eq!(expected.to_string(), b.to_string_as(si)); -} - -#[test] -fn test_to_string_as() { - assert_to_string("215 B", ByteSize::b(215), true); - assert_to_string("215 B", ByteSize::b(215), false); - - assert_to_string("1.0 KiB", ByteSize::kib(1), true); - assert_to_string("1.0 KB", ByteSize::kib(1), false); - - assert_to_string("293.9 KiB", ByteSize::kb(301), true); - assert_to_string("301.0 KB", ByteSize::kb(301), false); - - assert_to_string("1.0 MiB", ByteSize::mib(1), true); - assert_to_string("1048.6 KB", ByteSize::mib(1), false); - - // a bug case: https://github.com/flang-project/bytesize/issues/8 - assert_to_string("1.9 GiB", ByteSize::mib(1907), true); - assert_to_string("2.0 GB", ByteSize::mib(1908), false); - - assert_to_string("399.6 MiB", ByteSize::mb(419), true); - assert_to_string("419.0 MB", ByteSize::mb(419), false); - - assert_to_string("482.4 GiB", ByteSize::gb(518), true); - assert_to_string("518.0 GB", ByteSize::gb(518), false); - - assert_to_string("741.2 TiB", ByteSize::tb(815), true); - assert_to_string("815.0 TB", ByteSize::tb(815), false); - - assert_to_string("540.9 PiB", ByteSize::pb(609), true); - assert_to_string("609.0 PB", ByteSize::pb(609), false); -} +use bytesize::ByteSize; + +assert!(ByteSize::kib(4) > ByteSize::kb(4)); ``` -### Arithmetic operations +Display as human-readable string. ```rust use bytesize::ByteSize; -fn byte_arithmetic_operator() { - let x = ByteSize::mb(1); - let y = ByteSize::kb(100); +assert_eq!("482.4 GiB", ByteSize::gb(518).to_string_as(true)); +assert_eq!("518.0 GB", ByteSize::gb(518).to_string_as(false)); +``` + +Arithmetic operations are supported. + +```rust +use bytesize::ByteSize; - let plus = x + y; - print!("{}", plus); +let plus = ByteSize::mb(1) + ByteSize::kb(100); +println!("{plus}"); - let minus = ByteSize::tb(100) + ByteSize::gb(4); - print!("{}", minus); -} +let minus = ByteSize::tb(1) - ByteSize::gb(4); +assert_eq!(ByteSize::gb(996), minus); ``` + + diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..87f4f27e --- /dev/null +++ b/codecov.yml @@ -0,0 +1,16 @@ +comment: false + +coverage: + status: + project: + default: + threshold: 100% # make CI green + patch: + default: + threshold: 100% # make CI green + +# ignore code coverage on following paths +ignore: + - "**/tests" + - "**/benches" + - "**/examples" diff --git a/src/lib.rs b/src/lib.rs index 5d04d385..b098abeb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,23 +23,16 @@ //! ``` //! use bytesize::ByteSize; //! -//! assert_eq!("482.4 GiB", ByteSize::gb(518).to_string_as(true)); -//! assert_eq!("518.0 GB", ByteSize::gb(518).to_string_as(false)); +//! assert_eq!("482.4 GiB", ByteSize::gb(518).to_string_as(false)); +//! assert_eq!("518.0 GB", ByteSize::gb(518).to_string_as(true)); //! ``` mod parse; - -#[cfg(feature = "arbitrary")] -extern crate arbitrary; -#[cfg(feature = "serde")] -extern crate serde; #[cfg(feature = "serde")] -use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; -#[cfg(feature = "serde")] -use std::convert::TryFrom; +mod serde; use std::fmt::{self, Debug, Display, Formatter}; -use std::ops::{Add, AddAssign, Mul, MulAssign}; +use std::ops::{Add, AddAssign, Mul, MulAssign, Sub, SubAssign}; /// byte size for 1 byte pub const B: u64 = 1; @@ -66,9 +59,9 @@ pub const TIB: u64 = 1_099_511_627_776; pub const PIB: u64 = 1_125_899_906_842_624; static UNITS: &str = "KMGTPE"; -static UNITS_SI: &str = "KMGTPE"; -static LN_KB: f64 = 6.931471806; // ln 1024 -static LN_KIB: f64 = 6.907755279; // ln 1000 +static UNITS_SI: &str = "kMGTPE"; +static LN_KIB: f64 = 6.931471805599453; // ln 1024 +static LN_KB: f64 = 6.907755278982137; // ln 1000 pub fn kb>(size: V) -> u64 { size.into() * KB @@ -183,14 +176,14 @@ impl ByteSize { } pub fn to_string(bytes: u64, si_prefix: bool) -> String { - let unit = if si_prefix { KIB } else { KB }; - let unit_base = if si_prefix { LN_KIB } else { LN_KB }; + let unit = if si_prefix { KB } else { KIB }; + let unit_base = if si_prefix { LN_KB } else { LN_KIB }; let unit_prefix = if si_prefix { UNITS_SI.as_bytes() } else { UNITS.as_bytes() }; - let unit_suffix = if si_prefix { "iB" } else { "B" }; + let unit_suffix = if si_prefix { "B" } else { "iB" }; if bytes < unit { format!("{} B", bytes) @@ -212,7 +205,7 @@ pub fn to_string(bytes: u64, si_prefix: bool) -> String { impl Display for ByteSize { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad(&to_string(self.0, true)) + f.pad(&to_string(self.0, false)) } } @@ -284,88 +277,61 @@ where } } -impl Mul for ByteSize +impl Sub for ByteSize { + type Output = ByteSize; + + #[inline(always)] + fn sub(self, rhs: ByteSize) -> ByteSize { + ByteSize(self.0 - rhs.0) + } +} + +impl SubAssign for ByteSize { + #[inline(always)] + fn sub_assign(&mut self, rhs: ByteSize) { + self.0 -= rhs.0 + } +} + +impl Sub for ByteSize where T: Into, { type Output = ByteSize; #[inline(always)] - fn mul(self, rhs: T) -> ByteSize { - ByteSize(self.0 * rhs.into()) + fn sub(self, rhs: T) -> ByteSize { + ByteSize(self.0 - (rhs.into())) } } -impl MulAssign for ByteSize +impl SubAssign for ByteSize where T: Into, { #[inline(always)] - fn mul_assign(&mut self, rhs: T) { - self.0 *= rhs.into(); + fn sub_assign(&mut self, rhs: T) { + self.0 -= rhs.into(); } } -#[cfg(feature = "serde")] -impl<'de> Deserialize<'de> for ByteSize { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct ByteSizeVistor; - - impl<'de> de::Visitor<'de> for ByteSizeVistor { - type Value = ByteSize; - - fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str("an integer or string") - } - - fn visit_i64(self, value: i64) -> Result { - if let Ok(val) = u64::try_from(value) { - Ok(ByteSize(val)) - } else { - Err(E::invalid_value( - de::Unexpected::Signed(value), - &"integer overflow", - )) - } - } - - fn visit_u64(self, value: u64) -> Result { - Ok(ByteSize(value)) - } - - fn visit_str(self, value: &str) -> Result { - if let Ok(val) = value.parse() { - Ok(val) - } else { - Err(E::invalid_value( - de::Unexpected::Str(value), - &"parsable string", - )) - } - } - } - - if deserializer.is_human_readable() { - deserializer.deserialize_any(ByteSizeVistor) - } else { - deserializer.deserialize_u64(ByteSizeVistor) - } +impl Mul for ByteSize +where + T: Into, +{ + type Output = ByteSize; + #[inline(always)] + fn mul(self, rhs: T) -> ByteSize { + ByteSize(self.0 * rhs.into()) } } -#[cfg(feature = "serde")] -impl Serialize for ByteSize { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - if serializer.is_human_readable() { - ::serialize(self.to_string().as_str(), serializer) - } else { - self.0.serialize(serializer) - } +impl MulAssign for ByteSize +where + T: Into, +{ + #[inline(always)] + fn mul_assign(&mut self, rhs: T) { + self.0 *= rhs.into(); } } @@ -380,6 +346,8 @@ mod tests { assert_eq!((x + y).as_u64(), 1_100_000u64); + assert_eq!((x - y).as_u64(), 900_000u64); + assert_eq!((x + (100 * 1000) as u64).as_u64(), 1_100_000); assert_eq!((x * 2u64).as_u64(), 2_000_000); @@ -403,6 +371,14 @@ mod tests { assert_eq!((x + B as u8).as_u64(), 1_000_001); + assert_eq!((x - MB as u64).as_u64(), 0); + + assert_eq!((x - MB as u32).as_u64(), 0); + + assert_eq!((x - KB as u32).as_u64(), 999_000); + + assert_eq!((x - B as u32).as_u64(), 999_999); + x += MB as u64; x += MB as u32; x += 10u16; @@ -452,33 +428,32 @@ mod tests { #[test] fn test_to_string_as() { - assert_to_string("215 B", ByteSize::b(215), true); assert_to_string("215 B", ByteSize::b(215), false); + assert_to_string("215 B", ByteSize::b(215), true); - assert_to_string("1.0 KiB", ByteSize::kib(1), true); - assert_to_string("1.0 KB", ByteSize::kib(1), false); + assert_to_string("1.0 KiB", ByteSize::kib(1), false); + assert_to_string("1.0 kB", ByteSize::kib(1), true); - assert_to_string("293.9 KiB", ByteSize::kb(301), true); - assert_to_string("301.0 KB", ByteSize::kb(301), false); + assert_to_string("293.9 KiB", ByteSize::kb(301), false); + assert_to_string("301.0 kB", ByteSize::kb(301), true); - assert_to_string("1.0 MiB", ByteSize::mib(1), true); - assert_to_string("1048.6 KB", ByteSize::mib(1), false); + assert_to_string("1.0 MiB", ByteSize::mib(1), false); + assert_to_string("1.0 MB", ByteSize::mib(1), true); - // a bug case: https://github.com/flang-project/bytesize/issues/8 - assert_to_string("1.9 GiB", ByteSize::mib(1907), true); - assert_to_string("2.0 GB", ByteSize::mib(1908), false); + assert_to_string("1.9 GiB", ByteSize::mib(1907), false); + assert_to_string("2.0 GB", ByteSize::mib(1907), true); - assert_to_string("399.6 MiB", ByteSize::mb(419), true); - assert_to_string("419.0 MB", ByteSize::mb(419), false); + assert_to_string("399.6 MiB", ByteSize::mb(419), false); + assert_to_string("419.0 MB", ByteSize::mb(419), true); - assert_to_string("482.4 GiB", ByteSize::gb(518), true); - assert_to_string("518.0 GB", ByteSize::gb(518), false); + assert_to_string("482.4 GiB", ByteSize::gb(518), false); + assert_to_string("518.0 GB", ByteSize::gb(518), true); - assert_to_string("741.2 TiB", ByteSize::tb(815), true); - assert_to_string("815.0 TB", ByteSize::tb(815), false); + assert_to_string("741.2 TiB", ByteSize::tb(815), false); + assert_to_string("815.0 TB", ByteSize::tb(815), true); - assert_to_string("540.9 PiB", ByteSize::pb(609), true); - assert_to_string("609.0 PB", ByteSize::pb(609), false); + assert_to_string("540.9 PiB", ByteSize::pb(609), false); + assert_to_string("609.0 PB", ByteSize::pb(609), true); } #[test] @@ -488,28 +463,6 @@ mod tests { #[test] fn test_to_string() { - assert_to_string("609.0 PB", ByteSize::pb(609), false); - } - - #[cfg(feature = "serde")] - #[test] - fn test_serde() { - #[derive(Serialize, Deserialize)] - struct S { - x: ByteSize, - } - - let s: S = serde_json::from_str(r#"{ "x": "5 B" }"#).unwrap(); - assert_eq!(s.x, ByteSize(5)); - - let s: S = serde_json::from_str(r#"{ "x": 1048576 }"#).unwrap(); - assert_eq!(s.x, "1 MiB".parse::().unwrap()); - - let s: S = toml::from_str(r#"x = "2.5 MiB""#).unwrap(); - assert_eq!(s.x, "2.5 MiB".parse::().unwrap()); - - // i64 MAX - let s: S = toml::from_str(r#"x = "9223372036854775807""#).unwrap(); - assert_eq!(s.x, "9223372036854775807".parse::().unwrap()); + assert_to_string("609.0 PB", ByteSize::pb(609), true); } } diff --git a/src/parse.rs b/src/parse.rs index a5cca5b3..93afdb23 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -10,9 +10,7 @@ impl std::str::FromStr for ByteSize { let number = take_while(value, |c| c.is_ascii_digit() || c == '.'); match number.parse::() { Ok(v) => { - let suffix = skip_while(value, |c| { - c.is_whitespace() || c.is_ascii_digit() || c == '.' - }); + let suffix = skip_while(&value[number.len()..], char::is_whitespace); match suffix.parse::() { Ok(u) => Ok(Self((v * u) as u64)), Err(error) => Err(format!( @@ -192,7 +190,6 @@ mod tests { s.parse::().unwrap().0 } - assert_eq!("0".parse::().unwrap().0, 0); assert_eq!(parse("0"), 0); assert_eq!(parse("500"), 500); assert_eq!(parse("1K"), Unit::KiloByte * 1); @@ -220,6 +217,11 @@ mod tests { assert!(parse("").is_err()); assert!(parse("a124GB").is_err()); + assert!(parse("1.3 42.0 B").is_err()); + assert!(parse("1.3 ... B").is_err()); + // The original implementation did not account for the possibility that users may + // use whitespace to visually separate digits, thus treat it as an error + assert!(parse("1 000 B").is_err()); } #[test] @@ -229,9 +231,17 @@ mod tests { s.parse::().unwrap().0 } - assert_eq!(parse(&format!("{}", parse("128GB"))), 128 * Unit::GigaByte); + let expected = { + let init_value = 128 * Unit::GigaByte; + // Display for ByteSize is printing to IEC units with + // at least 1 digit after period. + let iec_float = init_value as f64 / Unit::GibiByte.factor() as f64; + let iec_float_with_1_digit = (iec_float * 10.0).round() / 10.0; + (iec_float_with_1_digit * Unit::GibiByte) as u64 // bytes + }; + assert_eq!(parse(&format!("{}", ByteSize(parse("128GB")))), expected); assert_eq!( - parse(&crate::to_string(parse("128.000 GiB"), true)), + parse(&crate::to_string(parse("128.000 GiB"), false)), 128 * Unit::GibiByte ); } diff --git a/src/serde.rs b/src/serde.rs new file mode 100644 index 00000000..5cda0d38 --- /dev/null +++ b/src/serde.rs @@ -0,0 +1,103 @@ +use std::fmt; + +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; + +use crate::ByteSize; + +impl<'de> Deserialize<'de> for ByteSize { + fn deserialize(de: D) -> Result + where + D: Deserializer<'de>, + { + struct ByteSizeVisitor; + + impl de::Visitor<'_> for ByteSizeVisitor { + type Value = ByteSize; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("an integer or string") + } + + fn visit_i64(self, value: i64) -> Result { + if let Ok(val) = u64::try_from(value) { + Ok(ByteSize(val)) + } else { + Err(E::invalid_value( + de::Unexpected::Signed(value), + &"integer overflow", + )) + } + } + + fn visit_u64(self, value: u64) -> Result { + Ok(ByteSize(value)) + } + + fn visit_str(self, value: &str) -> Result { + if let Ok(val) = value.parse() { + Ok(val) + } else { + Err(E::invalid_value( + de::Unexpected::Str(value), + &"parsable string", + )) + } + } + } + + if de.is_human_readable() { + de.deserialize_any(ByteSizeVisitor) + } else { + de.deserialize_u64(ByteSizeVisitor) + } + } +} + +impl Serialize for ByteSize { + fn serialize(&self, ser: S) -> Result + where + S: Serializer, + { + if ser.is_human_readable() { + ::serialize(self.to_string().as_str(), ser) + } else { + self.0.serialize(ser) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_serde() { + #[derive(Serialize, Deserialize)] + struct S { + x: ByteSize, + } + + let s = serde_json::from_str::(r#"{ "x": "5 B" }"#).unwrap(); + assert_eq!(s.x, ByteSize(5)); + + let s = serde_json::from_str::(r#"{ "x": 1048576 }"#).unwrap(); + assert_eq!(s.x, "1 MiB".parse::().unwrap()); + + let s = toml::from_str::(r#"x = "2.5 MiB""#).unwrap(); + assert_eq!(s.x, "2.5 MiB".parse::().unwrap()); + + // i64 MAX + let s = toml::from_str::(r#"x = "9223372036854775807""#).unwrap(); + assert_eq!(s.x, "9223372036854775807".parse::().unwrap()); + } + + #[test] + + fn test_serde_json() { + let json = serde_json::to_string(&ByteSize::mib(1)).unwrap(); + assert_eq!(json, "\"1.0 MiB\""); + + let deserialized = serde_json::from_str::(&json).unwrap(); + assert_eq!(deserialized.0, 1048576); + } +}