1use core::fmt;
16use core::fmt::Write as _;
17
18use crate::BaseByteSize;
19use crate::ByteSize;
20
21pub fn display(size: impl BaseByteSize) -> Display {
26 Display::new(size.to_f64())
27}
28
29impl<T: BaseByteSize> ByteSize<T> {
30 pub fn display(&self) -> Display {
34 Display::new(self.bytes().to_f64())
35 }
36}
37
38#[derive(Debug, Clone)]
124pub struct Display {
125 size: f64,
126 options: DisplayOptions,
127}
128
129#[derive(Debug, Clone, Copy)]
133pub struct DisplayOptions {
134 base_unit: DisplayBaseUnit,
135 scale: DisplayScale,
136 unit_system: DisplayUnitSystem,
137}
138
139impl DisplayOptions {
140 pub const BINARY: Self = Self {
144 base_unit: DisplayBaseUnit::Byte,
145 scale: DisplayScale::Auto,
146 unit_system: DisplayUnitSystem::Binary,
147 };
148
149 pub const DECIMAL: Self = Self {
153 base_unit: DisplayBaseUnit::Byte,
154 scale: DisplayScale::Auto,
155 unit_system: DisplayUnitSystem::Decimal,
156 };
157
158 #[inline(always)]
162 pub const fn new() -> Self {
163 DisplayOptions::BINARY
164 }
165
166 #[inline(always)]
170 pub const fn base_unit(mut self, base_unit: DisplayBaseUnit) -> Self {
171 self.base_unit = base_unit;
172 self
173 }
174
175 #[inline(always)]
179 pub const fn scale(mut self, scale: DisplayScale) -> Self {
180 self.scale = scale;
181 self
182 }
183
184 #[inline(always)]
188 pub const fn unit_system(mut self, unit_system: DisplayUnitSystem) -> Self {
189 self.unit_system = unit_system;
190 self
191 }
192}
193
194impl Default for DisplayOptions {
195 fn default() -> Self {
197 Self::new()
198 }
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, Eq)]
205#[non_exhaustive]
206pub enum DisplayBaseUnit {
207 Bit,
221 Byte,
223}
224
225#[derive(Debug, Clone, Copy, PartialEq, Eq)]
229#[non_exhaustive]
230pub enum DisplayScale {
231 Auto,
233 Base,
235 Kilo,
237 Mega,
239 Giga,
241 Tera,
243 Peta,
245 Exa,
247}
248
249#[derive(Debug, Clone, Copy, PartialEq, Eq)]
253#[non_exhaustive]
254pub enum DisplayUnitSystem {
255 Binary,
257 Decimal,
259}
260
261impl Display {
262 pub fn binary(mut self) -> Self {
266 self.options = DisplayOptions::BINARY;
267 self
268 }
269
270 pub fn decimal(mut self) -> Self {
274 self.options = DisplayOptions::DECIMAL;
275 self
276 }
277
278 pub fn options(mut self, f: impl FnOnce(DisplayOptions) -> DisplayOptions) -> Self {
311 self.options = f(self.options);
312 self
313 }
314
315 pub fn new(size: f64) -> Self {
332 assert!(size >= 0.0, "size must be non-negative and not NaN");
333 let options = DisplayOptions::BINARY;
334 Self { size, options }
335 }
336}
337
338impl fmt::Display for Display {
339 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
340 let value = match self.options.base_unit {
341 DisplayBaseUnit::Bit => self.size * 8.0,
342 DisplayBaseUnit::Byte => self.size,
343 };
344 let divisor = match self.options.unit_system {
345 DisplayUnitSystem::Binary => 1024.0,
346 DisplayUnitSystem::Decimal => 1000.0,
347 };
348 let (value, exponent) = scaled_value(value, divisor, self.options.scale);
349 let precision = f.precision();
350
351 let Some(width) = f.width() else {
352 return write_display(f, value, exponent, self.options, precision);
354 };
355
356 let mut counter = WidthCounter { width: 0 };
357 write_display(&mut counter, value, exponent, self.options, precision)?;
358
359 let padding = width.saturating_sub(counter.width);
360 let (left_padding, right_padding) = match f.align().unwrap_or(fmt::Alignment::Left) {
361 fmt::Alignment::Left => (0, padding),
362 fmt::Alignment::Right => (padding, 0),
363 fmt::Alignment::Center => (padding / 2, padding - padding / 2),
364 };
365
366 let fill = f.fill();
367 for _ in 0..left_padding {
368 f.write_char(fill)?;
369 }
370 write_display(f, value, exponent, self.options, precision)?;
371 for _ in 0..right_padding {
372 f.write_char(fill)?;
373 }
374 Ok(())
375 }
376}
377
378fn write_display(
379 f: &mut impl fmt::Write,
380 value: f64,
381 exponent: usize,
382 options: DisplayOptions,
383 precision: Option<usize>,
384) -> fmt::Result {
385 if let Some(precision) = precision {
386 write!(f, "{value:.precision$}")?;
387 } else if exponent == 0 {
388 write!(f, "{value}")?;
389 } else {
390 write!(f, "{value:.1}")?;
391 }
392
393 let unit_separator = " ";
394 f.write_str(unit_separator)?;
395
396 if exponent == 0 {
397 f.write_str(match options.base_unit {
398 DisplayBaseUnit::Bit => "bit",
399 DisplayBaseUnit::Byte => "B",
400 })
401 } else {
402 let unit_prefixes = match options.unit_system {
406 DisplayUnitSystem::Binary => b"KMGTPE",
407 DisplayUnitSystem::Decimal => b"kMGTPE",
408 };
409 let unit_suffix = match (options.unit_system, options.base_unit) {
410 (DisplayUnitSystem::Binary, DisplayBaseUnit::Bit) => "ibit",
411 (DisplayUnitSystem::Binary, DisplayBaseUnit::Byte) => "iB",
412 (DisplayUnitSystem::Decimal, DisplayBaseUnit::Bit) => "bit",
413 (DisplayUnitSystem::Decimal, DisplayBaseUnit::Byte) => "B",
414 };
415 let unit_prefix = unit_prefixes[exponent - 1] as char;
416 write!(f, "{unit_prefix}{unit_suffix}")
417 }
418}
419
420struct WidthCounter {
421 width: usize,
422}
423
424impl fmt::Write for WidthCounter {
425 fn write_str(&mut self, s: &str) -> fmt::Result {
426 self.width += s.len();
428 Ok(())
429 }
430
431 fn write_char(&mut self, _: char) -> fmt::Result {
432 self.width += 1;
433 Ok(())
434 }
435}
436
437fn scaled_value(mut value: f64, divisor: f64, scale: DisplayScale) -> (f64, usize) {
438 const MAX_EXPONENT: usize = 6;
439
440 let exponent = match scale {
441 DisplayScale::Auto => {
442 let mut exponent = 0;
443 while value >= divisor && exponent < MAX_EXPONENT {
444 value /= divisor;
445 exponent += 1;
446 }
447 return (value, exponent);
448 }
449 DisplayScale::Base => 0,
450 DisplayScale::Kilo => 1,
451 DisplayScale::Mega => 2,
452 DisplayScale::Giga => 3,
453 DisplayScale::Tera => 4,
454 DisplayScale::Peta => 5,
455 DisplayScale::Exa => 6,
456 };
457
458 for _ in 0..exponent {
459 value /= divisor;
460 }
461
462 (value, exponent)
463}
464
465#[cfg(test)]
466mod tests {
467 use alloc::format;
468
469 use insta::assert_snapshot;
470
471 use super::*;
472
473 #[test]
474 fn test_formatting_snapshots() {
475 use DisplayUnitSystem::*;
476
477 fn display(size: u64, system: DisplayUnitSystem) -> Display {
478 super::display(size).options(|opts| opts.unit_system(system))
479 }
480
481 assert_snapshot!(display(0, Binary), @"0 B");
482 assert_snapshot!(display(0, Decimal), @"0 B");
483 assert_snapshot!(display(1, Binary), @"1 B");
484 assert_snapshot!(display(1, Decimal), @"1 B");
485 assert_snapshot!(display(500, Binary), @"500 B");
486 assert_snapshot!(display(500, Decimal), @"500 B");
487 assert_snapshot!(display(999, Binary), @"999 B");
488 assert_snapshot!(display(999, Decimal), @"999 B");
489 assert_snapshot!(display(1000, Binary), @"1000 B");
490 assert_snapshot!(display(1000, Decimal), @"1.0 kB");
491 assert_snapshot!(display(1023, Binary), @"1023 B");
492 assert_snapshot!(display(1023, Decimal), @"1.0 kB");
493 assert_snapshot!(display(1024, Binary), @"1.0 KiB");
494 assert_snapshot!(display(1024, Decimal), @"1.0 kB");
495 assert_snapshot!(display(1025, Binary), @"1.0 KiB");
496 assert_snapshot!(display(1025, Decimal), @"1.0 kB");
497 assert_snapshot!(display(1500, Binary), @"1.5 KiB");
498 assert_snapshot!(display(1500, Decimal), @"1.5 kB");
499 assert_snapshot!(display(2048, Binary), @"2.0 KiB");
500 assert_snapshot!(display(2048, Decimal), @"2.0 kB");
501 assert_snapshot!(display(1_000_000, Binary), @"976.6 KiB");
502 assert_snapshot!(display(1_000_000, Decimal), @"1.0 MB");
503 assert_snapshot!(display(1_048_576, Binary), @"1.0 MiB");
504 assert_snapshot!(display(1_048_576, Decimal), @"1.0 MB");
505 assert_snapshot!(display(987_654_321, Binary), @"941.9 MiB");
506 assert_snapshot!(display(987_654_321, Decimal), @"987.7 MB");
507 assert_snapshot!(display(1_099_511_627_776, Binary), @"1.0 TiB");
508 assert_snapshot!(display(1_099_511_627_776, Decimal), @"1.1 TB");
509 assert_snapshot!(display(1_125_899_906_842_624, Binary), @"1.0 PiB");
510 assert_snapshot!(display(1_125_899_906_842_624, Decimal), @"1.1 PB");
511 assert_snapshot!(display(1_152_921_504_606_846_976, Binary), @"1.0 EiB");
512 assert_snapshot!(display(1_152_921_504_606_846_976, Decimal), @"1.2 EB");
513 assert_snapshot!(display(u64::MAX - 1, Binary), @"16.0 EiB");
514 assert_snapshot!(display(u64::MAX - 1, Decimal), @"18.4 EB");
515 assert_snapshot!(display(u64::MAX, Binary), @"16.0 EiB");
516 assert_snapshot!(display(u64::MAX, Decimal), @"18.4 EB");
517 }
518
519 #[test]
520 fn test_formats_fractional_sizes() {
521 assert_snapshot!(Display::new(42.5).binary(), @"42.5 B");
522 assert_snapshot!(Display::new(1000.5).decimal(), @"1.0 kB");
523 assert_snapshot!(format!("{:.2}", Display::new(2500.5).decimal()), @"2.50 kB");
524 }
525
526 #[test]
527 fn test_formats_infinite_size() {
528 assert_snapshot!(Display::new(f64::INFINITY).binary(), @"inf EiB");
529 assert_snapshot!(Display::new(f64::INFINITY).decimal(), @"inf EB");
530 }
531
532 #[test]
533 #[should_panic]
534 fn test_new_rejects_nan_size() {
535 Display::new(f64::NAN);
536 }
537
538 #[test]
539 #[should_panic]
540 fn test_new_rejects_negative_size() {
541 Display::new(-1.0);
542 }
543
544 #[test]
545 fn test_formats_default_binary() {
546 assert_snapshot!(display(999u64), @"999 B");
547 assert_snapshot!(display(1000u64), @"1000 B");
548 }
549
550 #[test]
551 fn test_formats_scales() {
552 assert_snapshot!(
553 display(1536u64).options(|opts| opts.scale(DisplayScale::Base)),
554 @"1536 B"
555 );
556 assert_snapshot!(
557 display(1536u64).options(|opts| opts.scale(DisplayScale::Kilo)),
558 @"1.5 KiB"
559 );
560 assert_snapshot!(
561 format!(
562 "{:.3}",
563 display(1536u64).options(|opts| opts
564 .unit_system(DisplayUnitSystem::Decimal)
565 .scale(DisplayScale::Mega))
566 ),
567 @"0.002 MB"
568 );
569 }
570
571 #[test]
572 fn test_formats_bits() {
573 assert_snapshot!(
574 display(1u64).options(|opts| opts.base_unit(DisplayBaseUnit::Bit)),
575 @"8 bit"
576 );
577 assert_snapshot!(
578 display(125u64).options(|opts| opts.base_unit(DisplayBaseUnit::Bit)),
579 @"1000 bit"
580 );
581 assert_snapshot!(
582 display(125u64).options(|opts| opts
583 .base_unit(DisplayBaseUnit::Bit)
584 .unit_system(DisplayUnitSystem::Decimal)),
585 @"1.0 kbit"
586 );
587 assert_snapshot!(
588 display(128u64).options(|opts| opts.base_unit(DisplayBaseUnit::Bit)),
589 @"1.0 Kibit"
590 );
591 }
592
593 #[test]
594 fn test_formats_with_display_options() {
595 assert_snapshot!(
596 display(1536u64).options(|opts| opts
597 .base_unit(DisplayBaseUnit::Bit)
598 .scale(DisplayScale::Kilo)),
599 @"12.0 Kibit"
600 );
601 }
602
603 #[test]
604 fn test_formats_with_width_fill_and_alignment() {
605 assert_snapshot!(format!("{:10}", display(1536u64)), @"1.5 KiB ");
606 assert_snapshot!(format!("{:<10}", display(1536u64)), @"1.5 KiB ");
607 assert_snapshot!(format!("{:>10}", display(1536u64)), @" 1.5 KiB");
608 assert_snapshot!(format!("{:^10}", display(1536u64)), @" 1.5 KiB ");
609 assert_snapshot!(format!("{:*^10}", display(1536u64)), @"*1.5 KiB**");
610 assert_snapshot!(format!("{:*>10.2}", display(1536u64)), @"**1.50 KiB");
611 }
612}