humansize/
lib.rs

1//! # **Humansize**
2//!
3//! Humansize lets you easily represent file sizes in a human-friendly format.
4//! You can specify your own formatting style, pick among the three defaults provided
5//! by the library:
6//!
7//! * Decimal (Multiples of 1000, `KB` units)
8//! * Binary (Multiples of 1024, `KiB` units)
9//! * Conventional (Multiples of 1024, `KB` units)
10//!
11//! ## How to use it
12//!
13//! Simply import the `FileSize` trait and the options module and call the
14//! file_size method on any positive integer, using one of the three standards
15//! provided by the options module.
16//!
17//! ```rust
18//! extern crate humansize;
19//! use humansize::{FileSize, file_size_opts as options};
20//!
21//! fn main() {
22//! 	let size = 1000;
23//! 	println!("Size is {}", size.file_size(options::DECIMAL).unwrap());
24//!
25//! 	println!("Size is {}", size.file_size(options::BINARY).unwrap());
26//!
27//! 	println!("Size is {}", size.file_size(options::CONVENTIONAL).unwrap());
28//! }
29//! ```
30//!
31//! If you wish to customize the way sizes are displayed, you may create your own custom `FileSizeOpts` struct
32//! and pass that to the method. See the `custom_options.rs` file in the example folder.
33
34static SCALE_DECIMAL: [&str; 9] = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
35static SCALE_DECIMAL_LONG: [&str; 9] = [
36    "Bytes",
37    "Kilobytes",
38    "Megabytes",
39    "Gigabytes",
40    "Terabytes",
41    "Petabytes",
42    "Exabytes",
43    "Zettabytes",
44    "Yottabytes",
45];
46
47static SCALE_BINARY: [&str; 9] = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
48static SCALE_BINARY_LONG: [&str; 9] = [
49    "Bytes",
50    "Kibibytes",
51    "Mebibytes",
52    "Gibibytes",
53    "Tebibytes",
54    "Pebibytes",
55    "Exbibytes",
56    "Zebibytes",
57    "Yobibytes",
58];
59
60pub mod file_size_opts {
61    //! Describes the struct that holds the options needed by the `file_size` method.
62    //! The three most common formats are provided as constants to be used easily
63
64    #[derive(Debug, PartialEq, Copy, Clone)]
65    /// Holds the standard to use when displying the size.
66    pub enum Kilo {
67        /// The decimal scale and units
68        Decimal,
69        /// The binary scale and units
70        Binary,
71    }
72
73    #[derive(Debug, Copy, Clone)]
74    /// Forces a certain representation of the resulting file size.
75    pub enum FixedAt {
76        Byte,
77        Kilo,
78        Mega,
79        Giga,
80        Tera,
81        Peta,
82        Exa,
83        Zetta,
84        Yotta,
85        No,
86    }
87
88    /// Holds the options for the `file_size` method.
89    #[derive(Debug)]
90    pub struct FileSizeOpts {
91        /// The scale (binary/decimal) to divide against.
92        pub divider: Kilo,
93        /// The unit set to display.
94        pub units: Kilo,
95        /// The amount of decimal places to display if the decimal part is non-zero.
96        pub decimal_places: usize,
97        /// The amount of zeroes to display if the decimal part is zero.
98        pub decimal_zeroes: usize,
99        /// Whether to force a certain representation and if so, which one.
100        pub fixed_at: FixedAt,
101        /// Whether to use the full suffix or its abbreveation.
102        pub long_units: bool,
103        /// Whether to place a space between value and units.
104        pub space: bool,
105        /// An optional suffix which will be appended after the unit.
106        pub suffix: &'static str,
107        /// Whether to allow negative numbers as input. If `False`, negative values will return an error.
108        pub allow_negative: bool,
109    }
110
111    impl AsRef<FileSizeOpts> for FileSizeOpts {
112        fn as_ref(&self) -> &FileSizeOpts {
113            self
114        }
115    }
116
117    /// Options to display sizes in the binary format.
118    pub const BINARY: FileSizeOpts = FileSizeOpts {
119        divider: Kilo::Binary,
120        units: Kilo::Binary,
121        decimal_places: 2,
122        decimal_zeroes: 0,
123        fixed_at: FixedAt::No,
124        long_units: false,
125        space: true,
126        suffix: "",
127        allow_negative: false,
128    };
129
130    /// Options to display sizes in the decimal format.
131    pub const DECIMAL: FileSizeOpts = FileSizeOpts {
132        divider: Kilo::Decimal,
133        units: Kilo::Decimal,
134        decimal_places: 2,
135        decimal_zeroes: 0,
136        fixed_at: FixedAt::No,
137        long_units: false,
138        space: true,
139        suffix: "",
140        allow_negative: false,
141    };
142
143    /// Options to display sizes in the "conventional" format.
144    /// This 1024 as the value of the `Kilo`, but displays decimal-style units (`KB`, not `KiB`).
145    pub const CONVENTIONAL: FileSizeOpts = FileSizeOpts {
146        divider: Kilo::Binary,
147        units: Kilo::Decimal,
148        decimal_places: 2,
149        decimal_zeroes: 0,
150        fixed_at: FixedAt::No,
151        long_units: false,
152        space: true,
153        suffix: "",
154        allow_negative: false,
155    };
156}
157/// The trait for the `file_size`method
158pub trait FileSize {
159    /// Formats self according to the parameters in `opts`. `opts` can either be one of the
160    /// three defaults providedby the `file_size_opts` module, or be custom-defined according
161    /// to your needs
162    ///
163    /// # Errors
164    /// Will fail by default if called on a negative number. Override this behavior by setting
165    /// `allow_negative` to `True` in a custom options struct.
166    ///
167    /// # Examples
168    /// ```rust
169    /// use humansize::{FileSize, file_size_opts as options};
170    ///
171    /// let size = 5128;
172    /// println!("Size is {}", size.file_size(options::DECIMAL).unwrap());
173    /// ```
174    ///
175    fn file_size<T: AsRef<FileSizeOpts>>(&self, opts: T) -> Result<String, String>;
176}
177
178fn f64_eq(left: f64, right: f64) -> bool {
179    left == right || (left - right).abs() <= std::f64::EPSILON
180}
181
182use self::file_size_opts::*;
183
184macro_rules! impl_file_size_u {
185    (for $($t:ty)*) => ($(
186        impl FileSize for $t {
187            fn file_size<T: AsRef<FileSizeOpts>>(&self, _opts: T) -> Result<String, String> {
188                let opts = _opts.as_ref();
189                let divider = match opts.divider {
190                    Kilo::Decimal => 1000.0,
191                    Kilo::Binary => 1024.0
192                };
193
194                let mut size: f64 = *self as f64;
195                let mut scale_idx = 0;
196
197                match opts.fixed_at {
198                    FixedAt::No => {
199                        while size >= divider {
200                            size /= divider;
201                            scale_idx += 1;
202                        }
203                    }
204                    val => {
205                        while scale_idx != val as usize {
206                            size /= divider;
207                            scale_idx += 1;
208                        }
209                    }
210                }
211
212                let mut scale = match (opts.units, opts.long_units) {
213                    (Kilo::Decimal, false) => SCALE_DECIMAL[scale_idx],
214                    (Kilo::Decimal, true) => SCALE_DECIMAL_LONG[scale_idx],
215                    (Kilo::Binary, false) => SCALE_BINARY[scale_idx],
216                    (Kilo::Binary, true) => SCALE_BINARY_LONG[scale_idx]
217                };
218
219                // Remove "s" from the scale if the size is 1.x
220                if opts.long_units && f64_eq(size.trunc(), 1.0) { scale = &scale[0 .. scale.len()-1]; }
221
222                let places = if f64_eq(size.fract(), 0.0) {
223                    opts.decimal_zeroes
224                } else {
225                    opts.decimal_places
226                };
227
228                let space = if opts.space {" "} else {""};
229
230                Ok(format!("{:.*}{}{}{}", places, size, space, scale, opts.suffix))
231            }
232        }
233    )*)
234}
235
236macro_rules! impl_file_size_i {
237    (for $($t:ty)*) => ($(
238        impl FileSize for $t {
239            fn file_size<T: AsRef<FileSizeOpts>>(&self, _opts: T) -> Result<String, String> {
240                let opts = _opts.as_ref();
241                if *self < 0 && !opts.allow_negative {
242                    return Err("Tried calling file_size on a negative value".to_owned());
243                } else {
244                    let sign = if *self < 0 {
245                        "-"
246                    } else {
247                        ""
248                    };
249
250                    Ok(format!("{}{}", sign, (self.abs() as u64).file_size(opts)?))
251                }
252
253            }
254        }
255    )*)
256}
257
258impl_file_size_u!(for usize u8 u16 u32 u64);
259impl_file_size_i!(for isize i8 i16 i32 i64);
260
261#[test]
262fn test_sizes() {
263    assert_eq!(0.file_size(BINARY).unwrap(), "0 B");
264    assert_eq!(999.file_size(BINARY).unwrap(), "999 B");
265    assert_eq!(1000.file_size(BINARY).unwrap(), "1000 B");
266    assert_eq!(1000.file_size(DECIMAL).unwrap(), "1 KB");
267    assert_eq!(1023.file_size(BINARY).unwrap(), "1023 B");
268    assert_eq!(1023.file_size(DECIMAL).unwrap(), "1.02 KB");
269    assert_eq!(1024.file_size(BINARY).unwrap(), "1 KiB");
270    assert_eq!(1024.file_size(CONVENTIONAL).unwrap(), "1 KB");
271
272    let semi_custom_options = file_size_opts::FileSizeOpts {
273        space: false,
274        ..file_size_opts::DECIMAL
275    };
276    assert_eq!(1000.file_size(semi_custom_options).unwrap(), "1KB");
277
278    let semi_custom_options2 = file_size_opts::FileSizeOpts {
279        suffix: "/s",
280        ..file_size_opts::BINARY
281    };
282    assert_eq!(999.file_size(semi_custom_options2).unwrap(), "999 B/s");
283
284    let semi_custom_options3 = file_size_opts::FileSizeOpts {
285        suffix: "/day",
286        space: false,
287        ..file_size_opts::DECIMAL
288    };
289    assert_eq!(1000.file_size(semi_custom_options3).unwrap(), "1KB/day");
290
291    let semi_custom_options4 = file_size_opts::FileSizeOpts {
292        fixed_at: file_size_opts::FixedAt::Byte,
293        ..file_size_opts::BINARY
294    };
295    assert_eq!(2048.file_size(semi_custom_options4).unwrap(), "2048 B");
296
297    let semi_custom_options5 = file_size_opts::FileSizeOpts {
298        fixed_at: file_size_opts::FixedAt::Kilo,
299        ..file_size_opts::BINARY
300    };
301    assert_eq!(
302        16584975.file_size(semi_custom_options5).unwrap(),
303        "16196.26 KiB"
304    );
305
306    let semi_custom_options6 = file_size_opts::FileSizeOpts {
307        fixed_at: file_size_opts::FixedAt::Tera,
308        decimal_places: 10,
309        ..file_size_opts::BINARY
310    };
311    assert_eq!(
312        15284975.file_size(semi_custom_options6).unwrap(),
313        "0.0000139016 TiB"
314    );
315
316    let semi_custom_options7 = file_size_opts::FileSizeOpts {
317        allow_negative: true,
318        ..file_size_opts::DECIMAL
319    };
320    assert_eq!(
321        (-5500).file_size(&semi_custom_options7).unwrap(),
322        "-5.50 KB"
323    );
324    assert_eq!((5500).file_size(&semi_custom_options7).unwrap(), "5.50 KB");
325}