├── .github └── workflows │ └── rust.yml ├── .gitignore ├── .rustfmt.toml ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── examples ├── blog.rs ├── readme.rs └── tweet.rs └── src ├── fmt.rs ├── from_str.rs ├── lib.rs ├── ops.rs ├── serde.rs ├── tests.rs └── tests_nostd.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | strategy: 15 | matrix: 16 | # This crate is high-level enough that it probably doesn't matter what OS we test under 17 | # os: [windows-latest, ubuntu-latest, macos-latest] 18 | os: [ubuntu-latest] 19 | runs-on: ${{ matrix.os }} 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Build (no features) 24 | run: cargo build --verbose --no-default-features 25 | - name: Build (default features) 26 | run: cargo build --verbose 27 | - name: Build (all features) 28 | run: cargo build --verbose --all-features 29 | - name: Run tests (no features) 30 | run: cargo test --verbose --no-default-features 31 | - name: Run tests (default features) 32 | run: cargo test --verbose 33 | - name: Run tests (all features) 34 | run: cargo test --verbose --all-features 35 | # Intentionally not building docs with `--no-default-features` because some links are broken. 36 | - name: Build documentation 37 | run: cargo doc --verbose --all-features 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2018" 2 | # wrap_comments is nice, but introduces trailing whitespace errors! 3 | wrap_comments = false 4 | # comment_width defaults to 80, despite a default line_width of 100 5 | comment_width = 100 6 | # use_small_heuristics defaults these to 60% of max_width 7 | fn_call_width=80 8 | attr_fn_like_width=80 9 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "size" 3 | version = "0.5.0" 4 | authors = ["Mahmoud Al-Qudsi ", "NeoSmart Technologies"] 5 | description = "A crate for expressing, formatting, and interacting with file sizes" 6 | homepage = "https://github.com/neosmart/prettysize-rs" 7 | repository = "https://github.com/neosmart/prettysize-rs" 8 | readme = "README.md" 9 | keywords = ["prettysize", "size", "file", "formatting"] 10 | categories = ["value-formatting", "rust-patterns", "no-std"] 11 | license = "MIT" 12 | edition = "2018" 13 | 14 | [dependencies] 15 | serde = { version = "1.0", default-features = false, optional = true, features = [ "derive"] } 16 | 17 | [features] 18 | default = [ "std" ] 19 | serde = [ "std", "dep:serde" ] 20 | std = [] 21 | 22 | [dev-dependencies] 23 | serde_json = "1.0.116" 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Developed and maintained by Mahmoud Al-Qudsi 4 | Copyright (c) 2018 NeoSmart Technologies 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # CargoMake by NeoSmart Technologies 2 | # Written and maintained by Mahmoud Al-Qudsi 3 | # Released under the MIT public license 4 | # Obtain updates from https://github.com/neosmart/CargoMake 5 | 6 | COLOR ?= always # Valid COLOR options: {always, auto, never} 7 | CARGO = cargo --color $(COLOR) 8 | 9 | .PHONY: all bench build check clean doc install publish run test update 10 | 11 | all: build 12 | 13 | bench: 14 | @$(CARGO) bench 15 | 16 | build: 17 | @$(CARGO) build 18 | @$(CARGO) build --no-default-features 19 | 20 | check: build test 21 | 22 | clean: 23 | @$(CARGO) clean 24 | 25 | doc: 26 | @$(CARGO) doc 27 | 28 | install: build 29 | @$(CARGO) install 30 | 31 | publish: 32 | @$(CARGO) publish 33 | 34 | run: build 35 | @$(CARGO) run 36 | 37 | test: build 38 | @$(CARGO) test 39 | @$(CARGO) test --no-default-features 40 | 41 | update: 42 | @$(CARGO) update 43 | 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PrettySize, rust edition 2 | 3 | [![crates.io](https://img.shields.io/crates/v/size.svg)](https://crates.io/crates/size) [![docs.rs](https://docs.rs/size/badge.svg)](https://docs.rs/size/latest/size/) 4 | 5 | A comprehensive file size crate for rust applications, meant to be light and effective. 6 | Includes utilities for human-readable formatting of file sizes as well as converting 7 | between different base-two and base-ten size units and performing both mathematical and 8 | logical operations on strongly-typed file sizes. 9 | 10 | [See the crate documentation](https://docs.rs/size/latest/size/) for a more complete summary of 11 | what this crate can do and how to use it. 12 | 13 | ## Features 14 | 15 | `PrettySize` provides 16 | 17 | * a `Size` type that can be used to hold a strongly-typed size 18 | (e.g. `let size = Size::from_gigabytes(4)`) and perform operations on it, 19 | * definitions for the base-two and base-ten file size units defined as `pub const` in the 20 | `size::consts` namespace, available both in abbreviated and unabridged forms (i.e. 21 | `consts::KiB` and `consts::KIBIBYTE` or `consts::GB` and `consts::GIGABYTE`), 22 | * an `std::Display` impl for `Size` to automatically display sizes in a human-readable 23 | format, automatically choosing the best size unit and numeric precision to 24 | give the nicest results (you can also use `Size::to_string()` instead). 25 | * a `Size.format()` method that gives you more control over how sizes are converted 26 | to a textual representation, letting you to specify the base of the human-readable 27 | units and their style (smart, abbreviated, or full; plus their lowercase variants). 28 | * mathematical and logical operations on strongly-typed `Size` values, 29 | * full support for expressing negative sizes (e.g. the difference between two sizes, or the 30 | amount of space reclaimed on a disk) 31 | * serialization to/from bare byte fields in network payloads or other api requests/responses 32 | * parsing sizes from text representation in a wide variety of formats 33 | 34 | This crate can also be used in `no_std` mode (by compiling with default features 35 | disabled). This disables string conversion/formatting/parsing but keeps all the strongly-typed 36 | size conversion and mathematical/logical operations available. 37 | 38 | This crate is free of any dependencies. 39 | 40 | ## Usage 41 | 42 | Cargo.toml: 43 | 44 | ```toml 45 | [dependencies] 46 | size = "0.5.0-preview2" 47 | ``` 48 | 49 | and in your code: 50 | 51 | ```rust 52 | use size::{Base, Size}; 53 | // You can use/import consts representing base2/base10 sizes individually 54 | // as (e.g.) size::KiB, or import all with `use size::consts::*` 55 | 56 | fn main() { 57 | // Create strongly-typed sizes: 58 | let byte_count = Size::from_kilobytes(42); 59 | assert_eq!(42_000, byte_count.bytes()); 60 | 61 | // Use predefined constants for the various units 62 | let byte_count = 42 * size::KiB; 63 | assert_eq!(43_008, byte_count); 64 | 65 | // `Size` can take any numeric type you throw at it 66 | let byte_count = Size::from_mib(0.040055); 67 | assert_eq!(byte_count.bytes(), 42_000); 68 | 69 | // And for those of you that haven't yet drunk the base-two Kool-Aid: 70 | let file_size = Size::from_kb(42); 71 | assert_eq!(file_size.bytes(), 42_000); 72 | 73 | println!("{}, I say!", file_size); 74 | // prints "41 KiB, I say!" 75 | 76 | // Override the default choice of base-2 units 77 | println!("{}, I meant!", file_size.format().with_base(Base::Base10)); 78 | // prints "42 KB, I meant!" 79 | 80 | // Add and subtract strongly-typed sizes, even with different underlying types 81 | let sum = Size::from_mb(1.0) + Size::from_kb(200); 82 | assert_eq!(sum.bytes(), 1_200_000); 83 | 84 | // Multiply and divide strongly-typed sizes by scalar values 85 | let new_size = Size::from_mib(2) * 2; 86 | assert_eq!(new_size, Size::from_mib(4)); 87 | 88 | // Compare sizes for equality or order 89 | let size1 = Size::from_gigabytes(2); 90 | let size2 = Size::from_gibibytes(1.99); 91 | assert!(size1 < size2); 92 | 93 | // Parse sizes from textual representations 94 | let size1 = Size::from_str("12 KiB").unwrap(); 95 | let size2 = Size::from_str("42mb").unwrap(); 96 | } 97 | ``` 98 | 99 | ## Parsing and formatting 100 | 101 | The `size` crate supports parsing textual representations of file sizes into strongly typed `Size` objects, both via the `Size::from_str()` function and its `FromStr` implementation that lets you call `"1234 kilobytes".parse()`. 102 | 103 | The `Size` type implements `std::fmt::Display` (in addition to many other traits), which provides a facility to generate properly formatted textual representations of file sizes via the `Size::to_string()` impl of the `ToString` trait or when used in a `format!(..., Size)` context. 104 | 105 | By default, `Size` objects are formatted as base-2 (KiB, MiB, etc) with heuristically chosen precision and units. The member function `Size::format()` can be used to override the unit base (e.g. MB vs MiB) and whether or not abbreviated unit names are used (e.g. KiB vs Kebibyte). 106 | 107 | Feel free to open a GitHub issue or PR if you need further control over formatting (precision, case, etc)! 108 | 109 | ## `no_std` usage 110 | 111 | Add the crate to `Cargo.toml` with `default-features` disabled for `no_std` support: 112 | 113 | ```toml 114 | [dependencies] 115 | size = { version = ..., default-features = false } 116 | ``` 117 | 118 | Building in `no_std` mode disables support for floating point `Size` operations/conversions as well as string formatting and conversion. 119 | 120 | ## `serde` support 121 | 122 | For serialization and deserialization support, add the `size` crate to your `Cargo.toml` with the `serde` feature enabled: 123 | 124 | ```toml 125 | [dependencies] 126 | size = { version = ..., features = [ "serde" ] } 127 | ``` 128 | 129 | **The `Size` type is serialized/deserialized transparently.** This means that it acts as if it were a `u64` field denoting the size in bytes. This was done to allow directly deserializing from network payloads from languages/apis that do not express sizes as strongly typed fields (and vice-versa). 130 | 131 | As a concrete example, let's pretend you have the following struct that contains a `Size` field: 132 | 133 | ```rust 134 | #[derive(Serialize, Deserialize)] 135 | struct File { 136 | path: PathBuf, 137 | size: Size, 138 | } 139 | ``` 140 | 141 | Using JSON as an example, the `File` type above will serialize to/from the following: 142 | 143 | ```json 144 | { 145 | "path:" "/foo/bar", 146 | "size:" 1024 147 | } 148 | ``` 149 | 150 | As you can see, the `size` field has been serialized directly to a numeric value (and not a `Size` object _containing_ that number value). 151 | 152 | ## Parsing sizes from strings 153 | 154 | The `FromStr` impl or the static `Size::from_str()` member function can be used to parse sizes from text, and supports a wide variety of input formats and representations: 155 | 156 | ```rust 157 | let size1 = Size::from_str("123456").unwrap(); 158 | let size2 = Size::from_str("17mib").unwrap(); 159 | let size3 = Size::from_str("12.8 KB").unwrap(); 160 | let size4 = Size::from_str("18.9 gigabytes").unwrap(); 161 | ``` 162 | 163 | ## About 164 | 165 | This project started off as a port of Mahmoud's 166 | [PrettySize.NET](https://github.com/neosmart/PrettySize.net) library from C# to Rust. Like 167 | the C# edition of this project. Rust's richer `enum` types and powerful generics made 168 | implementing a custom `Size` generic over the number type without verbosity additionally 169 | possible. Its scope has since grown considerably. 170 | 171 | # License 172 | 173 | `PrettySize` is written and maintained by Mahmoud Al-Qudsi of NeoSmart Technologies and 174 | released to the general public under the terms of the MIT public license. 175 | 176 | ## To-Do 177 | 178 | *This section is currently empty 🎉* 179 | 180 | Pull requests are welcome! 181 | -------------------------------------------------------------------------------- /examples/blog.rs: -------------------------------------------------------------------------------- 1 | #![allow(deprecated)] 2 | 3 | // This is the code in the release post at 4 | // https://neosmart.net/blog/2018/prettysize-for-rust/ 5 | extern crate size; 6 | 7 | #[cfg(feature = "std")] 8 | use size::{consts, Size}; 9 | 10 | #[cfg(feature = "std")] 11 | fn main() { 12 | let bytes = 42 * consts::MiB; 13 | assert_eq!(bytes, 44040192); 14 | 15 | let bytes = Size::Mebibytes(42); 16 | assert_eq!(format!("{}", bytes), "42.0 MiB"); 17 | } 18 | 19 | #[cfg(not(feature = "std"))] 20 | fn main() {} 21 | -------------------------------------------------------------------------------- /examples/readme.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "std")] 2 | mod std { 3 | use size::consts; 4 | use size::{Base, Size}; 5 | 6 | pub(super) fn main() { 7 | // Create strongly-typed sizes: 8 | let byte_count = Size::from_kilobytes(42); 9 | assert_eq!(42_000, byte_count.bytes()); 10 | 11 | // Use predefined constants for the various units 12 | let byte_count = 42 * consts::KiB; 13 | assert_eq!(43_008, byte_count); 14 | 15 | // `Size` can take any numeric type you throw at it 16 | let byte_count = Size::from_mib(0.040055); 17 | assert_eq!(byte_count.bytes(), 42_000); 18 | 19 | // And for those of you that haven't yet drunk the base-two Kool-Aid: 20 | let file_size = Size::from_kb(42); 21 | assert_eq!(file_size.bytes(), 42_000); 22 | 23 | println!("{}, I say!", file_size); 24 | // prints "41 KiB, I say!" 25 | 26 | // Override the default choice of base-2 units 27 | println!("{}, I meant!", file_size.format().with_base(Base::Base10)); 28 | // prints "42 KB, I meant!" 29 | 30 | // Add and subtract strongly-typed sizes, even with different underlying types 31 | let sum = Size::from_mb(1.0) + Size::from_kb(200); 32 | assert_eq!(sum.bytes(), 1_200_000); 33 | 34 | // Multiply and divide strongly-typed sizes by scalar values 35 | let new_size = Size::from_mib(2) * 2; 36 | assert_eq!(new_size, Size::from_mib(4)); 37 | 38 | // Compare sizes for equality or order 39 | let size1 = Size::from_gigabytes(2); 40 | let size2 = Size::from_gibibytes(1.99); 41 | assert!(size1 < size2); 42 | } 43 | } 44 | 45 | #[cfg(not(feature = "std"))] 46 | fn main() {} 47 | #[cfg(feature = "std")] 48 | fn main() { 49 | std::main() 50 | } 51 | -------------------------------------------------------------------------------- /examples/tweet.rs: -------------------------------------------------------------------------------- 1 | // Work around https://github.com/rust-lang/cargo/issues/9208 2 | #[cfg(not(feature = "serde"))] 3 | fn main() { 4 | panic!("Requisite feature is not enabled!") 5 | } 6 | 7 | #[cfg(feature = "serde")] 8 | fn main() { 9 | use serde::{Deserialize, Serialize}; 10 | use size::consts::*; // Import consts like KB, MiB, etc. 11 | use size::Size; // Core type for all size operations 12 | 13 | // Flexible construction options 14 | let s = Size::from_bytes(440 * KB) + Size::from_mib(12.9); 15 | println!("The pretty file size {s}"); // 13.3 MiB 16 | 17 | // Mathematical operations on sizes and scalar values 18 | let double = Size::from_kb(0.668) * 2 + Size::from_bytes(1); 19 | assert_eq!(double.bytes(), 1337); 20 | 21 | // Parse sizes from strings in almost any format 22 | let parsed = Size::from_str("43.008 KB").unwrap(); 23 | assert_eq!(Size::from_kib(42.0), parsed); 24 | 25 | #[derive(Debug, Deserialize, Serialize)] 26 | struct File { 27 | name: String, 28 | size: Size, 29 | disk_size: Size, 30 | } 31 | 32 | // Serialize and and deserialize from byte values or strings 33 | let _: File = serde_json::from_str( 34 | r#"{ 35 | "name": "Hello.txt", 36 | "size": "12.92 gigabytes", 37 | "disk_size": 12920000000 38 | }"#, 39 | ) 40 | .unwrap(); 41 | } 42 | -------------------------------------------------------------------------------- /src/fmt.rs: -------------------------------------------------------------------------------- 1 | //! The `fmt` module contains [`SizeFormatter`] and other types pertaining to formatting a size as 2 | //! human-readable text. 3 | //! 4 | //! You will likely not need to interact with this module directly, as the functionality of a 5 | //! [`SizeFormatter`] is exposed by simply calling [`Size::format()`]. However, a new 6 | //! [`SizeFormatter`] can be instantiated directly if you would like a standalone pretty-printer for 7 | //! raw byte sizes. 8 | //! 9 | //! The formatting-related enums in this module ([`Base`] and [`Style`]) are re-exported at the 10 | //! crate level as `size::Base` and `size::Style`. 11 | 12 | use super::*; 13 | use core::fmt; 14 | 15 | /// An enumeration of supported bases to use for generating textual descriptions of sizes. 16 | /// 17 | /// [`Base::Base10`] is the "usual" units like "kilobyte" and "exabyte", while [`Base::Base2`] is 18 | /// the SI/memory units like "mebibyte" and "tebibyte", (more often referred to as "MiB" and "TiB", 19 | /// respectively). 20 | #[non_exhaustive] 21 | #[derive(Copy, Clone, Debug)] 22 | pub enum Base { 23 | /// Base-2 units like "kibibyte" and "mebibyte", more often referred to via their abbreviations 24 | /// ("KiB" and "MiB", respectively). Each unit is 1024 times greater than the preceding one. 25 | Base2, 26 | /// Base-10 units like "kilobyte" and "megabyte". Each unit is 1000 times greater than the 27 | /// preceding one. 28 | Base10, 29 | } 30 | 31 | /// A collection of units used to refer to sizes, for all supported bases. 32 | enum Unit { 33 | /// The basic "byte" unit, used by both base-2 and base-10 styles. 34 | Byte, 35 | /// The base-2 "kibibyte" unit, equal to 1024 bytes. 36 | Kibibyte, 37 | /// The base-10 "kilobyte" unit, equal to 1000 bytes. 38 | Kilobyte, 39 | /// The base-2 "mebibyte" unit, equal to 1024 kibibytes. 40 | Mebibyte, 41 | /// The base-10 "megabyte" unit, equal to 1000 kilobytes. 42 | Megabyte, 43 | /// The base-2 "gibibyte" unit, equal to 1024 mebibytes. 44 | Gibibyte, 45 | /// The base-10 "gigabyte" unit, equal to 1000 megabytes. 46 | Gigabyte, 47 | /// The base-2 "tebibyte" unit, equal to 1024 gibibytes. 48 | Tebibyte, 49 | /// The base-10 "terabyte" unit, equal to 1000 gigabytes. 50 | Terabyte, 51 | /// The base-2 "pebibyte" unit, equal to 1024 tebibytes. 52 | Pebibyte, 53 | /// The base-10 "petabyte" unit, equal to 1000 terabytes. 54 | Petabyte, 55 | /// The base-2 "exbibyte" unit, equal to 1024 pebibytes. 56 | Exbibyte, 57 | /// The base-10 "exabyte" unit, equal to 1000 petabytes. 58 | Exabyte, 59 | } 60 | 61 | impl Unit { 62 | #[rustfmt::skip] 63 | const fn text(&self) -> (&'static str, &'static str, &'static str, &'static str) { 64 | use self::Unit::*; 65 | 66 | match self { 67 | Byte => ("byte", "Byte", "b", "B"), 68 | 69 | Kilobyte => ("kilobyte", "Kilobyte", "kb", "KB"), 70 | Megabyte => ("megabyte", "Megabyte", "mb", "MB"), 71 | Gigabyte => ("gigabyte", "Gigabyte", "gb", "GB"), 72 | Terabyte => ("terabyte", "Terabyte", "tb", "TB"), 73 | Petabyte => ("petabyte", "Petabyte", "pb", "PB"), 74 | Exabyte => ("exabyte", "Exabyte", "eb", "EB"), 75 | 76 | Kibibyte => ("kibibyte", "Kibibyte", "kib", "KiB"), 77 | Mebibyte => ("mebibyte", "Mebibyte", "mib", "MiB"), 78 | Gibibyte => ("gibibyte", "Gibibyte", "gib", "GiB"), 79 | Pebibyte => ("pebibyte", "Pebibyte", "pib", "PiB"), 80 | Tebibyte => ("tebibyte", "Tebibyte", "tib", "TiB"), 81 | Exbibyte => ("exbibyte", "Exbibyte", "eib", "EiB"), 82 | } 83 | } 84 | 85 | fn format(&self, fmt: &mut fmt::Formatter, bytes: u64, style: &Style) -> fmt::Result { 86 | match (&style, bytes) { 87 | (&Style::Default, _) => match &self { 88 | &Unit::Byte => self.format(fmt, bytes, &Style::FullLowercase), 89 | _ => self.format(fmt, bytes, &Style::Abbreviated), 90 | }, 91 | 92 | (&Style::FullLowercase, 1) => write!(fmt, " {}", self.text().0), 93 | (&Style::Full, 1) => write!(fmt, " {}", self.text().1), 94 | (&Style::AbbreviatedLowercase, 1) => write!(fmt, " {}", self.text().2), 95 | (&Style::Abbreviated, 1) => write!(fmt, " {}", self.text().3), 96 | 97 | (&Style::FullLowercase, _) => write!(fmt, " {}s", self.text().0), 98 | (&Style::Full, _) => write!(fmt, " {}s", self.text().1), 99 | (&Style::AbbreviatedLowercase, _) => write!(fmt, " {}", self.text().2), 100 | (&Style::Abbreviated, _) => write!(fmt, " {}", self.text().3), 101 | } 102 | } 103 | } 104 | 105 | /// An enumeration of supported styles to be used when formatting/printing a [`Size`] type, 106 | /// specifying how the unit should be spelled out. 107 | #[non_exhaustive] 108 | #[derive(Copy, Clone, Debug)] 109 | pub enum Style { 110 | /// The default "smart" style, currently equal to [`Style::FullLowercase`] when the final unit 111 | /// is in bytes or [`Style::Abbreviated`] otherwise, e.g. "1024 bytes" and "1.29 GiB" 112 | Default, 113 | /// Abbreviated style, e.g. "1024 KB" and "1.29 GiB" 114 | Abbreviated, 115 | /// Abbreviated, lowercase style, e.g. "1024 kb" and "1.29 gib" 116 | AbbreviatedLowercase, 117 | /// Full unit name style, e.g. "1024 Kilobytes" and "1.29 Gibibytes" 118 | Full, 119 | /// Full, lowercase unit name style, e.g. "1024 kilobytes" and "1.29 gibibytes" 120 | FullLowercase, 121 | } 122 | 123 | // Backwards-compatibility associated constants to mimic `Style` variants to enable compilation of 124 | // older code. They are all hidden from the docs. 125 | impl Style { 126 | #[doc(hidden)] 127 | #[allow(non_upper_case_globals)] 128 | #[deprecated(since = "0.3.0", note = "Use Style::Default instead")] 129 | /// A backwards-compatible alias for [`Style::Default`] 130 | pub const Smart: Style = Style::Default; 131 | 132 | #[doc(hidden)] 133 | #[allow(non_upper_case_globals)] 134 | #[deprecated(since = "0.3.0", note = "Use Style::AbbreviatedLowercase instead")] 135 | /// A backwards-compatible alias for [`Style::AbbreviatedLowercase`] 136 | pub const AbbreviatedLowerCase: Style = Style::AbbreviatedLowercase; 137 | 138 | #[doc(hidden)] 139 | #[allow(non_upper_case_globals)] 140 | #[deprecated(since = "0.3.0", note = "Use Style::FullLowercase instead")] 141 | /// A backwards-compatible alias for [`Style::FullLowercase`] 142 | pub const FullLowerCase: Style = Style::FullLowercase; 143 | } 144 | 145 | impl std::fmt::Display for Size { 146 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 147 | write!(fmt, "{}", self.format()) 148 | } 149 | } 150 | 151 | mod sealed { 152 | pub trait FormatterSize {} 153 | 154 | impl FormatterSize for () {} 155 | impl<'a> FormatterSize for &'a crate::Size {} 156 | } 157 | 158 | /// A standalone size formatter that is configured via the builder pattern (via the various `.with_` 159 | /// methods) which can then be used to format an integral byte value as a pretty printed `String` in 160 | /// accordance with the configured properties. 161 | /// 162 | /// Use of the strongly typed [`Size`] to hold and display sizes is strongly preferred over this 163 | /// approach, but it may come in handy when you have many sizes and all need to be formatted in an 164 | /// identical and manually-specified fashion. 165 | /// 166 | /// ``` 167 | /// use size::{Base, Size, SizeFormatter, Style}; 168 | /// 169 | /// let formatter = SizeFormatter::new() 170 | /// // Use base-10 units like MB and KB, not MiB and KiB 171 | /// .with_base(Base::Base10) 172 | /// // Use abbreviated unit names (e.g. MB and not megabyte) 173 | /// .with_style(Style::Abbreviated) 174 | /// // Print sizes with two digits after the decimal point (e.g. 2.00 KiB, not 2 KiB) 175 | /// .with_scale(Some(2)); 176 | /// 177 | /// # let mut sizes: Vec = Vec::new(); 178 | /// for raw_size in [ 1024, 2048, 4096 ] 179 | /// # // Work around limitation in earlier rustc versions (e.g. 1.50) 180 | /// # .iter() 181 | /// { 182 | /// # // Work around limitation in earlier rustc versions (e.g. 1.50) 183 | /// # let raw_size = *raw_size; 184 | /// let formatted = formatter.format(raw_size); 185 | /// println!("{}", &formatted); 186 | /// # sizes.push(formatted); 187 | /// } 188 | /// 189 | /// // Prints: 190 | /// // 1.02 KB 191 | /// // 2.05 KB 192 | /// // 4.10 KB 193 | /// 194 | /// # assert_eq!(sizes[0].as_str(), "1.02 KB"); 195 | /// # assert_eq!(sizes[1].as_str(), "2.05 KB"); 196 | /// # assert_eq!(sizes[2].as_str(), "4.10 KB"); 197 | /// ``` 198 | pub struct SizeFormatter { 199 | size: T, 200 | base: Base, 201 | style: Style, 202 | scale: Option, 203 | } 204 | 205 | impl Default for SizeFormatter<()> { 206 | fn default() -> Self { 207 | Self::new() 208 | } 209 | } 210 | 211 | /// Makes it possible to obtain a string from an `fmt(f: &mut Formatter)` function by initializing 212 | /// this type as a wrapper around said format function, then using `format!("{}", foo)` on the 213 | /// resulting object. 214 | struct FmtRenderer fmt::Result> { 215 | formatter: F, 216 | } 217 | 218 | impl fmt::Result> FmtRenderer { 219 | pub fn new(formatter: F) -> Self { 220 | Self { formatter } 221 | } 222 | } 223 | 224 | impl fmt::Result> fmt::Display for FmtRenderer { 225 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 226 | (self.formatter)(f) 227 | } 228 | } 229 | 230 | impl SizeFormatter { 231 | /// Specify the base of the units to be used when generating the textual description of the 232 | /// `Size`. 233 | /// 234 | /// This lets users choose between "standard" base-10 units like "KB" and "MB" or the improved 235 | /// SI base-2 units like "KiB" and "MiB". See [`Base`] for more information. 236 | pub fn with_base(self, base: Base) -> Self { 237 | Self { base, ..self } 238 | } 239 | 240 | /// Specify the style used to write the accompanying unit for a formatted file size. 241 | /// 242 | /// See [`Style`] for more information. 243 | pub fn with_style(self, style: Style) -> Self { 244 | Self { style, ..self } 245 | } 246 | 247 | /// Specify the scale/precision of the formatted sizes. 248 | /// 249 | /// Sets the number of digits after the decimal point for formatted sizes. A value of `Some(0)` 250 | /// prints whole numbers only while a value of `None` uses the default formatting which uses a 251 | /// different scale/precision for sizes depending on the chosen unit. 252 | /// 253 | /// # Examples 254 | /// 255 | /// ``` 256 | /// use size::Size; 257 | /// 258 | /// let formatted = Size::from_bytes(42_000) 259 | /// .format() 260 | /// .with_scale(Some(3)) 261 | /// .to_string(); 262 | /// assert_eq!(&formatted, "41.016 KiB"); 263 | /// ``` 264 | /// 265 | /// Sizes that are printed as a whole number of bytes do not have a scale: 266 | /// ``` 267 | /// use size::SizeFormatter; 268 | /// 269 | /// let bytes = SizeFormatter::new() 270 | /// .with_scale(Some(2)) 271 | /// .format(123); 272 | /// assert_eq!(&bytes, "123 bytes"); 273 | /// ``` 274 | pub fn with_scale(self, scale: Option) -> Self { 275 | Self { scale, ..self } 276 | } 277 | 278 | /// Formats the provided `bytes` value with the configured [`self.base`], [`self.style`], and 279 | /// [`self.scale`]. 280 | fn inner_fmt(&self, fmt: &mut fmt::Formatter, bytes: i64) -> fmt::Result { 281 | let bytes = match bytes { 282 | x @ 0..=i64::MAX => x as u64, 283 | y => { 284 | write!(fmt, "-")?; 285 | 286 | // The absolute magnitude of T::MIN for a signed number is one more than 287 | // that of T::MAX, meaning T::MIN.abs() will panic. 288 | match y.checked_abs() { 289 | Some(abs) => abs as u64, 290 | None => i64::MAX as u64, 291 | } 292 | } 293 | }; 294 | 295 | let rule = match self.base { 296 | Base::Base2 => match BASE2_RULES.binary_search_by_key(&bytes, |rule| rule.less_than) { 297 | Ok(index) => &BASE2_RULES[index + 1], 298 | Err(index) => &BASE2_RULES[index], 299 | }, 300 | Base::Base10 => { 301 | match BASE10_RULES.binary_search_by_key(&bytes, |rule| rule.less_than) { 302 | Ok(index) => &BASE10_RULES[index + 1], 303 | Err(index) => &BASE10_RULES[index], 304 | } 305 | } 306 | }; 307 | 308 | (rule.formatter)(fmt, bytes, self.scale)?; 309 | rule.unit.format(fmt, bytes, &self.style)?; 310 | 311 | Ok(()) 312 | } 313 | } 314 | 315 | impl SizeFormatter<()> { 316 | /// Create a new `SizeFormatter` that can be used to repeatedly format a number of file sizes 317 | /// according to its configured options. 318 | pub const fn new() -> SizeFormatter<()> { 319 | SizeFormatter { 320 | size: (), 321 | base: DEFAULT_BASE, 322 | style: DEFAULT_STYLE, 323 | scale: DEFAULT_SCALE, 324 | } 325 | } 326 | 327 | /// Formats a provided size in bytes as a string, per the configuration of the current 328 | /// `SizeFormatter` instance. 329 | pub fn format(&self, bytes: i64) -> String { 330 | format!( 331 | "{}", 332 | FmtRenderer::new(|fmt: &mut fmt::Formatter| { self.inner_fmt(fmt, bytes) }) 333 | ) 334 | } 335 | } 336 | 337 | /// Result of [`Size::format()`], allowing customization of size pretty printing. 338 | /// 339 | /// This is a specialization of [`SizeFormatter`] that can be used to achieve greater control over 340 | /// how a specific [`Size`] value is formatted as human-readable text, created by calling 341 | /// [`Size::format()`]. The `SizeFormatter` follows the builder model and exposes a chaining API for 342 | /// configuration (via the `.with_` functions). 343 | /// 344 | /// After configuration, a `FormattableSize` may be passed directly to the `println!()` or 345 | /// `format!()` macros and their friends because it implements [`Display`](std::fmt::Display), or 346 | /// [`FormattableSize::to_string()`](ToString::to_string) can be used to retrieve a `String` 347 | /// containing the formatted result. 348 | /// 349 | /// To configure once and repeatedly print sizes in the same format, create a standalone 350 | /// [`SizeFormatter`] instead of using `Size::format()`. 351 | /// 352 | /// Example: 353 | /// ``` 354 | /// use size::{Base, Size, Style}; 355 | /// 356 | /// let size = Size::from_mib(1.907349); 357 | /// let text = size.format() 358 | /// .with_base(Base::Base10) // use base-10 sizes 359 | /// .with_style(Style::Full) // print full unit names 360 | /// .with_scale(Some(2)) // two digits after the decimal 361 | /// .to_string(); 362 | /// 363 | /// assert_eq!(text.as_str(), "2.00 Megabytes"); 364 | /// ``` 365 | /// 366 | /// The call to `.to_string()` can be omitted if you are passing the `SizeFormatter`/ 367 | /// `FormattableSize` to any of the format macros like `println!()` and co. 368 | pub type FormattableSize<'a> = SizeFormatter<&'a Size>; 369 | 370 | impl fmt::Display for FormattableSize<'_> { 371 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 372 | self.inner_fmt(f, self.size.bytes()) 373 | } 374 | } 375 | 376 | impl Size { 377 | /// Returns a textual representation of the [`Size`] for display purposes. 378 | /// 379 | /// Gives the caller control over the returned value's scale, base (see [`Base::Base2`] and 380 | /// [`Base::Base10`]), and the style used to express the determined unit (see [`Style`]). 381 | /// 382 | /// Example: 383 | /// ``` 384 | /// use size::{Base, Size, Style}; 385 | /// 386 | /// let size = Size::from_mib(1.907349); 387 | /// let text = size.format() 388 | /// .with_base(Base::Base10) 389 | /// .with_style(Style::Full) 390 | /// .with_scale(Some(2)) 391 | /// .to_string(); 392 | /// 393 | /// assert_eq!(text.as_str(), "2.00 Megabytes"); 394 | /// ``` 395 | /// 396 | /// It is not necessary to call `.to_string()` if you are passing the formatted size to a 397 | /// `format!()` macro or similar (e.g. `println!` and friends), as the result implements 398 | /// [`Display`](std::fmt::Display) and will resolve to the same text. 399 | pub fn format(&self) -> FormattableSize { 400 | FormattableSize { 401 | size: self, 402 | base: DEFAULT_BASE, 403 | style: DEFAULT_STYLE, 404 | scale: DEFAULT_SCALE, 405 | } 406 | } 407 | } 408 | 409 | struct FormatRule { 410 | less_than: u64, 411 | formatter: fn(&mut fmt::Formatter, bytes: u64, scale: Option) -> fmt::Result, 412 | unit: Unit, 413 | } 414 | 415 | const BASE10_RULES: [FormatRule; 17] = [ 416 | FormatRule { 417 | less_than: KILOBYTE as u64, 418 | formatter: |fmt, bytes, _| write!(fmt, "{0:.0}", bytes), 419 | unit: Unit::Byte, 420 | }, 421 | FormatRule { 422 | less_than: 10 * KILOBYTE as u64, 423 | formatter: |fmt, bytes, scale| { 424 | write!(fmt, "{0:.1$}", bytes as f64 / (KILOBYTE as f64), scale.unwrap_or(2)) 425 | }, 426 | unit: Unit::Kilobyte, 427 | }, 428 | FormatRule { 429 | less_than: 100 * KILOBYTE as u64, 430 | formatter: |fmt, bytes, scale| { 431 | write!(fmt, "{0:.1$}", bytes as f64 / (KILOBYTE as f64), scale.unwrap_or(1)) 432 | }, 433 | unit: Unit::Kilobyte, 434 | }, 435 | FormatRule { 436 | less_than: MEGABYTE as u64, 437 | formatter: |fmt, bytes, scale| { 438 | write!(fmt, "{0:.1$}", bytes as f64 / (KILOBYTE as f64), scale.unwrap_or(0)) 439 | }, 440 | unit: Unit::Kilobyte, 441 | }, 442 | FormatRule { 443 | less_than: 10 * MEGABYTE as u64, 444 | formatter: |fmt, bytes, scale| { 445 | write!(fmt, "{0:.1$}", bytes as f64 / (MEGABYTE as f64), scale.unwrap_or(2)) 446 | }, 447 | unit: Unit::Megabyte, 448 | }, 449 | FormatRule { 450 | less_than: 100 * MEGABYTE as u64, 451 | formatter: |fmt, bytes, scale| { 452 | write!(fmt, "{0:.1$}", bytes as f64 / (MEGABYTE as f64), scale.unwrap_or(1)) 453 | }, 454 | unit: Unit::Megabyte, 455 | }, 456 | FormatRule { 457 | less_than: GIGABYTE as u64, 458 | formatter: |fmt, bytes, scale| { 459 | write!(fmt, "{0:.1$}", bytes as f64 / (MEGABYTE as f64), scale.unwrap_or(0)) 460 | }, 461 | unit: Unit::Megabyte, 462 | }, 463 | FormatRule { 464 | less_than: 10 * GIGABYTE as u64, 465 | formatter: |fmt, bytes, scale| { 466 | write!(fmt, "{0:.1$}", bytes as f64 / (GIGABYTE as f64), scale.unwrap_or(2)) 467 | }, 468 | unit: Unit::Gigabyte, 469 | }, 470 | FormatRule { 471 | less_than: 100 * GIGABYTE as u64, 472 | formatter: |fmt, bytes, scale| { 473 | write!(fmt, "{0:.1$}", bytes as f64 / (GIGABYTE as f64), scale.unwrap_or(1)) 474 | }, 475 | unit: Unit::Gigabyte, 476 | }, 477 | FormatRule { 478 | less_than: TERABYTE as u64, 479 | formatter: |fmt, bytes, scale| { 480 | write!(fmt, "{0:.1$}", bytes as f64 / (GIGABYTE as f64), scale.unwrap_or(0)) 481 | }, 482 | unit: Unit::Gigabyte, 483 | }, 484 | FormatRule { 485 | less_than: 10 * TERABYTE as u64, 486 | formatter: |fmt, bytes, scale| { 487 | write!(fmt, "{0:.1$}", bytes as f64 / (TERABYTE as f64), scale.unwrap_or(2)) 488 | }, 489 | unit: Unit::Terabyte, 490 | }, 491 | FormatRule { 492 | less_than: 100 * TERABYTE as u64, 493 | formatter: |fmt, bytes, scale| { 494 | write!(fmt, "{0:.1$}", bytes as f64 / (TERABYTE as f64), scale.unwrap_or(1)) 495 | }, 496 | unit: Unit::Terabyte, 497 | }, 498 | FormatRule { 499 | less_than: PETABYTE as u64, 500 | formatter: |fmt, bytes, scale| { 501 | write!(fmt, "{0:.1$}", bytes as f64 / (TERABYTE as f64), scale.unwrap_or(0)) 502 | }, 503 | unit: Unit::Terabyte, 504 | }, 505 | FormatRule { 506 | less_than: 10 * PETABYTE as u64, 507 | formatter: |fmt, bytes, scale| { 508 | write!(fmt, "{0:.1$}", bytes as f64 / (PETABYTE as f64), scale.unwrap_or(2)) 509 | }, 510 | unit: Unit::Petabyte, 511 | }, 512 | FormatRule { 513 | less_than: 100 * PETABYTE as u64, 514 | formatter: |fmt, bytes, scale| { 515 | write!(fmt, "{0:.1$}", bytes as f64 / (PETABYTE as f64), scale.unwrap_or(1)) 516 | }, 517 | unit: Unit::Petabyte, 518 | }, 519 | FormatRule { 520 | less_than: EXABYTE as u64, 521 | formatter: |fmt, bytes, scale| { 522 | write!(fmt, "{0:.1$}", bytes as f64 / (PETABYTE as f64), scale.unwrap_or(0)) 523 | }, 524 | unit: Unit::Petabyte, 525 | }, 526 | FormatRule { 527 | less_than: u64::MAX, 528 | formatter: |fmt, bytes, scale| { 529 | write!(fmt, "{0:.1$}", bytes as f64 / (EXABYTE as f64), scale.unwrap_or(0)) 530 | }, 531 | unit: Unit::Exabyte, 532 | }, 533 | ]; 534 | 535 | const BASE2_RULES: [FormatRule; 17] = [ 536 | FormatRule { 537 | less_than: KIBIBYTE as u64, 538 | formatter: |fmt, bytes, _| write!(fmt, "{0:.0}", bytes), 539 | unit: Unit::Byte, 540 | }, 541 | FormatRule { 542 | less_than: 10 * KIBIBYTE as u64, 543 | formatter: |fmt, bytes, scale| { 544 | write!(fmt, "{0:.1$}", bytes as f64 / (KIBIBYTE as f64), scale.unwrap_or(2)) 545 | }, 546 | unit: Unit::Kibibyte, 547 | }, 548 | FormatRule { 549 | less_than: 100 * KIBIBYTE as u64, 550 | formatter: |fmt, bytes, scale| { 551 | write!(fmt, "{0:.1$}", bytes as f64 / (KIBIBYTE as f64), scale.unwrap_or(1)) 552 | }, 553 | unit: Unit::Kibibyte, 554 | }, 555 | FormatRule { 556 | less_than: MEBIBYTE as u64, 557 | formatter: |fmt, bytes, scale| { 558 | write!(fmt, "{0:.1$}", bytes as f64 / (KIBIBYTE as f64), scale.unwrap_or(0)) 559 | }, 560 | unit: Unit::Kibibyte, 561 | }, 562 | FormatRule { 563 | less_than: 10 * MEBIBYTE as u64, 564 | formatter: |fmt, bytes, scale| { 565 | write!(fmt, "{0:.1$}", bytes as f64 / (MEBIBYTE as f64), scale.unwrap_or(2)) 566 | }, 567 | unit: Unit::Mebibyte, 568 | }, 569 | FormatRule { 570 | less_than: 100 * MEBIBYTE as u64, 571 | formatter: |fmt, bytes, scale| { 572 | write!(fmt, "{0:.1$}", bytes as f64 / (MEBIBYTE as f64), scale.unwrap_or(1)) 573 | }, 574 | unit: Unit::Mebibyte, 575 | }, 576 | FormatRule { 577 | less_than: GIBIBYTE as u64, 578 | formatter: |fmt, bytes, scale| { 579 | write!(fmt, "{0:.1$}", bytes as f64 / (MEBIBYTE as f64), scale.unwrap_or(0)) 580 | }, 581 | unit: Unit::Mebibyte, 582 | }, 583 | FormatRule { 584 | less_than: 10 * GIBIBYTE as u64, 585 | formatter: |fmt, bytes, scale| { 586 | write!(fmt, "{0:.1$}", bytes as f64 / (GIBIBYTE as f64), scale.unwrap_or(2)) 587 | }, 588 | unit: Unit::Gibibyte, 589 | }, 590 | FormatRule { 591 | less_than: 100 * GIBIBYTE as u64, 592 | formatter: |fmt, bytes, scale| { 593 | write!(fmt, "{0:.1$}", bytes as f64 / (GIBIBYTE as f64), scale.unwrap_or(1)) 594 | }, 595 | unit: Unit::Gibibyte, 596 | }, 597 | FormatRule { 598 | less_than: TEBIBYTE as u64, 599 | formatter: |fmt, bytes, scale| { 600 | write!(fmt, "{0:.1$}", bytes as f64 / (GIBIBYTE as f64), scale.unwrap_or(0)) 601 | }, 602 | unit: Unit::Gibibyte, 603 | }, 604 | FormatRule { 605 | less_than: 10 * TEBIBYTE as u64, 606 | formatter: |fmt, bytes, scale| { 607 | write!(fmt, "{0:.1$}", bytes as f64 / (TEBIBYTE as f64), scale.unwrap_or(2)) 608 | }, 609 | unit: Unit::Tebibyte, 610 | }, 611 | FormatRule { 612 | less_than: 100 * TEBIBYTE as u64, 613 | formatter: |fmt, bytes, scale| { 614 | write!(fmt, "{0:.1$}", bytes as f64 / (TEBIBYTE as f64), scale.unwrap_or(1)) 615 | }, 616 | unit: Unit::Tebibyte, 617 | }, 618 | FormatRule { 619 | less_than: PEBIBYTE as u64, 620 | formatter: |fmt, bytes, scale| { 621 | write!(fmt, "{0:.1$}", bytes as f64 / (TEBIBYTE as f64), scale.unwrap_or(0)) 622 | }, 623 | unit: Unit::Tebibyte, 624 | }, 625 | FormatRule { 626 | less_than: 10 * PEBIBYTE as u64, 627 | formatter: |fmt, bytes, scale| { 628 | write!(fmt, "{0:.1$}", bytes as f64 / (PEBIBYTE as f64), scale.unwrap_or(2)) 629 | }, 630 | unit: Unit::Pebibyte, 631 | }, 632 | FormatRule { 633 | less_than: 100 * PEBIBYTE as u64, 634 | formatter: |fmt, bytes, scale| { 635 | write!(fmt, "{0:.1$}", bytes as f64 / (PEBIBYTE as f64), scale.unwrap_or(1)) 636 | }, 637 | unit: Unit::Pebibyte, 638 | }, 639 | FormatRule { 640 | less_than: EXBIBYTE as u64, 641 | formatter: |fmt, bytes, scale| { 642 | write!(fmt, "{0:.1$}", bytes as f64 / (PEBIBYTE as f64), scale.unwrap_or(0)) 643 | }, 644 | unit: Unit::Pebibyte, 645 | }, 646 | FormatRule { 647 | less_than: u64::MAX, 648 | formatter: |fmt, bytes, scale| { 649 | write!(fmt, "{0:.1$}", bytes as f64 / (EXBIBYTE as f64), scale.unwrap_or(0)) 650 | }, 651 | unit: Unit::Exbibyte, 652 | }, 653 | ]; 654 | -------------------------------------------------------------------------------- /src/from_str.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::str::FromStr; 3 | 4 | use crate::consts::*; 5 | use crate::Size; 6 | 7 | /// Represents an error parsing a `Size` from a string representation. 8 | #[derive(Debug, PartialEq, Clone, Eq)] 9 | pub struct ParseSizeError; 10 | 11 | impl Error for ParseSizeError {} 12 | impl core::fmt::Display for ParseSizeError { 13 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 14 | f.write_str("Error parsing Size") 15 | } 16 | } 17 | 18 | impl Size { 19 | /// Parse a string representation of size to a `Size` value. 20 | /// 21 | /// Supports any mix-and-match of the following text formats: 22 | /// * 1234 23 | /// * 1234 b/kb/mb/etc 24 | /// * 1234 B/KB/MB/etc 25 | /// * 1234 B/KiB/MiB/etc 26 | /// * 1234MB 27 | /// * 12.34 GB 28 | /// * 1234 byte/kilobyte/terabyte/etc 29 | /// * 1234 bytes/kilobytes/terabytes/etc 30 | /// * 12.34 Kibibytes/MegaBytes/etc 31 | /// 32 | /// # Example 33 | /// 34 | /// ```rust 35 | /// use size::Size; 36 | /// 37 | /// let size = Size::from_str("12.34 KB").unwrap(); 38 | /// assert_eq!(size.bytes(), 12_340); 39 | /// ``` 40 | #[allow(clippy::should_implement_trait)] 41 | pub fn from_str(s: &str) -> Result { 42 | FromStr::from_str(s) 43 | } 44 | } 45 | 46 | /// This test just ensures everything is wired up correctly between the member function 47 | /// `[Size::from_str()]` and the `FromStr` trait impl. 48 | #[test] 49 | fn from_str() { 50 | let input = "12.34 kIloByte"; 51 | let parsed = Size::from_str(input); 52 | let expected = Size::from_bytes(12.34 * crate::consts::KB as f64); 53 | assert_eq!(parsed, Ok(expected)); 54 | } 55 | 56 | #[test] 57 | fn parse() { 58 | let size = "12.34 kIloByte".parse(); 59 | assert_eq!(size, Ok(Size::from_bytes(12 * KB + 340))); 60 | } 61 | 62 | impl FromStr for Size { 63 | type Err = ParseSizeError; 64 | 65 | fn from_str(s: &str) -> Result { 66 | let s = s.trim(); 67 | 68 | // Try to split before the first unit char in the input. This supports the (unadvertised) 69 | // ability to parse scientific notation w/o spaces between scalar and unit. 70 | let (num_str, unit) = match s.rfind(|c: char| !c.is_ascii_alphabetic()).map(|i| i + 1) { 71 | None => (s, ""), // just a number, no unit 72 | Some(idx) => s.split_at(idx), 73 | }; 74 | 75 | let number: f64 = num_str.trim_end().parse().map_err(|_| ParseSizeError)?; 76 | let unit = unit.to_lowercase(); 77 | 78 | let multiplier = match unit.as_str().trim_end_matches('s') { 79 | "" | "b" | "byte" => B, 80 | "kb" | "kilobyte" => KB, 81 | "mb" | "megabyte" => MB, 82 | "gb" | "gigabyte" => GB, 83 | "tb" | "terabyte" => TB, 84 | "pb" | "petabyte" => PB, 85 | "eb" | "exabyte" => EB, 86 | 87 | "kib" | "kibibyte" => KiB, 88 | "mib" | "mebibyte" => MiB, 89 | "gib" | "gibibyte" => GiB, 90 | "tib" | "tebibyte" => TiB, 91 | "pib" | "pebibyte" => PiB, 92 | "eib" | "exbibyte" => EiB, 93 | 94 | _ => return Err(ParseSizeError), 95 | }; 96 | 97 | Ok(Size::from_bytes(number * multiplier as f64)) 98 | } 99 | } 100 | 101 | #[cfg(test)] 102 | mod tests { 103 | use super::*; 104 | 105 | #[test] 106 | fn parse_bare_bytes() { 107 | assert_eq!(Size::from_str("1234"), Ok(Size { bytes: 1234 })); 108 | assert_eq!(Size::from_str(" 1234 "), Ok(Size { bytes: 1234 })); // Leading and trailing whitespace 109 | } 110 | 111 | #[test] 112 | fn parse_abbr_unit() { 113 | let tests = vec![ 114 | ("1234B", 1234), 115 | ("1234 KB", 1234 * KB), 116 | ("1234KiB", 1234 * KiB), 117 | ("12.34 MB", (12.34 * MB as f64) as i64), 118 | ("12.34MiB", (12.34 * MiB as f64) as i64), 119 | (" 1234 GB ", 1234 * GB), 120 | ]; 121 | 122 | for (input, expected) in tests { 123 | assert_eq!(Size::from_str(input), Ok(Size { bytes: expected })); 124 | } 125 | } 126 | 127 | #[test] 128 | fn parse_full_unit() { 129 | let tests = vec![ 130 | ("1234 bytes", 1234), 131 | ("1234 kilobytes", 1234 * KB), 132 | ("1234 kibibytes", 1234 * KiB), 133 | ("12.34 gigabytes", (12.34 * GB as f64) as i64), 134 | ("12.34 gibibytes", (12.34 * GiB as f64) as i64), 135 | ]; 136 | 137 | for (input, expected) in tests { 138 | assert_eq!(Size::from_str(input), Ok(Size { bytes: expected })); 139 | } 140 | } 141 | 142 | #[test] 143 | fn parse_invalid_inputs() { 144 | let tests = vec![ 145 | "Not a number", 146 | "1234 XB", // Unknown suffix 147 | "12..34 MB", // Invalid number format 148 | ]; 149 | 150 | for input in tests { 151 | assert_eq!(dbg!(Size::from_str(input)), Err(ParseSizeError)); 152 | } 153 | } 154 | 155 | #[test] 156 | fn parse_boundary() { 157 | assert_eq!(Size::from_str("42.0"), Ok(Size::from_bytes(42))); 158 | assert_eq!(Size::from_str("42.0kib "), Ok(Size::from_bytes(42 * KiB))); 159 | } 160 | 161 | #[test] 162 | fn parse_scientific() { 163 | assert_eq!(Size::from_str("0.423E3"), Ok(Size::from_bytes(423))); 164 | assert_eq!(Size::from_str("423E-3 mb"), Ok(Size::from_bytes(423_000))); 165 | assert_eq!(Size::from_str("0.423e3kb"), Ok(Size::from_bytes(423_000))); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std)] 2 | #![cfg_attr(not(feature = "std"), allow(clippy::unnecessary_cast))] 3 | #![warn(missing_docs)] 4 | 5 | //! This crate provides an ergonomic, type-safe, and aesthetically-pleasing [`Size`] type that can 6 | //! be used to express, format, or operate on sizes. While it was initially created to make it 7 | //! painless to "pretty print" file sizes (by automatically determining which unit and with what 8 | //! precision a file size should be textually "written out" or formatted), it has expanded in scope 9 | //! to make it easier and safer to perform the different types of operations that would arise when 10 | //! dealing with sizes. 11 | //! 12 | //! For almost all users, the only surface of interaction with this crate will take place via the 13 | //! `Size` type, which can be used to create a strongly-typed representation of a file size (or any 14 | //! other "size" you need to deal with in the abstract). This crate's API is intended to be as 15 | //! natural and intuitive as possible, providing sensible defaults with zero boilerplate but also 16 | //! allowing the developer to manually control aspects how sizes are expressed as text if needed. 17 | //! 18 | //! The core [`Size`] type is a simple wrapper around a signed numeric value - it can be initialized 19 | //! using whatever primitive numeric type you wish, e.g. constructing a `Size` from an `i64` or from 20 | //! a `foo: f64` number of kilobytes. 21 | //! 22 | //! ## Using this crate and creating a `Size` object 23 | //! 24 | //! To use this crate, you only need to place `use size::Size` at the top of your rust code, then 25 | //! create a `Size` from a constructor/initializer that matches the size you have on hand. Both 26 | //! base-2 (KiB, MiB, etc) and base-10 (KB, MB, etc) units are supported and are exposed via the 27 | //! same API. You can either use the abbreviated form of the unit to instantiate your type, or use 28 | //! the full unit name to be more expressive. Here's an example: 29 | #![cfg_attr(not(feature = "std"), doc = "```ignore")] 30 | #![cfg_attr(feature = "std", doc = "```")] 31 | //! use size::Size; 32 | //! 33 | //! // Create a strongly-typed size object. We don't even need to specify a numeric type! 34 | //! let file1_size = Size::from_bytes(200); 35 | //! // Create another Size instance, this time from a floating-point literal: 36 | //! let file2_size = Size::from_kb(20.1); 37 | //! ``` 38 | //! 39 | //! You can obtain a scalar `i64` value equal to the total number of bytes described by a 40 | //! `Size` instance by calling [`Size::bytes()`] (see link for more info): 41 | #![cfg_attr(not(feature = "std"), doc = "```ignore")] 42 | #![cfg_attr(feature = "std", doc = "```")] 43 | //! use size::Size; 44 | //! 45 | //! let file_size = Size::from_gibibytes(4); 46 | //! assert_eq!(file_size.bytes(), 4_294_967_296); 47 | //! ``` 48 | //! 49 | //! All `Size` types can be directly compared (both for order and equality) to one another (or to 50 | //! references of one another), regardless of their original type: 51 | //! ``` 52 | //! use size::Size; 53 | //! 54 | //! let size1 = Size::from_kib(4 as u8); 55 | //! let size2 = Size::from_bytes(4096 as i64); 56 | //! assert_eq!(size1, size2); 57 | //! 58 | //! let size1 = Size::from_kib(7); 59 | //! let size2 = Size::from_kb(7); 60 | //! assert!(&size2 < &size1); 61 | //! ``` 62 | //! 63 | //! ## Textual representation 64 | //! 65 | //! The majority of users will be interested in this crate for its ability to "pretty print" sizes 66 | //! with little ceremony and great results. All `Size` instances implement both 67 | //! [`std::fmt::Display`] and [`std::fmt::Debug`], so you can just directly `format!(...)` or 68 | //! `println!(...)` with whatever `Size` you have on hand: 69 | #![cfg_attr(not(feature = "std"), doc = "```ignore")] 70 | #![cfg_attr(feature = "std", doc = "```")] 71 | //! use size::Size; 72 | //! 73 | //! let file_size = Size::from_bytes(1_340_249); 74 | //! let textual = format!("{}", file_size); // "1.28 MiB" 75 | //! assert_eq!(textual.as_str(), "1.28 MiB"); 76 | //! ``` 77 | //! 78 | //! [`Size::to_string()`](ToString::to_string) can be used to directly return a `String` containing 79 | //! the formatted, human-readable size, instead of needing to use the `format!()` macro or similar: 80 | #![cfg_attr(not(feature = "std"), doc = "```ignore")] 81 | #![cfg_attr(feature = "std", doc = "```")] 82 | //! use size::Size; 83 | //! 84 | //! let file_size = Size::from_bytes(1_340_249); 85 | //! assert_eq!(file_size.to_string(), "1.28 MiB".to_string()); 86 | //! ``` 87 | //! 88 | //! For fine-grained control over how a size is formatted and displayed, you can manually use the 89 | //! [`Size::format()`] function, which returns a [`FormattableSize`](crate::fmt::FormattableSize) 90 | //! implementing the builder model to allow you to change one or more properties of how a `Size` 91 | //! is formatted: 92 | #![cfg_attr(not(feature = "std"), doc = "```ignore")] 93 | #![cfg_attr(feature = "std", doc = "```")] 94 | //! use size::{Size, Base, Style}; 95 | //! 96 | //! let file_size = Size::from_bytes(1_340_249); // same as before 97 | //! let textual_size = file_size.format() 98 | //! .with_base(Base::Base10) 99 | //! .with_style(Style::FullLowercase) 100 | //! .to_string(); 101 | //! assert_eq!(textual_size, "1.34 megabytes".to_string()); 102 | //! ``` 103 | //! 104 | //! It is also possible to create and configure a standalone [`SizeFormatter`] that can be reused to 105 | //! format many sizes in a single, consistent style. This should not be seen as an alternative to 106 | //! wrapping file sizes in strongly-typed `Size` structs, which should always be the initial 107 | //! instinct. 108 | //! 109 | //! ## Mathematical operations 110 | //! 111 | //! You can perform mathematical operations on `Size` types and the type safety makes sure that 112 | //! what you're doing makes sense: 113 | #![cfg_attr(not(feature = "std"), doc = "```ignore")] 114 | #![cfg_attr(feature = "std", doc = "```")] 115 | //! use size::Size; 116 | //! 117 | //! let sum = Size::from_mib(2) + Size::from_kib(200); 118 | //! assert_eq!(sum, Size::from_mb(2.301_952)); 119 | //! 120 | //! let size = Size::from_gb(4.2) / 2; 121 | //! assert_eq!(size, Size::from_gb(2.1)); 122 | //! ``` 123 | //! 124 | //! See the documentation of the [`ops`] module for more on this topic. 125 | //! 126 | //! ## Parsing sizes from text 127 | //! 128 | //! The [`Size::from_str()`] function can be used to convert the most commonly encountered textual 129 | //! representations of file sizes into properly typed `Size` objects, with flexible support for 130 | //! various input whitespace formatting, abbreviated/full unit names, mixed upper/lower-case 131 | //! representation, etc. 132 | //! 133 | //! ## Crate features 134 | //! 135 | //! The following crate features may be chosen: 136 | //! * `std` (enabled by default) 137 | //! * `serde` 138 | //! 139 | //! If compiled without the `std` feature (i.e. with `--no-default-features` or used as a dependency 140 | //! with default features disabled), the crate becomes `no_std` compatible. When used in `no_std` 141 | //! mode, the following restrictions and limitations are observed: 142 | //! 143 | //! * All formatting/stringification of `Size` types is disabled. 144 | //! * `Size` no longer implements [`std::fmt::Display`] (`core::fmt::Debug` is still implemented). 145 | //! * The intermediate type used for mathematical operations on `Size` types is changed from `f64` 146 | //! to `i64` so that no implicit floating-point math is performed. To prevent inadvertent loss of 147 | //! precision, it is forbidden to pass in floating point values to the `Size` API under `no_std` 148 | //! mode. 149 | //! * The ability to parse strings into `Size` objects (`Size::from_str()` and the `FromStr` impl) 150 | //! are removed. 151 | //! 152 | //! ## Base-2 and Base-10 constants 153 | //! 154 | //! You can individually use constants like `size::KiB` or `size::GB` directly or import all 155 | //! constants into scope with `use size::consts::*` (or just `use size::*`, but that also imports 156 | //! the types and traits defined by this crate, too). 157 | //! 158 | //! ## Serialization support 159 | //! 160 | //! If the crate is compiled with the optional (default: disabled) `serde` feature, the `Size` type 161 | //! may be serialized/deserialized directly to/from payloads via the `serde` crate. The `Size` type 162 | //! is treated as a transparent new-type around `u64` for serialization purposes (i.e. it serializes 163 | //! directly to the number of bytes, not as a struct with the number of bytes as a member/field); 164 | //! this allows deserializing payloads from various APIs or other languages that typically do not 165 | //! use strongly-typed `Size` objects to denote (file) size. 166 | //! 167 | //! As an example, `struct File { name: String, size: Size } ` will serialize to `{ name: "name", 168 | //! size: 1234 }` instead of `{ name: "name", size: { bytes: 1234 }`. 169 | 170 | #[cfg(feature = "std")] 171 | pub mod fmt; 172 | #[cfg(feature = "std")] 173 | mod from_str; 174 | pub mod ops; 175 | #[cfg(feature = "serde")] 176 | mod serde; 177 | #[cfg(test)] 178 | mod tests; 179 | #[cfg(test)] 180 | mod tests_nostd; 181 | 182 | pub use crate::consts::*; 183 | #[cfg(feature = "std")] 184 | pub use crate::fmt::{Base, SizeFormatter, Style}; 185 | #[cfg(feature = "std")] 186 | pub use crate::from_str::ParseSizeError; 187 | use crate::sealed::AsIntermediate; 188 | 189 | #[cfg(feature = "std")] 190 | type Intermediate = f64; 191 | #[cfg(not(feature = "std"))] 192 | type Intermediate = i64; 193 | 194 | #[cfg(feature = "std")] 195 | const DEFAULT_BASE: Base = Base::Base2; 196 | #[cfg(feature = "std")] 197 | const DEFAULT_STYLE: Style = Style::Default; 198 | #[cfg(feature = "std")] 199 | const DEFAULT_SCALE: Option = None; 200 | 201 | mod sealed { 202 | use super::Intermediate; 203 | 204 | pub trait AsIntermediate: Sized { 205 | // This is the same name and signature as `AsPrimitive` trait from the `num_traits` crate 206 | fn as_(self) -> Intermediate; 207 | } 208 | 209 | macro_rules! as_intermediate { 210 | ($type:ty) => { 211 | impl AsIntermediate for $type { 212 | fn as_(self) -> Intermediate { 213 | use core::mem::size_of; 214 | const SIGNED_MAX: $type = Intermediate::MAX as $type; 215 | 216 | // A separate implementation is required for no_std's intermediate i64 to make 217 | // sure u64::MAX is clamped to i64::MAX rather than cast directly to -1. The 218 | // first three checks should be elided per impl via compile-time optimization. 219 | if cfg!(not(feature = "std")) // we are in no_std mode 220 | && <$type>::MIN == 0 as $type // it's an unsigned type 221 | && size_of::() >= size_of::<$type>() // with a greater +range 222 | && self > SIGNED_MAX // and exceeds our max 223 | { 224 | Intermediate::MAX 225 | } else { 226 | self as Intermediate 227 | } 228 | } 229 | } 230 | }; 231 | } 232 | 233 | as_intermediate!(u8); 234 | as_intermediate!(u16); 235 | as_intermediate!(u32); 236 | as_intermediate!(u64); 237 | as_intermediate!(usize); 238 | as_intermediate!(i8); 239 | as_intermediate!(i16); 240 | as_intermediate!(i32); 241 | as_intermediate!(i64); 242 | as_intermediate!(isize); 243 | #[cfg(feature = "std")] 244 | as_intermediate!(f32); 245 | #[cfg(feature = "std")] 246 | as_intermediate!(f64); 247 | } 248 | 249 | /// A collection of constants for base-2 and base-10 units. 250 | /// 251 | /// These can be used in a `const` context in conjunction with the `const` [`Size::from_const()`] 252 | /// function to create strongly-sized [`Size`] objects expressing various sizes, e.g. 253 | /// 254 | /// ``` 255 | /// use size::Size; 256 | /// use size::consts::*; 257 | /// 258 | /// pub const TOTAL_SIZE: Size = Size::from_const(3 * MiB); 259 | /// ``` 260 | /// 261 | /// You can use these directly from the root `size` namespace (e.g. `size::KiB`) or import all size 262 | /// constants with `use size::consts::*` to get access to them all in the scope. 263 | pub mod consts { 264 | #![allow(non_upper_case_globals)] 265 | 266 | /// Basic "byte" constant, used across all bases. 267 | pub const BYTE: i64 = 1; 268 | /// Base-10 "kilobyte" constant, equal to 1000 bytes. 269 | pub const KILOBYTE: i64 = 1000 * BYTE; 270 | /// Base-10 "megabyte" constant, equal to 1000 kilobytes. 271 | pub const MEGABYTE: i64 = 1000 * KILOBYTE; 272 | /// Base-10 "gigabyte" constant, equal to 1000 megabytes. 273 | pub const GIGABYTE: i64 = 1000 * MEGABYTE; 274 | /// Base-10 "terabyte" constant, equal to 1000 gigabytes. 275 | pub const TERABYTE: i64 = 1000 * GIGABYTE; 276 | /// Base-10 "petabyte" constant, equal to 1000 terabytes. 277 | pub const PETABYTE: i64 = 1000 * TERABYTE; 278 | /// Base-10 "exabyte" constant, equal to 1000 petabytes. 279 | pub const EXABYTE: i64 = 1000 * PETABYTE; 280 | 281 | /// Abbreviated "byte" constant. Identical to [`BYTE`]. 282 | pub const B: i64 = BYTE; 283 | /// Abbreviated base-10 "kilobyte" constant, equal to 1000 bytes. Identical to [`KILOBYTE`]. 284 | pub const KB: i64 = KILOBYTE; 285 | /// Abbreviated base-10 "megabyte" constant, equal to 1000 kilobytes. Identical to [`MEGABYTE`]. 286 | pub const MB: i64 = MEGABYTE; 287 | /// Abbreviated base-10 "gigabyte" constant, equal to 1000 megabytes. Identical to [`GIGABYTE`]. 288 | pub const GB: i64 = GIGABYTE; 289 | /// Abbreviated base-10 "terabyte" constant, equal to 1000 gigabytes. Identical to [`TERABYTE`]. 290 | pub const TB: i64 = TERABYTE; 291 | /// Abbreviated base-10 "petabyte" constant, equal to 1000 terabytes. Identical to [`PETABYTE`]. 292 | pub const PB: i64 = PETABYTE; 293 | /// Abbreviated base-10 "exabyte" constant, equal to 1000 petabytes. Identical to [`EXABYTE`]. 294 | pub const EB: i64 = EXABYTE; 295 | 296 | /// Base-2 "kibibyte" constant, equal to 2^10 bytes. 297 | pub const KIBIBYTE: i64 = 1 << 10; 298 | /// Base-2 "mebibyte" constant, equal to 2^20 bytes. 299 | pub const MEBIBYTE: i64 = 1 << 20; 300 | /// Base-2 "gibibyte" constant, equal to 2^30 bytes. 301 | pub const GIBIBYTE: i64 = 1 << 30; 302 | /// Base-2 "tebibyte" constant, equal to 2^40 bytes. 303 | pub const TEBIBYTE: i64 = 1 << 40; 304 | /// Base-2 "pebibyte" constant, equal to 2^50 bytes. 305 | pub const PEBIBYTE: i64 = 1 << 50; 306 | /// Base-2 "exbibyte" constant, equal to 2^60 bytes. 307 | pub const EXBIBYTE: i64 = 1 << 60; 308 | 309 | /// Abbreviated base-2 "kibibyte" constant, equal to 1024 bytes. Identical to [`KIBIBYTE`]. 310 | pub const KiB: i64 = KIBIBYTE; 311 | /// Abbreviated base-2 "mebibyte" constant, equal to 1024 kibibytes. Identical to [`MEBIBYTE`]. 312 | pub const MiB: i64 = MEBIBYTE; 313 | /// Abbreviated base-2 "gibibyte" constant, equal to 1024 mebibytes. Identical to [`GIBIBYTE`]. 314 | pub const GiB: i64 = GIBIBYTE; 315 | /// Abbreviated base-2 "tebibyte" constant, equal to 1024 gibibytes. Identical to [`TEBIBYTE`]. 316 | pub const TiB: i64 = TEBIBYTE; 317 | /// Abbreviated base-2 "pebibyte" constant, equal to 1024 tebibytes. Identical to [`PEBIBYTE`]. 318 | pub const PiB: i64 = PEBIBYTE; 319 | /// Abbreviated base-2 "exbibyte" constant, equal to 1024 pebibytes. Identical to [`EXBIBYTE`]. 320 | pub const EiB: i64 = EXBIBYTE; 321 | } 322 | 323 | /// `Size` is the core type exposed by this crate and allows the developer to express a file size 324 | /// (or the general concept of a "size") as a strongly-typed, convertible type that can be used for 325 | /// textual formatting ("pretty printing") and mathematical operations. 326 | /// 327 | /// A size can be created in terms of any supported unit and an associated numeric value of any 328 | /// type. 329 | #[cfg_attr(not(feature = "std"), doc = "```ignore")] 330 | #[cfg_attr(feature = "std", doc = "```")] 331 | /// use size::Size; 332 | /// 333 | /// // Identical sizes expressed in different units with different primitive types: 334 | /// assert_eq!(Size::from_kibibytes(2_u8), Size::from_kilobytes(2.048_f64)); 335 | /// ``` 336 | #[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash, Default)] 337 | pub struct Size { 338 | bytes: i64, 339 | } 340 | 341 | impl Size { 342 | /// Zero value (0 bytes) 343 | pub const ZERO: Self = Self { bytes: 0 }; 344 | 345 | /// Initialize a `Size` from the provided value, in bytes. This is a constant function and may 346 | /// be used in a `const` context. 347 | /// 348 | /// Unlike the other "from" functions (e.g. [`from_kilobytes()`](Size::from_kilobytes())), it is 349 | /// not generic because 350 | /// a) trait methods (required to use a generic type) may not be declared as `const`, and 351 | /// b) it's always safe to use `as i64` on whatever type you're actually passing into 352 | /// `from_bytes()` without any (additional) loss of precision as compared to passing in an 353 | /// arbitrary numeric type, since there is no math required to calculate the equivalent size in 354 | /// bytes. 355 | /// 356 | /// To further illustrate this point, let's look at this hypothetical initialization of a `Size` 357 | /// from a floating-point literal: `let s = Size::from_kib(2.5);` - when the conversion from 358 | /// "2.5 KiB" to "bytes" happens internally, the result is equivalent to `(2.5 * 1024.0) as i64` 359 | /// and yields the correct result of 2560 bytes. But if `from_kib` weren't generic and you 360 | /// needed to use `as i64` (i.e. `Size::from_kib(2.5 as i64)`), the calculated size in bytes 361 | /// would start from an already-truncated `2_i64` and yield an incorrect answer of 2048 bytes 362 | /// (`(2.5 as i64) * 1024`). However, with `from_bytes()`, there can be no loss of precision 363 | /// (or, pedantically, even truncation) when `as i64` is used since the file size, expressed in 364 | /// bytes, must always be a whole number; this means it is safe to perform the integer 365 | /// conversion/rounding at the call site itself and `Size::from_const(float_val as i64)` would 366 | /// necessarily always yield the same result as the generic/type-agnostic 367 | /// `Size::from_bytes::(float_val)`. 368 | pub const fn from_const(bytes: i64) -> Self { 369 | Self { bytes } 370 | } 371 | 372 | /// Initialize a `Size` from the provided value, in bytes. 373 | pub fn from_bytes(value: T) -> Self { 374 | Self { 375 | bytes: value.as_() as i64, 376 | } 377 | } 378 | 379 | /// Express a size in kilobytes. Actual size is 10^3 \* the value. 380 | pub fn from_kilobytes(value: T) -> Self { 381 | Self { 382 | bytes: (value.as_() * KILOBYTE as Intermediate) as i64, 383 | } 384 | } 385 | 386 | /// Express a size in megabytes. Actual size is 10^6 \* the value. 387 | pub fn from_megabytes(value: T) -> Self { 388 | Self { 389 | bytes: (value.as_() * MEGABYTE as Intermediate) as i64, 390 | } 391 | } 392 | 393 | /// Express a size in gigabytes. Actual size is 10^9 \* the value. 394 | pub fn from_gigabytes(value: T) -> Self { 395 | Self { 396 | bytes: (value.as_() * GIGABYTE as Intermediate) as i64, 397 | } 398 | } 399 | 400 | /// Express a size in terabytes. Actual size is 10^12 \* the value. 401 | pub fn from_terabytes(value: T) -> Self { 402 | Self { 403 | bytes: (value.as_() * TERABYTE as Intermediate) as i64, 404 | } 405 | } 406 | 407 | /// Express a size in petabytes. Actual size is 10^15 \* the value. 408 | pub fn from_petabytes(value: T) -> Self { 409 | Self { 410 | bytes: (value.as_() * PETABYTE as Intermediate) as i64, 411 | } 412 | } 413 | 414 | /// Express a size in exabytes. Actual size is 10^18 \* the value. 415 | pub fn from_exabytes(value: T) -> Self { 416 | Self { 417 | bytes: (value.as_() * EXABYTE as Intermediate) as i64, 418 | } 419 | } 420 | 421 | #[inline] 422 | /// Express a size in kilobytes, as a shortcut for using [`Size::from_kilobytes()`]. 423 | pub fn from_kb(value: T) -> Self { 424 | Self::from_kilobytes(value) 425 | } 426 | #[inline] 427 | /// Express a size in megabytes, as a shortcut for using [`Size::from_megabytes()`]. 428 | pub fn from_mb(value: T) -> Self { 429 | Self::from_megabytes(value) 430 | } 431 | #[inline] 432 | /// Express a size in gigabytes, as a shortcut for using [`Size::from_gigabytes()`]. 433 | pub fn from_gb(value: T) -> Self { 434 | Self::from_gigabytes(value) 435 | } 436 | #[inline] 437 | /// Express a size in terabytes, as a shortcut for using [`Size::from_terabytes()`]. 438 | pub fn from_tb(value: T) -> Self { 439 | Self::from_terabytes(value) 440 | } 441 | #[inline] 442 | /// Express a size in petabytes, as a shortcut for using [`Size::from_petabytes()`]. 443 | pub fn from_pb(value: T) -> Self { 444 | Self::from_petabytes(value) 445 | } 446 | #[inline] 447 | /// Express a size in exabytes, as a shortcut for using [`Size::from_exabytes()`]. 448 | pub fn from_eb(value: T) -> Self { 449 | Self::from_exabytes(value) 450 | } 451 | 452 | /// Express a size in kibibytes. Actual size is 2^10 \* the value. 453 | pub fn from_kibibytes(value: T) -> Self { 454 | Self { 455 | bytes: (value.as_() * KIBIBYTE as Intermediate) as i64, 456 | } 457 | } 458 | 459 | /// Express a size in mebibytes. Actual size is 2^20 \* the value. 460 | pub fn from_mebibytes(value: T) -> Self { 461 | Self { 462 | bytes: (value.as_() * MEBIBYTE as Intermediate) as i64, 463 | } 464 | } 465 | 466 | /// Express a size in gibibytes. Actual size is 2^30 \* the value. 467 | pub fn from_gibibytes(value: T) -> Self { 468 | Self { 469 | bytes: (value.as_() * GIBIBYTE as Intermediate) as i64, 470 | } 471 | } 472 | 473 | /// Express a size in tebibytes. Actual size is 2^40 \* the value. 474 | pub fn from_tebibytes(value: T) -> Self { 475 | Self { 476 | bytes: (value.as_() * TEBIBYTE as Intermediate) as i64, 477 | } 478 | } 479 | 480 | /// Express a size in pebibytes. Actual size is 2^50 \* the value. 481 | pub fn from_pebibytes(value: T) -> Self { 482 | Self { 483 | bytes: (value.as_() * PEBIBYTE as Intermediate) as i64, 484 | } 485 | } 486 | 487 | /// Express a size in exbibytes. Actual size is 2^60 \* the value. 488 | pub fn from_exbibytes(value: T) -> Self { 489 | Self { 490 | bytes: (value.as_() * EXBIBYTE as Intermediate) as i64, 491 | } 492 | } 493 | 494 | #[inline] 495 | /// Express a size in kibibytes, as a shortcut for using [`Size::from_kibibytes()`]. 496 | pub fn from_kib(value: T) -> Self { 497 | Self::from_kibibytes(value) 498 | } 499 | #[inline] 500 | /// Express a size in mebibytes, as a shortcut for using [`Size::from_mebibytes()`]. 501 | pub fn from_mib(value: T) -> Self { 502 | Self::from_mebibytes(value) 503 | } 504 | #[inline] 505 | /// Express a size in gibibytes, as a shortcut for using [`Size::from_gibibytes()`]. 506 | pub fn from_gib(value: T) -> Self { 507 | Self::from_gibibytes(value) 508 | } 509 | #[inline] 510 | /// Express a size in tebibytes, as a shortcut for using [`Size::from_tebibytes()`]. 511 | pub fn from_tib(value: T) -> Self { 512 | Self::from_tebibytes(value) 513 | } 514 | #[inline] 515 | /// Express a size in pebibytes, as a shortcut for using [`Size::from_pebibytes()`]. 516 | pub fn from_pib(value: T) -> Self { 517 | Self::from_pebibytes(value) 518 | } 519 | #[inline] 520 | /// Express a size in exbibytes, as a shortcut for using [`Size::from_exbibytes()`]. 521 | pub fn from_eib(value: T) -> Self { 522 | Self::from_exbibytes(value) 523 | } 524 | } 525 | 526 | impl Size { 527 | #[inline] 528 | /// Returns the effective size in bytes of the type, useful for obtaining a plain/scalar 529 | /// representation of the full size represented by a [`Size`] object. This always returns an 530 | /// `i64` regardless of the underlying type originally used, to avoid (or at least mitigate) 531 | /// issues with integer overflow (e.g. when trying to retrieve `Size::from_tb(16_i32).bytes()`). 532 | /// 533 | /// Example: 534 | /// ``` 535 | /// use size::Size; 536 | /// assert_eq!(Size::from_mib(4_u8).bytes(), 4_194_304 as i64); 537 | /// ``` 538 | pub const fn bytes(&self) -> i64 { 539 | self.bytes 540 | } 541 | } 542 | 543 | // The original `size` approach was a rust enum with each unit expressed as a different variant, but 544 | // that was never really a "rusty" solution and didn't actually match how size calculation was 545 | // handled (with each value being converted to an f64/i64 before calculating the total bytes or the 546 | // mathematical sum/difference/product/etc). The impl block below is for backwards 547 | // source-compatibility purposes (with functions masquerading as enum variants). 548 | #[doc(hidden)] 549 | impl Size { 550 | #![allow(non_snake_case)] 551 | 552 | #[inline] 553 | #[deprecated(since = "0.3.0", note = "Use Size::from_bytes() instead")] 554 | /// Express a size in bytes. 555 | pub fn Bytes(t: T) -> Self { 556 | Self::from_bytes(t) 557 | } 558 | #[inline] 559 | #[deprecated(since = "0.3.0", note = "Use Size::from_kibibytes() instead")] 560 | /// Express a size in kibibytes. Actual size is 2^10 \* the value. 561 | pub fn Kibibytes(t: T) -> Self { 562 | Self::from_kibibytes(t) 563 | } 564 | #[inline] 565 | #[deprecated(since = "0.3.0", note = "Use Size::from_kilobytes() instead")] 566 | /// Express a size in kilobytes. Actual size is 10^3 \* the value. 567 | pub fn Kilobytes(t: T) -> Self { 568 | Self::from_kilobytes(t) 569 | } 570 | #[inline] 571 | #[deprecated(since = "0.3.0", note = "Use Size::from_mebibytes() instead")] 572 | /// Express a size in mebibytes. Actual size is 2^20 \* the value. 573 | pub fn Mebibytes(t: T) -> Self { 574 | Self::from_mebibytes(t) 575 | } 576 | #[inline] 577 | #[deprecated(since = "0.3.0", note = "Use Size::from_megabytes() instead")] 578 | /// Express a size in megabytes. Actual size is 10^6 \* the value. 579 | pub fn Megabytes(t: T) -> Self { 580 | Self::from_megabytes(t) 581 | } 582 | #[inline] 583 | #[deprecated(since = "0.3.0", note = "Use Size::from_gibibytes() instead")] 584 | /// Express a size in gibibytes. Actual size is 2^30 \* the value. 585 | pub fn Gibibytes(t: T) -> Self { 586 | Self::from_gibibytes(t) 587 | } 588 | #[inline] 589 | #[deprecated(since = "0.3.0", note = "Use Size::from_gigabytes() instead")] 590 | /// Express a size in gigabytes. Actual size is 10^9 \* the value. 591 | pub fn Gigabytes(t: T) -> Self { 592 | Self::from_gigabytes(t) 593 | } 594 | #[inline] 595 | #[deprecated(since = "0.3.0", note = "Use Size::from_tebibytes() instead")] 596 | /// Express a size in tebibytes. Actual size is 2^40 \* the value. 597 | pub fn Tebibytes(t: T) -> Self { 598 | Self::from_tebibytes(t) 599 | } 600 | #[inline] 601 | #[deprecated(since = "0.3.0", note = "Use Size::from_terabytes() instead")] 602 | /// Express a size in terabytes. Actual size is 10^12 \* the value. 603 | pub fn Terabytes(t: T) -> Self { 604 | Self::from_terabytes(t) 605 | } 606 | #[inline] 607 | #[deprecated(since = "0.3.0", note = "Use Size::from_pebibytes() instead")] 608 | /// Express a size in pebibytes. Actual size is 2^50 \* the value. 609 | pub fn Pebibytes(t: T) -> Self { 610 | Self::from_pebibytes(t) 611 | } 612 | #[inline] 613 | #[deprecated(since = "0.3.0", note = "Use Size::from_petabytes() instead")] 614 | /// Express a size in petabytes. Actual size is 10^15 \* the value. 615 | pub fn Petabytes(t: T) -> Self { 616 | Self::from_petabytes(t) 617 | } 618 | #[inline] 619 | #[deprecated(since = "0.3.0", note = "Use Size::from_exbibytes() instead")] 620 | /// Express a size in exbibytes. Actual size is 2^60 \* the value. 621 | pub fn Exbibytes(t: T) -> Self { 622 | Self::from_exbibytes(t) 623 | } 624 | #[inline] 625 | #[deprecated(since = "0.3.0", note = "Use Size::from_exabytes() instead")] 626 | /// Express a size in exabytes. Actual size is 10^18 \* the value. 627 | pub fn Exabytes(t: T) -> Self { 628 | Self::from_exabytes(t) 629 | } 630 | 631 | #[inline] 632 | #[deprecated(since = "0.3.0", note = "Use Size::from_bytes() instead")] 633 | /// Express a size in bytes, as a shortcut for using [`Size::Bytes`]. 634 | pub fn B(t: T) -> Self { 635 | Self::from_bytes(t) 636 | } 637 | #[inline] 638 | #[deprecated(since = "0.3.0", note = "Use Size::from_kib() instead")] 639 | /// Express a size in kibibytes, as a shortcut for using [`Size::Kibibytes`]. 640 | pub fn KiB(t: T) -> Self { 641 | Self::from_kib(t) 642 | } 643 | #[inline] 644 | #[deprecated(since = "0.3.0", note = "Use Size::from_kb() instead")] 645 | /// Express a size in kilobytes, as a shortcut for using [`Size::Kilobytes`]. 646 | pub fn KB(t: T) -> Self { 647 | Self::from_kb(t) 648 | } 649 | #[inline] 650 | #[deprecated(since = "0.3.0", note = "Use Size::from_mib() instead")] 651 | /// Express a size in mebibytes, as a shortcut for using [`Size::Mebibytes`]. 652 | pub fn MiB(t: T) -> Self { 653 | Self::from_mib(t) 654 | } 655 | #[inline] 656 | #[deprecated(since = "0.3.0", note = "Use Size::from_mb() instead")] 657 | /// Express a size in megabytes, as a shortcut for using [`Size::Megabytes`]. 658 | pub fn MB(t: T) -> Self { 659 | Self::from_mb(t) 660 | } 661 | #[inline] 662 | #[deprecated(since = "0.3.0", note = "Use Size::from_gib() instead")] 663 | /// Express a size in gibibytes, as a shortcut for using [`Size::Gibibytes`]. 664 | pub fn GiB(t: T) -> Self { 665 | Self::from_gib(t) 666 | } 667 | #[inline] 668 | #[deprecated(since = "0.3.0", note = "Use Size::from_gb() instead")] 669 | /// Express a size in gigabytes, as a shortcut for using [`Size::Gigabytes`]. 670 | pub fn GB(t: T) -> Self { 671 | Self::from_gb(t) 672 | } 673 | #[inline] 674 | #[deprecated(since = "0.3.0", note = "Use Size::from_tib() instead")] 675 | /// Express a size in tebibytes, as a shortcut for using [`Size::Tebibytes`]. 676 | pub fn TiB(t: T) -> Self { 677 | Self::from_tib(t) 678 | } 679 | #[inline] 680 | #[deprecated(since = "0.3.0", note = "Use Size::from_tb() instead")] 681 | /// Express a size in terabytes, as a shortcut for using [`Size::Terabytes`]. 682 | pub fn TB(t: T) -> Self { 683 | Self::from_tb(t) 684 | } 685 | #[inline] 686 | #[deprecated(since = "0.3.0", note = "Use Size::from_pib() instead")] 687 | /// Express a size in pebibytes, as a shortcut for using [`Size::Pebibytes`]. 688 | pub fn PiB(t: T) -> Self { 689 | Self::from_pib(t) 690 | } 691 | #[inline] 692 | #[deprecated(since = "0.3.0", note = "Use Size::from_pb() instead")] 693 | /// Express a size in petabytes, as a shortcut for using [`Size::Petabytes`]. 694 | pub fn PB(t: T) -> Self { 695 | Self::from_pb(t) 696 | } 697 | #[inline] 698 | #[deprecated(since = "0.3.0", note = "Use Size::from_eib() instead")] 699 | /// Express a size in exbibytes, as a shortcut for using [`Size::Exbibytes`]. 700 | pub fn EiB(t: T) -> Self { 701 | Self::from_eib(t) 702 | } 703 | #[inline] 704 | #[deprecated(since = "0.3.0", note = "Use Size::from_eb() instead")] 705 | /// Express a size in exabytes, as a shortcut for using [`Size::Exabytes`]. 706 | pub fn EB(t: T) -> Self { 707 | Self::from_eb(t) 708 | } 709 | } 710 | 711 | impl core::fmt::Debug for Size { 712 | fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result { 713 | write!(fmt, "{} bytes", self.bytes()) 714 | } 715 | } 716 | -------------------------------------------------------------------------------- /src/ops.rs: -------------------------------------------------------------------------------- 1 | //! Implementations of basic arithmetic operations on/between `Size` values. 2 | //! 3 | //! Only operations that make sense are implemented, e.g. while it is OK to add two `Size` objects, 4 | //! it does not make sense to multiply them. Meanwhile, `17 MiB / 2` is perfectly rational and 5 | //! returns a size of `3.5 MiB`, but `12 KB + 14` isn't (as the addition of a scalar value to a 6 | //! sized type is undefined) -- on the other hand, `12 KB + 14 B` is both supported and perfectly 7 | //! fine. 8 | //! 9 | //! Some examples of supported mathematical operations: 10 | #![cfg_attr(not(feature = "std"), doc = "```ignore")] 11 | #![cfg_attr(feature = "std", doc = "```")] 12 | //! use size::Size; 13 | //! 14 | //! // Perform scalar multiplication/division on a `Size` 15 | //! let s1 = Size::from_mib(13) / 2; 16 | //! assert_eq!(s1, Size::from_mib(6.5_f32)); 17 | //! 18 | //! // Perform addition or subtraction of two `Size` instances, 19 | //! // regardless of their underlying types 20 | //! let s2 = Size::from_kib(4) + Size::from_mb(8); 21 | //! assert_eq!(s2.bytes(), 8_004_096); 22 | //! 23 | //! // Express the negative difference between two sizes 24 | //! let s3 = Size::from_mib(12) - Size::from_mib(14.2_f64); 25 | //! assert_eq!(s3, Size::from_kib(-2252.8)); 26 | //! ``` 27 | //! 28 | //! Some other things you cannot do are multiply/divide two sizes (did you mean to multiply one size 29 | //! by a scalar value instead?), add/subtract scalar values from sizes (you can call `size.bytes()` 30 | //! then do all the scalar math you like, however), or perform mathematical operations that exceed 31 | //! the bounds of the intermediate type (`f64` by default or `i64` if `no_std` mode is used). 32 | //! 33 | //! A current limitation of this crate that may be revisited at a later date is that mathematical 34 | //! operations (or textual representation, for that matter) of that result in a size that exceeds 35 | //! the bounds of an `i64` are not supported (i.e. they will not be promoted to a 36 | //! floating-point-backed `Size` instance) and will panic in debug mode or silently fail with 37 | //! undefined results in release mode. 38 | 39 | use crate::{AsIntermediate, Intermediate, Size}; 40 | use core::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign}; 41 | use core::iter::Sum; 42 | 43 | impl Add for Size { 44 | type Output = Size; 45 | 46 | fn add(self, other: Size) -> Self::Output { 47 | Size::from_bytes(self.bytes() + other.bytes()) 48 | } 49 | } 50 | 51 | impl Add for &Size { 52 | type Output = Size; 53 | 54 | fn add(self, other: Size) -> Self::Output { 55 | Size::from_bytes(self.bytes() + other.bytes()) 56 | } 57 | } 58 | 59 | impl Add<&Size> for Size { 60 | type Output = Size; 61 | 62 | fn add(self, other: &Size) -> Self::Output { 63 | Size::from_bytes(self.bytes() + other.bytes()) 64 | } 65 | } 66 | 67 | impl Add<&Size> for &Size { 68 | type Output = Size; 69 | 70 | fn add(self, other: &Size) -> Self::Output { 71 | Size::from_bytes(self.bytes() + other.bytes()) 72 | } 73 | } 74 | 75 | impl Sub for Size { 76 | type Output = Size; 77 | 78 | fn sub(self, other: Size) -> Self::Output { 79 | Size::from_bytes(self.bytes() - other.bytes()) 80 | } 81 | } 82 | 83 | impl Sub for &Size { 84 | type Output = Size; 85 | 86 | fn sub(self, other: Size) -> Self::Output { 87 | Size::from_bytes(self.bytes() - other.bytes()) 88 | } 89 | } 90 | 91 | impl Sub<&Size> for Size { 92 | type Output = Size; 93 | 94 | fn sub(self, other: &Size) -> Self::Output { 95 | Size::from_bytes(self.bytes() - other.bytes()) 96 | } 97 | } 98 | 99 | impl Sub<&Size> for &Size { 100 | type Output = Size; 101 | 102 | fn sub(self, other: &Size) -> Self::Output { 103 | Size::from_bytes(self.bytes() - other.bytes()) 104 | } 105 | } 106 | 107 | impl Sum for Size { 108 | fn sum>(iter: I) -> Self { 109 | iter.fold(Self::ZERO, |total, size| total + size) 110 | } 111 | } 112 | 113 | impl<'a> Sum<&'a Size> for Size { 114 | fn sum>(iter: I) -> Self { 115 | iter.fold(Self::ZERO, |total, size| total + size) 116 | } 117 | } 118 | 119 | impl Mul for Size 120 | where 121 | T: AsIntermediate, 122 | { 123 | type Output = Size; 124 | 125 | fn mul(self, other: T) -> Self::Output { 126 | Size::from_bytes((self.bytes() as Intermediate * other.as_()) as i64) 127 | } 128 | } 129 | 130 | impl Mul for &Size 131 | where 132 | T: AsIntermediate, 133 | { 134 | type Output = Size; 135 | 136 | fn mul(self, other: T) -> Self::Output { 137 | Size::from_bytes((self.bytes() as Intermediate * other.as_()) as i64) 138 | } 139 | } 140 | 141 | macro_rules! impl_mul { 142 | ($type:ty) => { 143 | impl Mul for $type { 144 | type Output = Size; 145 | 146 | fn mul(self, other: Size) -> Self::Output { 147 | Size::from_bytes((self.as_() * other.bytes() as Intermediate) as i64) 148 | } 149 | } 150 | 151 | impl Mul<&Size> for $type { 152 | type Output = Size; 153 | 154 | fn mul(self, other: &Size) -> Self::Output { 155 | Size::from_bytes((self.as_() * other.bytes() as Intermediate) as i64) 156 | } 157 | } 158 | }; 159 | } 160 | 161 | impl_mul!(i64); 162 | #[cfg(feature = "std")] 163 | impl_mul!(f64); 164 | 165 | impl Div for Size 166 | where 167 | T: AsIntermediate, 168 | { 169 | type Output = Size; 170 | 171 | fn div(self, other: T) -> Self::Output { 172 | Size::from_bytes((self.bytes() as Intermediate / other.as_()) as i64) 173 | } 174 | } 175 | 176 | impl Div for &Size 177 | where 178 | T: AsIntermediate, 179 | { 180 | type Output = Size; 181 | 182 | fn div(self, other: T) -> Self::Output { 183 | Size::from_bytes((self.bytes() as Intermediate / other.as_()) as i64) 184 | } 185 | } 186 | 187 | /* XxxAssign impls, only for `Size` and not for `&Size` */ 188 | 189 | impl AddAssign for Size { 190 | fn add_assign(&mut self, other: Size) { 191 | *self = *self + other; 192 | } 193 | } 194 | 195 | impl AddAssign<&Size> for Size { 196 | fn add_assign(&mut self, other: &Size) { 197 | *self = *self + other; 198 | } 199 | } 200 | 201 | impl SubAssign for Size { 202 | fn sub_assign(&mut self, other: Size) { 203 | *self = *self - other; 204 | } 205 | } 206 | 207 | impl SubAssign<&Size> for Size { 208 | fn sub_assign(&mut self, other: &Size) { 209 | *self = *self - other; 210 | } 211 | } 212 | 213 | impl MulAssign for Size 214 | where 215 | T: AsIntermediate, 216 | { 217 | fn mul_assign(&mut self, other: T) { 218 | *self = *self * other; 219 | } 220 | } 221 | 222 | impl DivAssign for Size 223 | where 224 | T: AsIntermediate, 225 | { 226 | fn div_assign(&mut self, other: T) { 227 | *self = *self / other; 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/serde.rs: -------------------------------------------------------------------------------- 1 | use crate::Size; 2 | use serde::de; 3 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 4 | use std::fmt; 5 | 6 | struct SizeVisitor; 7 | 8 | impl<'de> de::Visitor<'de> for SizeVisitor { 9 | type Value = Size; 10 | 11 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 12 | formatter.write_str("an integer or a floating point number representing size in bytes") 13 | } 14 | 15 | fn visit_i64(self, value: i64) -> Result 16 | where 17 | E: de::Error, 18 | { 19 | Ok(Size { bytes: value }) 20 | } 21 | 22 | fn visit_u64(self, value: u64) -> Result 23 | where 24 | E: de::Error, 25 | { 26 | if value > i64::MAX as u64 { 27 | Err(E::custom(format!("u64 size {} is out of range", value))) 28 | } else { 29 | Ok(Size { 30 | bytes: value as i64, 31 | }) 32 | } 33 | } 34 | 35 | fn visit_f32(self, value: f32) -> Result 36 | where 37 | E: de::Error, 38 | { 39 | if value.is_infinite() || value > i64::MAX as f32 || value < i64::MIN as f32 { 40 | Err(E::custom(format!("f32 size {} is out of range", value))) 41 | } else { 42 | Ok(Size { 43 | bytes: value as i64, 44 | }) 45 | } 46 | } 47 | 48 | fn visit_f64(self, value: f64) -> Result 49 | where 50 | E: de::Error, 51 | { 52 | if value.is_infinite() || value > i64::MAX as f64 || value < i64::MIN as f64 { 53 | Err(E::custom(format!("f64 size {} is out of range", value))) 54 | } else { 55 | Ok(Size { 56 | bytes: value as i64, 57 | }) 58 | } 59 | } 60 | 61 | fn visit_str(self, value: &str) -> Result 62 | where 63 | E: de::Error, 64 | { 65 | Size::from_str(value).map_err(|_| E::custom(format!("Invalid size: \"{value}\""))) 66 | } 67 | } 68 | 69 | impl Serialize for Size { 70 | fn serialize(&self, serializer: S) -> Result 71 | where 72 | S: Serializer, 73 | { 74 | serializer.serialize_i64(self.bytes) 75 | } 76 | } 77 | 78 | impl<'de> Deserialize<'de> for Size { 79 | fn deserialize(deserializer: D) -> Result 80 | where 81 | D: Deserializer<'de>, 82 | { 83 | deserializer.deserialize_any(SizeVisitor) 84 | } 85 | } 86 | 87 | #[test] 88 | /// Assert that [`Size`] serializes to its inner value directly 89 | fn test_serialize() { 90 | use serde::{Deserialize, Serialize}; 91 | 92 | #[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] 93 | struct Foo { 94 | size: Size, 95 | } 96 | 97 | let foo = Foo { 98 | size: Size::from_bytes(1024), 99 | }; 100 | let json = serde_json::to_string(&foo); 101 | assert_eq!(json.as_ref().unwrap(), &r#"{"size":1024}"#.to_string()); 102 | } 103 | 104 | #[test] 105 | fn test_deserialize_i64() { 106 | use serde::{Deserialize, Serialize}; 107 | 108 | #[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] 109 | struct Foo { 110 | size: Size, 111 | } 112 | 113 | let json = r#"{"size": 42}"#; 114 | let foo: Foo = serde_json::from_str(json).unwrap(); 115 | assert_eq!( 116 | foo, 117 | Foo { 118 | size: Size::from_bytes(42) 119 | } 120 | ); 121 | } 122 | 123 | #[test] 124 | fn test_deserialize_f64() { 125 | use serde::{Deserialize, Serialize}; 126 | 127 | #[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] 128 | struct Foo { 129 | size: Size, 130 | } 131 | 132 | let json = r#"{"size": 42.00}"#; 133 | let foo: Foo = serde_json::from_str(json).unwrap(); 134 | assert_eq!( 135 | foo, 136 | Foo { 137 | size: Size::from_bytes(42) 138 | } 139 | ); 140 | } 141 | 142 | #[test] 143 | fn test_deserialize_overflow() { 144 | use serde::{Deserialize, Serialize}; 145 | 146 | #[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] 147 | struct Foo { 148 | size: Size, 149 | } 150 | 151 | let json = r#"{"size": 2.99792458e118}"#; 152 | let foo: Result = serde_json::from_str(json); 153 | assert!(foo.is_err()); 154 | let msg = foo.unwrap_err().to_string(); 155 | assert!(msg.contains("out of range")); 156 | } 157 | -------------------------------------------------------------------------------- /src/tests.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "std")] 2 | #![allow(deprecated)] 3 | 4 | use crate::Size; 5 | 6 | #[test] 7 | fn unit_tests() { 8 | assert_eq!("200 bytes", format!("{}", Size::from_bytes(200))); 9 | assert_eq!("200 KiB", format!("{}", Size::from_kibibytes(200))); 10 | assert_eq!("2.00 MiB", format!("{}", Size::from_kibibytes(2048))); 11 | } 12 | 13 | #[test] 14 | fn negative_tests() { 15 | assert_eq!("-200 bytes", format!("{}", Size::from_bytes(-200))); 16 | assert_eq!("-200 KiB", format!("{}", Size::from_kibibytes(-200))); 17 | assert_eq!("-2.00 MiB", format!("{}", Size::from_kibibytes(-2048))); 18 | } 19 | 20 | #[test] 21 | fn integral_limits() { 22 | assert_eq!("8 EiB", format!("{}", Size::from_bytes(i64::MAX))); 23 | assert_eq!("-8 EiB", format!("{}", Size::from_bytes(i64::MIN))); 24 | 25 | assert_eq!("8 EiB", format!("{}", Size::from_kib(u64::MAX))); 26 | assert_eq!("0 bytes", format!("{}", Size::from_kib(u64::MIN))); 27 | 28 | // Also test for the old-style API, which does no math at the point of creation 29 | assert_eq!("8 EiB", format!("{}", Size::Bytes(u64::MAX))); 30 | assert_eq!("0 bytes", format!("{}", Size::Bytes(u64::MIN))); 31 | } 32 | 33 | #[test] 34 | fn float_limits() { 35 | assert_eq!("8 EiB", format!("{}", Size::from_kib(f64::MAX))); 36 | assert_eq!("-8 EiB", format!("{}", Size::from_kib(f64::MIN))); 37 | 38 | // Also test for the old-style API, which does no math at the point of creation 39 | assert_eq!("8 EiB", format!("{}", Size::Bytes(f64::MAX))); 40 | assert_eq!("-8 EiB", format!("{}", Size::Bytes(f64::MIN))); 41 | } 42 | 43 | #[test] 44 | /// Make sure invalid floats don't panic. The *actual* result is officially undefined by this 45 | /// crate's API contract. 46 | fn invalid_floats() { 47 | assert_eq!("0 bytes", format!("{}", Size::from_kib(f64::NAN))); 48 | assert_eq!("8 EiB", format!("{}", Size::from_kib(f64::INFINITY))); 49 | assert_eq!("-8 EiB", format!("{}", Size::from_kib(f64::NEG_INFINITY))); 50 | 51 | // Also test for the old-style API, which does no math at the point of creation 52 | assert_eq!("0 bytes", format!("{}", Size::Bytes(f64::NAN))); 53 | assert_eq!("8 EiB", format!("{}", Size::Bytes(f64::INFINITY))); 54 | assert_eq!("-8 EiB", format!("{}", Size::Bytes(f64::NEG_INFINITY))); 55 | } 56 | 57 | #[test] 58 | fn size_equality() { 59 | assert_eq!( 60 | Size::from_bytes(200), 61 | Size::from_bytes(200), 62 | "Testing equality of two identically-constructed sizes" 63 | ); 64 | assert_eq!( 65 | Size::from_mib(2), 66 | Size::from_kib(2048), 67 | "Testing equality of two identical sizes expressed in different units" 68 | ); 69 | assert_eq!( 70 | Size::from_mib(2u8), 71 | Size::from_mib(2f64), 72 | "Testing equality of two identical sizes expressed in different types" 73 | ); 74 | assert_eq!( 75 | Size::from_mib(2u8), 76 | Size::from_kib(2048), 77 | "Testing equality of two identical sizes expressed in different types" 78 | ); 79 | assert_eq!( 80 | &Size::from_bytes(2097), 81 | &Size::from_kib(2.048), 82 | "Testing equality of two Size references" 83 | ); 84 | } 85 | 86 | #[test] 87 | fn size_cmp() { 88 | // Use legacy/backwards-compatible syntax: 89 | assert!(Size::Bytes(1) > Size::Bytes(0), "Comparison of two Size types directly"); 90 | assert!( 91 | &Size::KiB(1) >= &Size::KB(1), 92 | "Comparison of two Size types via their references" 93 | ); 94 | } 95 | 96 | #[test] 97 | fn size_addition() { 98 | // as a reference... 99 | let size = &Size::from_mib(20) + &Size::from_mib(22); 100 | assert_eq!(size, Size::Mebibytes(42)); 101 | 102 | // and not as a reference 103 | let size = Size::from_mib(20) + Size::from_mib(22_f64); 104 | assert_eq!(size, Size::Mebibytes(42)); 105 | } 106 | 107 | #[test] 108 | fn size_subtraction() { 109 | let size = &Size::from_mib(20) - &Size::from_mib(22); 110 | assert_eq!(size, Size::Mebibytes(-2)); 111 | 112 | let size = Size::from_mib(20) - Size::from_mib(22_f64); 113 | assert_eq!(size, Size::Mebibytes(-2)); 114 | } 115 | 116 | #[test] 117 | fn size_summation() { 118 | let sizes = vec![Size::from_mib(20), Size::from_mib(22)]; 119 | 120 | // sum references 121 | assert_eq!(sizes.iter().sum::(), Size::Mebibytes(42)); 122 | 123 | // sum owned values 124 | assert_eq!(sizes.into_iter().sum::(), Size::Mebibytes(42)); 125 | } 126 | 127 | #[test] 128 | fn primitive_multiplication() { 129 | let size = &Size::from_gb(12) * 7; 130 | assert_eq!(size.bytes(), 84000000000); 131 | let size = Size::from_gb(12) * 7; 132 | assert_eq!(size.bytes(), 84000000000); 133 | 134 | // and the other way around 135 | let size = 7 * Size::from_gb(12); 136 | assert_eq!(size.bytes(), 84000000000); 137 | 138 | // and with other types 139 | let size = &Size::from_gb(12) * 7.0; 140 | assert_eq!(size.bytes(), 84000000000); 141 | let size = 7.0 * &Size::from_gb(12); 142 | assert_eq!(size.bytes(), 84000000000); 143 | } 144 | 145 | #[test] 146 | fn primitive_division() { 147 | let size = &Size::from_gb(12) / 13f64; 148 | assert_eq!(size.bytes(), 923076923); 149 | 150 | let size = Size::from_gb(12.0) / 13; 151 | assert_eq!(size.bytes(), 923076923); 152 | } 153 | 154 | /// Floats that cannot be expressed as an `i64` may be instantiated, but give undefined results 155 | /// when operated on. 156 | #[test] 157 | fn nan_size() { 158 | let size = Size::from_kib(f32::NAN); 159 | let _ = size + Size::from_bytes(1); 160 | let _ = format!("{}", size); 161 | } 162 | 163 | /// Floats that cannot be expressed as an `i64` may be instantiated, but give undefined results 164 | /// when operated on. The code below panics in debug mode but continues with undefined results in 165 | /// release mode. 166 | #[test] 167 | fn overflow_size() { 168 | use std::panic; 169 | 170 | // This value is well out of the range of an i64, but is a perfectly valid floating point value. 171 | let result = panic::catch_unwind(|| { 172 | let _ = Size::from_kb(7.3E200_f64) + Size::from_kib(2); 173 | }); 174 | 175 | if cfg!(debug_assertions) { 176 | assert!(result.is_err()); 177 | } else { 178 | assert!(result.is_ok()); 179 | } 180 | } 181 | 182 | #[test] 183 | fn size_div_assign_f64() { 184 | let mut size = Size::from_gb(12); 185 | size /= 13f64; 186 | assert_eq!(size.bytes(), 923076923); 187 | } 188 | -------------------------------------------------------------------------------- /src/tests_nostd.rs: -------------------------------------------------------------------------------- 1 | #![allow(deprecated)] 2 | 3 | use crate::Size; 4 | 5 | #[test] 6 | fn nostd_add() { 7 | let s1 = Size::from_kib(12); 8 | let s2 = Size::from_kib(24); 9 | let sum = s1 + s2; 10 | assert_eq!(sum.bytes(), Size::KiB(36).bytes()); 11 | } 12 | 13 | #[test] 14 | fn nostd_sub() { 15 | let s1 = Size::from_kib(24_i32); 16 | let s2 = Size::from_kib(12_i64); 17 | let sum = s1 - s2; 18 | assert_eq!(sum.bytes(), Size::KiB(12).bytes()); 19 | } 20 | 21 | #[test] 22 | fn nostd_neg_sub() { 23 | let s1 = Size::from_kib(12_u64); 24 | let s2 = Size::from_kib(24_i64); 25 | let sum = s1 - s2; 26 | assert_eq!(sum.bytes(), Size::from_kib(-12).bytes()); 27 | } 28 | 29 | #[test] 30 | fn nostd_bytes() { 31 | let s1 = Size::from_kib(36); 32 | let s2 = Size::from_bytes(36 << 10); 33 | assert_eq!(s1.bytes(), s2.bytes()); 34 | assert_eq!(s1.bytes(), 36 << 10); 35 | } 36 | 37 | #[test] 38 | fn nostd_integral_limits() { 39 | // Test the old-style API, which does no math at the point of creation 40 | assert_eq!(Size::from_bytes(i64::MAX), Size::Bytes(u64::MAX)); 41 | assert_eq!(Size::from_bytes(0), Size::Bytes(u64::MIN)); 42 | assert_eq!(Size::from_bytes(i64::MAX), Size::Bytes(u64::MAX - 1)); 43 | } 44 | 45 | #[test] 46 | fn nostd_add_assign() { 47 | let mut size = Size::from_mib(20); 48 | size += Size::from_mib(22); 49 | assert_eq!(size, Size::Mebibytes(42)); 50 | } 51 | 52 | #[test] 53 | fn nostd_mul_assign() { 54 | let mut size = Size::from_gb(12); 55 | size *= 7; 56 | assert_eq!(size.bytes(), 84000000000); 57 | } 58 | 59 | #[test] 60 | fn nostd_sub_assign() { 61 | let mut s1 = Size::from_kib(24_i32); 62 | let s2 = Size::from_kib(12_i64); 63 | s1 -= s2; 64 | assert_eq!(s1.bytes(), Size::KiB(12).bytes()); 65 | } 66 | 67 | #[test] 68 | fn nostd_neg_sub_assign() { 69 | let mut s1 = Size::from_kib(12_u64); 70 | let s2 = Size::from_kib(24_i64); 71 | s1 -= s2; 72 | assert_eq!(s1.bytes(), Size::from_kib(-12).bytes()); 73 | } 74 | --------------------------------------------------------------------------------