1static 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 #[derive(Debug, PartialEq, Copy, Clone)]
65 pub enum Kilo {
67 Decimal,
69 Binary,
71 }
72
73 #[derive(Debug, Copy, Clone)]
74 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 #[derive(Debug)]
90 pub struct FileSizeOpts {
91 pub divider: Kilo,
93 pub units: Kilo,
95 pub decimal_places: usize,
97 pub decimal_zeroes: usize,
99 pub fixed_at: FixedAt,
101 pub long_units: bool,
103 pub space: bool,
105 pub suffix: &'static str,
107 pub allow_negative: bool,
109 }
110
111 impl AsRef<FileSizeOpts> for FileSizeOpts {
112 fn as_ref(&self) -> &FileSizeOpts {
113 self
114 }
115 }
116
117 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 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 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}
157pub trait FileSize {
159 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 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}