1use core::{fmt, write};
2
3use crate::ByteSize;
4
5const BITS_PER_BYTE: u64 = 8;
6
7#[derive(Debug, Clone, Copy)]
9pub(crate) enum Format {
10 Iec,
11 IecShort,
12 Si,
13 SiShort,
14 IecBits,
15 SiBits,
16}
17
18impl Format {
19 fn unit(self) -> u64 {
20 match self {
21 Format::Iec | Format::IecShort => crate::KIB,
22 Format::Si | Format::SiShort => crate::KB,
23 Format::IecBits => crate::KIB,
24 Format::SiBits => crate::KB,
25 }
26 }
27
28 fn unit_base(self) -> f64 {
29 match self {
30 Format::Iec | Format::IecShort => crate::LN_KIB,
31 Format::Si | Format::SiShort => crate::LN_KB,
32 Format::IecBits => crate::LN_KIB,
33 Format::SiBits => crate::LN_KB,
34 }
35 }
36
37 fn unit_prefixes(self) -> &'static [u8] {
38 match self {
39 Format::Iec | Format::IecShort | Format::IecBits => crate::UNITS_IEC.as_bytes(),
40 Format::Si | Format::SiShort | Self::SiBits => crate::UNITS_SI.as_bytes(),
41 }
42 }
43
44 fn unit_separator(self) -> &'static str {
45 match self {
46 Format::Iec | Format::Si | Format::IecBits | Format::SiBits => " ",
47 Format::IecShort | Format::SiShort => "",
48 }
49 }
50
51 fn unit_suffix(self) -> &'static str {
52 match self {
53 Format::Iec => "iB",
54 Format::Si => "B",
55 Format::IecShort | Format::SiShort => "",
56 Format::IecBits => "ib",
57 Format::SiBits => "b",
58 }
59 }
60
61 fn is_bits(self) -> bool {
62 matches!(self, Format::IecBits | Format::SiBits)
63 }
64}
65
66#[derive(Debug, Clone)]
85pub struct Display {
86 pub(crate) byte_size: ByteSize,
87 pub(crate) format: Format,
88}
89
90impl Display {
91 #[must_use]
95 #[doc(alias = "binary")]
96 pub fn iec(mut self) -> Self {
97 self.format = Format::Iec;
98 self
99 }
100
101 #[must_use]
107 #[doc(alias = "binary")]
108 pub fn iec_short(mut self) -> Self {
109 self.format = Format::IecShort;
110 self
111 }
112
113 #[must_use]
117 #[doc(alias = "decimal")]
118 pub fn si(mut self) -> Self {
119 self.format = Format::Si;
120 self
121 }
122
123 #[must_use]
127 #[doc(alias = "decimal")]
128 pub fn si_short(mut self) -> Self {
129 self.format = Format::SiShort;
130 self
131 }
132
133 #[must_use]
137 pub fn iec_bits(mut self) -> Self {
138 self.format = Format::IecBits;
139 self
140 }
141
142 #[must_use]
146 pub fn si_bits(mut self) -> Self {
147 self.format = Format::SiBits;
148 self
149 }
150}
151
152impl fmt::Display for Display {
153 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154 let bytes = self.byte_size.as_u64();
155
156 let is_bits = self.format.is_bits();
157
158 let unit = self.format.unit();
159 #[allow(unused_variables)] let unit_base = self.format.unit_base();
161
162 let unit_prefixes = self.format.unit_prefixes();
163 let unit_separator = self.format.unit_separator();
164 let unit_suffix = self.format.unit_suffix();
165 let precision = f.precision().unwrap_or(1);
166
167 let threshold = if is_bits {
168 unit.div_ceil(BITS_PER_BYTE)
169 } else {
170 unit
171 };
172
173 if bytes < threshold {
174 let value = if is_bits {
175 bytes * BITS_PER_BYTE
176 } else {
177 bytes
178 };
179
180 if is_bits {
181 write!(f, "{value}{unit_separator}b")?;
182 } else {
183 write!(f, "{value}{unit_separator}B")?;
184 }
185 } else {
186 let size = bytes as f64 * if is_bits { BITS_PER_BYTE as f64 } else { 1.0 };
187
188 #[cfg(feature = "std")]
189 let exp = ideal_unit_std(size, unit, unit_base);
190
191 #[cfg(not(feature = "std"))]
192 let exp = ideal_unit_no_std(size, unit);
193
194 let unit_prefix = unit_prefixes[exp - 1] as char;
195
196 write!(
197 f,
198 "{:.precision$}{unit_separator}{unit_prefix}{unit_suffix}",
199 (size / unit.pow(exp as u32) as f64),
200 )?;
201 }
202
203 Ok(())
204 }
205}
206
207#[allow(dead_code)] fn ideal_unit_no_std(size: f64, unit: u64) -> usize {
209 assert!(size >= unit as f64, "only called when bytes >= unit");
210
211 let mut ideal_prefix = 0;
212 let mut ideal_size = size;
213
214 loop {
215 ideal_prefix += 1;
216 ideal_size /= unit as f64;
217
218 if ideal_size < unit as f64 {
219 break;
220 }
221 }
222
223 ideal_prefix
224}
225
226#[cfg(feature = "std")]
227#[allow(dead_code)] fn ideal_unit_std(size: f64, unit: u64, unit_base: f64) -> usize {
229 assert!(size >= unit as f64, "only called when bytes >= unit");
230
231 let unit = unit as f64;
237 let mut exp = ((size.ln() / unit_base) as isize).max(1);
238
239 while exp > 1 && size / unit.powi(exp as i32 - 1) < unit {
240 exp -= 1;
241 }
242 while size / unit.powi(exp as i32) >= unit {
243 exp += 1;
244 }
245
246 exp as usize
247}
248
249#[cfg(test)]
250mod tests {
251 use alloc::{format, string::ToString as _};
252 use core::fmt::Write as _;
253
254 use super::*;
255
256 #[cfg(feature = "std")]
257 quickcheck::quickcheck! {
258 #[test]
259 fn ideal_unit_selection_std_no_std_iec(bytes: ByteSize) -> bool {
260 if bytes.0 < 1025 {
261 return true;
262 }
263
264 let size = bytes.0 as f64;
265
266 ideal_unit_std(size, crate::KIB, crate::LN_KIB) == ideal_unit_no_std(size, crate::KIB)
267 }
268
269 #[test]
270 fn ideal_unit_selection_std_no_std_si(bytes: ByteSize) -> bool {
271 if bytes.0 < 1025 {
272 return true;
273 }
274
275 let size = bytes.0 as f64;
276
277 ideal_unit_std(size, crate::KB, crate::LN_KB) == ideal_unit_no_std(size, crate::KB)
278 }
279
280 #[test]
281 fn ideal_unit_selection_std_no_std_iec_bits(bytes: ByteSize) -> bool {
282 if bytes.0 < 128 {
283 return true;
284 }
285
286 let size = bytes.0 as f64 * BITS_PER_BYTE as f64;
287
288 ideal_unit_std(size, crate::KIB, crate::LN_KIB)
289 == ideal_unit_no_std(size, crate::KIB)
290 }
291
292 #[test]
293 fn ideal_unit_selection_std_no_std_si_bits(bytes: ByteSize) -> bool {
294 if bytes.0 < 125 {
295 return true;
296 }
297
298 let size = bytes.0 as f64 * BITS_PER_BYTE as f64;
299
300 ideal_unit_std(size, crate::KB, crate::LN_KB) == ideal_unit_no_std(size, crate::KB)
301 }
302 }
303
304 #[cfg(feature = "std")]
310 #[test]
311 fn ideal_unit_std_matches_no_std_near_pebi_boundary() {
312 for bytes in [
313 1_125_899_906_842_621u64, 1_125_899_906_842_622, 1_125_899_906_842_623, 1_125_899_906_842_624, ] {
318 let size = bytes as f64;
319 assert_eq!(
320 ideal_unit_std(size, crate::KIB, crate::LN_KIB),
321 ideal_unit_no_std(size, crate::KIB),
322 "mismatch at {bytes} bytes (IEC)",
323 );
324 }
325
326 for bytes in [
327 999_999_999_999_996u64, 999_999_999_999_997, 999_999_999_999_998, 999_999_999_999_999, 1_000_000_000_000_000, ] {
333 let size = bytes as f64;
334 assert_eq!(
335 ideal_unit_std(size, crate::KB, crate::LN_KB),
336 ideal_unit_no_std(size, crate::KB),
337 "mismatch at {bytes} bytes (SI)",
338 );
339 }
340 }
341
342 #[test]
343 fn display_matches_just_below_pebi_boundary() {
344 assert_eq!(
345 "1024.0 TiB",
346 Display {
347 byte_size: ByteSize(1_125_899_906_842_623),
348 format: Format::Iec,
349 }
350 .to_string()
351 );
352 assert_eq!(
353 "1.0 PiB",
354 Display {
355 byte_size: ByteSize(1_125_899_906_842_624),
356 format: Format::Iec,
357 }
358 .to_string()
359 );
360 }
361
362 #[track_caller]
363 fn assert_to_string(expected: &str, byte_size: ByteSize, format: Format) {
364 assert_eq!(expected, Display { byte_size, format }.to_string());
365 }
366
367 #[test]
368 fn to_string_iec() {
369 let display = Display {
370 byte_size: ByteSize::gib(1),
371 format: Format::Iec,
372 };
373 assert_eq!("1.0 GiB", display.to_string());
374
375 let display = Display {
376 byte_size: ByteSize::gb(1),
377 format: Format::Iec,
378 };
379 assert_eq!("953.7 MiB", display.to_string());
380 }
381
382 #[test]
383 fn to_string_si() {
384 let display = Display {
385 byte_size: ByteSize::gib(1),
386 format: Format::Si,
387 };
388 assert_eq!("1.1 GB", display.to_string());
389
390 let display = Display {
391 byte_size: ByteSize::gb(1),
392 format: Format::Si,
393 };
394 assert_eq!("1.0 GB", display.to_string());
395 }
396
397 #[test]
398 fn to_string_short() {
399 let display = Display {
400 byte_size: ByteSize::gib(1),
401 format: Format::IecShort,
402 };
403 assert_eq!("1.0G", display.to_string());
404
405 let display = Display {
406 byte_size: ByteSize::gb(1),
407 format: Format::IecShort,
408 };
409 assert_eq!("953.7M", display.to_string());
410 }
411
412 #[test]
413 fn test_to_string_as() {
414 assert_to_string("215 B", ByteSize::b(215), Format::Iec);
415 assert_to_string("215 B", ByteSize::b(215), Format::Si);
416
417 assert_to_string("1.0 KiB", ByteSize::kib(1), Format::Iec);
418 assert_to_string("1.0 kB", ByteSize::kib(1), Format::Si);
419
420 assert_to_string("293.9 KiB", ByteSize::kb(301), Format::Iec);
421 assert_to_string("301.0 kB", ByteSize::kb(301), Format::Si);
422
423 assert_to_string("1.0 MiB", ByteSize::mib(1), Format::Iec);
424 assert_to_string("1.0 MB", ByteSize::mib(1), Format::Si);
425
426 assert_to_string("1.9 GiB", ByteSize::mib(1907), Format::Iec);
427 assert_to_string("2.0 GB", ByteSize::mib(1908), Format::Si);
428
429 assert_to_string("399.6 MiB", ByteSize::mb(419), Format::Iec);
430 assert_to_string("419.0 MB", ByteSize::mb(419), Format::Si);
431
432 assert_to_string("482.4 GiB", ByteSize::gb(518), Format::Iec);
433 assert_to_string("518.0 GB", ByteSize::gb(518), Format::Si);
434
435 assert_to_string("741.2 TiB", ByteSize::tb(815), Format::Iec);
436 assert_to_string("815.0 TB", ByteSize::tb(815), Format::Si);
437
438 assert_to_string("540.9 PiB", ByteSize::pb(609), Format::Iec);
439 assert_to_string("609.0 PB", ByteSize::pb(609), Format::Si);
440 }
441
442 #[test]
443 fn to_string_bits() {
444 assert_to_string("1016 b", ByteSize(127), Format::IecBits);
445 assert_to_string("1.0 Kib", ByteSize(128), Format::IecBits);
446 assert_to_string("992 b", ByteSize(124), Format::SiBits);
447 assert_to_string("1.0 kb", ByteSize(125), Format::SiBits);
448
449 assert_to_string("7.8 Kib", ByteSize::kb(1), Format::IecBits);
450 assert_to_string("8.0 kb", ByteSize::kb(1), Format::SiBits);
451 assert_to_string("128.0 Eib", ByteSize(u64::MAX), Format::IecBits);
452 assert_to_string("147.6 Eb", ByteSize(u64::MAX), Format::SiBits);
453 }
454
455 #[test]
456 fn display_bits_public_api() {
457 assert_eq!("1.0 Kib", ByteSize(128).display().iec_bits().to_string());
458 assert_eq!("1.0 kb", ByteSize(125).display().si_bits().to_string());
459 }
460
461 #[test]
462 fn display_propagates_write_errors() {
463 struct FailingWriter;
464
465 impl fmt::Write for FailingWriter {
466 fn write_str(&mut self, _: &str) -> fmt::Result {
467 Err(fmt::Error)
468 }
469 }
470
471 let mut writer = FailingWriter;
472
473 assert_eq!(
474 Err(fmt::Error),
475 write!(
476 writer,
477 "{}",
478 Display {
479 byte_size: ByteSize(127),
480 format: Format::IecBits,
481 },
482 ),
483 );
484 assert_eq!(
485 Err(fmt::Error),
486 write!(
487 writer,
488 "{}",
489 Display {
490 byte_size: ByteSize(1),
491 format: Format::Iec,
492 },
493 ),
494 );
495 }
496
497 #[test]
498 fn precision() {
499 let size = ByteSize::mib(1908);
500 assert_eq!("1.9 GiB".to_string(), format!("{size}"));
501 assert_eq!("2 GiB".to_string(), format!("{size:.0}"));
502 assert_eq!("1.86328 GiB".to_string(), format!("{size:.5}"));
503 }
504}