#![forbid(unsafe_code)]
const BINARY_UNITS_SHORT: &[&str] = &["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
const DECIMAL_UNITS_SHORT: &[&str] = &["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const BINARY_UNITS_LONG: &[&str] = &[
"bytes",
"kibibytes",
"mebibytes",
"gibibytes",
"tebibytes",
"pebibytes",
"exbibytes",
"zebibytes",
"yobibytes",
];
const DECIMAL_UNITS_LONG: &[&str] = &[
"bytes",
"kilobytes",
"megabytes",
"gigabytes",
"terabytes",
"petabytes",
"exabytes",
"zettabytes",
"yottabytes",
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SizeUnit {
Binary,
Decimal,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnitStyle {
Short,
Long,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SizeFormat {
pub unit: SizeUnit,
pub style: UnitStyle,
pub precision: usize,
}
impl Default for SizeFormat {
fn default() -> Self {
Self::binary()
}
}
impl SizeFormat {
#[must_use]
pub const fn binary() -> Self {
Self {
unit: SizeUnit::Binary,
style: UnitStyle::Short,
precision: 1,
}
}
#[must_use]
pub const fn decimal() -> Self {
Self {
unit: SizeUnit::Decimal,
style: UnitStyle::Short,
precision: 1,
}
}
#[must_use]
pub fn with_precision(mut self, precision: usize) -> Self {
self.precision = precision;
self
}
#[must_use]
pub fn short(mut self) -> Self {
self.style = UnitStyle::Short;
self
}
#[must_use]
pub fn long(mut self) -> Self {
self.style = UnitStyle::Long;
self
}
}
#[must_use]
pub fn format_size(size: i64, format: SizeFormat) -> String {
let (base, units): (f64, &[&str]) = match (format.unit, format.style) {
(SizeUnit::Binary, UnitStyle::Short) => (1024.0, BINARY_UNITS_SHORT),
(SizeUnit::Binary, UnitStyle::Long) => (1024.0, BINARY_UNITS_LONG),
(SizeUnit::Decimal, UnitStyle::Short) => (1000.0, DECIMAL_UNITS_SHORT),
(SizeUnit::Decimal, UnitStyle::Long) => (1000.0, DECIMAL_UNITS_LONG),
};
let negative = size < 0;
let abs_size = size.unsigned_abs();
#[allow(clippy::cast_precision_loss)]
if abs_size < base as u64 {
let prefix = if negative { "-" } else { "" };
return format!("{prefix}{abs_size} {}", units[0]);
}
#[allow(clippy::cast_precision_loss)]
let mut value = abs_size as f64;
let mut unit_idx = 0;
while value >= base && unit_idx < units.len() - 1 {
value /= base;
unit_idx += 1;
}
let prefix = if negative { "-" } else { "" };
format!(
"{prefix}{value:.precision$} {}",
units[unit_idx],
precision = format.precision
)
}
#[must_use]
pub fn decimal(size: u64) -> String {
let clamped = size.min(i64::MAX as u64) as i64;
format_size(clamped, SizeFormat::decimal())
}
#[must_use]
pub fn decimal_with_precision(size: u64, precision: usize) -> String {
let clamped = size.min(i64::MAX as u64) as i64;
format_size(clamped, SizeFormat::decimal().with_precision(precision))
}
#[must_use]
pub fn binary(size: u64) -> String {
let clamped = size.min(i64::MAX as u64) as i64;
format_size(clamped, SizeFormat::binary())
}
#[must_use]
pub fn binary_with_precision(size: u64, precision: usize) -> String {
let clamped = size.min(i64::MAX as u64) as i64;
format_size(clamped, SizeFormat::binary().with_precision(precision))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decimal_bytes_thresholds() {
let fmt = SizeFormat::decimal();
assert_eq!(format_size(0, fmt), "0 B");
assert_eq!(format_size(999, fmt), "999 B");
assert_eq!(format_size(1000, fmt), "1.0 KB");
}
#[test]
fn binary_bytes_thresholds() {
let fmt = SizeFormat::binary();
assert_eq!(format_size(0, fmt), "0 B");
assert_eq!(format_size(1023, fmt), "1023 B");
assert_eq!(format_size(1024, fmt), "1.0 KiB");
}
#[test]
fn decimal_precision_rounding() {
assert_eq!(decimal_with_precision(1_234_567, 0), "1 MB");
assert_eq!(decimal_with_precision(1_234_567, 1), "1.2 MB");
assert_eq!(decimal_with_precision(1_234_567, 2), "1.23 MB");
assert_eq!(decimal_with_precision(1_234_567, 3), "1.235 MB");
}
#[test]
fn binary_precision_rounding() {
assert_eq!(binary_with_precision(1_572_864, 2), "1.50 MiB");
}
#[test]
fn long_unit_style() {
let fmt = SizeFormat::decimal().long();
assert_eq!(format_size(1_500, fmt), "1.5 kilobytes");
}
#[test]
fn negative_sizes() {
let fmt = SizeFormat::decimal();
assert_eq!(format_size(-1_000, fmt), "-1.0 KB");
}
#[test]
fn negative_bytes_are_unscaled() {
let fmt = SizeFormat::binary();
assert_eq!(format_size(-500, fmt), "-500 B");
let fmt_long = SizeFormat::decimal().long();
assert_eq!(format_size(-12, fmt_long), "-12 bytes");
}
#[test]
fn negative_binary_units() {
let fmt = SizeFormat::binary();
assert_eq!(format_size(-1_024, fmt), "-1.0 KiB");
}
#[test]
fn large_sizes() {
assert_eq!(decimal(1_000_000_000_000_000), "1.0 PB");
assert_eq!(binary(1_125_899_906_842_624), "1.0 PiB");
}
#[test]
fn filesize_gb_binary() {
assert_eq!(binary(1_073_741_824), "1.0 GiB");
assert_eq!(binary(2_147_483_648), "2.0 GiB");
}
#[test]
fn filesize_tb_binary() {
assert_eq!(binary(1_099_511_627_776), "1.0 TiB");
}
#[test]
fn filesize_gb_decimal() {
assert_eq!(decimal(1_000_000_000), "1.0 GB");
assert_eq!(decimal(2_500_000_000), "2.5 GB");
}
#[test]
fn filesize_tb_decimal() {
assert_eq!(decimal(1_000_000_000_000), "1.0 TB");
}
#[test]
fn filesize_short_style() {
let fmt = SizeFormat::binary().short();
assert_eq!(format_size(1_048_576, fmt), "1.0 MiB");
}
#[test]
fn filesize_long_binary_style() {
let fmt = SizeFormat::binary().long();
assert_eq!(format_size(1_048_576, fmt), "1.0 mebibytes");
assert_eq!(format_size(500, fmt), "500 bytes");
}
#[test]
fn filesize_edge_zero() {
assert_eq!(decimal(0), "0 B");
assert_eq!(binary(0), "0 B");
let fmt = SizeFormat::decimal().long();
assert_eq!(format_size(0, fmt), "0 bytes");
}
#[test]
fn filesize_edge_one_byte() {
assert_eq!(decimal(1), "1 B");
assert_eq!(binary(1), "1 B");
}
#[test]
fn filesize_convenience_functions() {
assert_eq!(decimal(1_500_000), "1.5 MB");
assert_eq!(binary(1_024), "1.0 KiB");
}
#[test]
fn size_format_default_is_binary() {
let default = SizeFormat::default();
assert_eq!(default.unit, SizeUnit::Binary);
assert_eq!(default.style, UnitStyle::Short);
assert_eq!(default.precision, 1);
}
#[test]
fn size_format_builder_chaining() {
let fmt = SizeFormat::decimal().with_precision(3).long();
assert_eq!(fmt.unit, SizeUnit::Decimal);
assert_eq!(fmt.style, UnitStyle::Long);
assert_eq!(fmt.precision, 3);
}
#[test]
fn clamp_to_i64_max() {
let max = i64::MAX as u64;
assert_eq!(decimal(u64::MAX), decimal(max));
assert_eq!(binary(u64::MAX), binary(max));
assert_eq!(
decimal_with_precision(u64::MAX, 2),
decimal_with_precision(max, 2)
);
assert_eq!(
binary_with_precision(u64::MAX, 2),
binary_with_precision(max, 2)
);
}
#[test]
fn decimal_all_unit_levels() {
let fmt = SizeFormat::decimal();
assert_eq!(format_size(1_000, fmt), "1.0 KB");
assert_eq!(format_size(1_000_000, fmt), "1.0 MB");
assert_eq!(format_size(1_000_000_000, fmt), "1.0 GB");
assert_eq!(format_size(1_000_000_000_000, fmt), "1.0 TB");
assert_eq!(format_size(1_000_000_000_000_000, fmt), "1.0 PB");
assert_eq!(format_size(1_000_000_000_000_000_000, fmt), "1.0 EB");
}
#[test]
fn binary_all_unit_levels() {
let fmt = SizeFormat::binary();
assert_eq!(format_size(1_024, fmt), "1.0 KiB");
assert_eq!(format_size(1_048_576, fmt), "1.0 MiB");
assert_eq!(format_size(1_073_741_824, fmt), "1.0 GiB");
assert_eq!(format_size(1_099_511_627_776, fmt), "1.0 TiB");
assert_eq!(format_size(1_125_899_906_842_624, fmt), "1.0 PiB");
assert_eq!(format_size(1_152_921_504_606_846_976, fmt), "1.0 EiB");
}
#[test]
fn long_decimal_all_levels() {
let fmt = SizeFormat::decimal().long();
assert_eq!(format_size(42, fmt), "42 bytes");
assert_eq!(format_size(1_500, fmt), "1.5 kilobytes");
assert_eq!(format_size(1_500_000, fmt), "1.5 megabytes");
assert_eq!(format_size(1_500_000_000, fmt), "1.5 gigabytes");
assert_eq!(format_size(1_500_000_000_000, fmt), "1.5 terabytes");
assert_eq!(format_size(1_500_000_000_000_000, fmt), "1.5 petabytes");
assert_eq!(format_size(1_500_000_000_000_000_000, fmt), "1.5 exabytes");
}
#[test]
fn long_binary_all_levels() {
let fmt = SizeFormat::binary().long();
assert_eq!(format_size(42, fmt), "42 bytes");
assert_eq!(format_size(1_536, fmt), "1.5 kibibytes");
assert_eq!(format_size(1_572_864, fmt), "1.5 mebibytes");
assert_eq!(format_size(1_610_612_736, fmt), "1.5 gibibytes");
assert_eq!(format_size(1_649_267_441_664, fmt), "1.5 tebibytes");
assert_eq!(format_size(1_688_849_860_263_936, fmt), "1.5 pebibytes");
assert_eq!(format_size(1_729_382_256_910_270_464, fmt), "1.5 exbibytes");
}
#[test]
fn negative_decimal_various_levels() {
let fmt = SizeFormat::decimal();
assert_eq!(format_size(-1, fmt), "-1 B");
assert_eq!(format_size(-1_000, fmt), "-1.0 KB");
assert_eq!(format_size(-1_500_000, fmt), "-1.5 MB");
assert_eq!(format_size(-2_000_000_000, fmt), "-2.0 GB");
}
#[test]
fn negative_binary_various_levels() {
let fmt = SizeFormat::binary();
assert_eq!(format_size(-1, fmt), "-1 B");
assert_eq!(format_size(-1_024, fmt), "-1.0 KiB");
assert_eq!(format_size(-1_048_576, fmt), "-1.0 MiB");
assert_eq!(format_size(-1_073_741_824, fmt), "-1.0 GiB");
}
#[test]
fn negative_long_style() {
let fmt = SizeFormat::decimal().long();
assert_eq!(format_size(-500, fmt), "-500 bytes");
assert_eq!(format_size(-1_500_000, fmt), "-1.5 megabytes");
}
#[test]
fn negative_with_precision() {
let fmt = SizeFormat::decimal().with_precision(3);
assert_eq!(format_size(-1_234_567, fmt), "-1.235 MB");
}
#[test]
fn i64_min_does_not_panic() {
let result = format_size(i64::MIN, SizeFormat::binary());
assert!(result.starts_with('-'));
assert!(!result.is_empty());
}
#[test]
fn i64_max_formats() {
let result = format_size(i64::MAX, SizeFormat::decimal());
assert!(!result.is_empty());
assert!(
result.contains("EB"),
"i64::MAX should be in EB range: {result}"
);
}
#[test]
fn decimal_just_under_threshold() {
let fmt = SizeFormat::decimal();
assert_eq!(format_size(999, fmt), "999 B");
assert_eq!(format_size(1000, fmt), "1.0 KB");
assert_eq!(format_size(999_999, fmt), "1000.0 KB");
assert_eq!(format_size(1_000_000, fmt), "1.0 MB");
}
#[test]
fn binary_just_under_threshold() {
let fmt = SizeFormat::binary();
assert_eq!(format_size(1023, fmt), "1023 B");
assert_eq!(format_size(1024, fmt), "1.0 KiB");
}
#[test]
fn decimal_fractional_kb() {
let fmt = SizeFormat::decimal().with_precision(2);
assert_eq!(format_size(1_500, fmt), "1.50 KB");
assert_eq!(format_size(1_999, fmt), "2.00 KB");
assert_eq!(format_size(1_001, fmt), "1.00 KB");
}
#[test]
fn precision_zero_at_byte_level() {
let fmt = SizeFormat::decimal().with_precision(0);
assert_eq!(format_size(500, fmt), "500 B");
}
#[test]
fn high_precision() {
let fmt = SizeFormat::decimal().with_precision(6);
assert_eq!(format_size(1_234_567, fmt), "1.234567 MB");
}
#[test]
fn precision_zero_rounds_up() {
let fmt = SizeFormat::decimal().with_precision(0);
assert_eq!(format_size(1_500_000, fmt), "2 MB");
}
#[test]
fn precision_zero_rounds_down() {
let fmt = SizeFormat::decimal().with_precision(0);
assert_eq!(format_size(1_400_000, fmt), "1 MB");
}
#[test]
fn all_unit_style_combinations() {
let size = 1_048_576_i64;
assert_eq!(format_size(size, SizeFormat::binary().short()), "1.0 MiB");
assert_eq!(
format_size(size, SizeFormat::binary().long()),
"1.0 mebibytes"
);
assert_eq!(format_size(size, SizeFormat::decimal().short()), "1.0 MB");
assert_eq!(
format_size(size, SizeFormat::decimal().long()),
"1.0 megabytes"
);
}
#[test]
fn binary_with_precision_zero() {
assert_eq!(binary_with_precision(1_048_576, 0), "1 MiB");
}
#[test]
fn binary_with_precision_high() {
assert_eq!(binary_with_precision(1_572_864, 4), "1.5000 MiB");
}
#[test]
fn decimal_with_precision_zero() {
assert_eq!(decimal_with_precision(1_500_000, 0), "2 MB");
}
#[test]
fn decimal_with_precision_high() {
assert_eq!(decimal_with_precision(1_234_567, 5), "1.23457 MB");
}
#[test]
fn size_unit_debug_clone_copy_eq() {
let unit = SizeUnit::Binary;
let cloned = unit;
assert_eq!(unit, cloned);
assert_eq!(format!("{unit:?}"), "Binary");
assert_eq!(format!("{:?}", SizeUnit::Decimal), "Decimal");
assert_ne!(SizeUnit::Binary, SizeUnit::Decimal);
}
#[test]
fn unit_style_debug_clone_copy_eq() {
let style = UnitStyle::Short;
let cloned = style;
assert_eq!(style, cloned);
assert_eq!(format!("{style:?}"), "Short");
assert_eq!(format!("{:?}", UnitStyle::Long), "Long");
assert_ne!(UnitStyle::Short, UnitStyle::Long);
}
#[test]
fn size_format_debug_clone_copy_eq() {
let fmt = SizeFormat::decimal();
let cloned = fmt;
assert_eq!(fmt, cloned);
let _ = format!("{fmt:?}");
assert_ne!(
SizeFormat::decimal().with_precision(1),
SizeFormat::decimal().with_precision(2)
);
}
#[test]
fn short_builder_overrides_long() {
let fmt = SizeFormat::binary().long().short();
assert_eq!(fmt.style, UnitStyle::Short);
}
#[test]
fn long_builder_overrides_short() {
let fmt = SizeFormat::decimal().short().long();
assert_eq!(fmt.style, UnitStyle::Long);
}
#[test]
fn with_precision_overrides_default() {
let fmt = SizeFormat::binary().with_precision(5);
assert_eq!(fmt.precision, 5);
}
#[test]
fn decimal_rounding_at_half() {
assert_eq!(decimal(1_550_000), "1.6 MB");
}
#[test]
fn binary_rounding_boundary() {
assert_eq!(binary(1_536), "1.5 KiB");
}
#[test]
fn format_size_single_byte() {
let fmt = SizeFormat::decimal().long();
assert_eq!(format_size(1, fmt), "1 bytes");
}
#[test]
fn unit_arrays_consistent_length() {
assert_eq!(BINARY_UNITS_SHORT.len(), BINARY_UNITS_LONG.len());
assert_eq!(DECIMAL_UNITS_SHORT.len(), DECIMAL_UNITS_LONG.len());
assert_eq!(BINARY_UNITS_SHORT.len(), DECIMAL_UNITS_SHORT.len());
}
#[test]
fn unit_arrays_start_with_bytes() {
assert_eq!(BINARY_UNITS_SHORT[0], "B");
assert_eq!(DECIMAL_UNITS_SHORT[0], "B");
assert_eq!(BINARY_UNITS_LONG[0], "bytes");
assert_eq!(DECIMAL_UNITS_LONG[0], "bytes");
}
}