const BINARY_UNITS: &[&str] = &[
"bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB",
];
const DECIMAL_UNITS: &[&str] = &["bytes", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SizeUnit {
#[default]
Binary,
Decimal,
}
#[must_use]
pub fn format_size(size: i64, unit: SizeUnit, precision: usize) -> String {
let (base, units): (f64, &[&str]) = match unit {
SizeUnit::Binary => (1024.0, BINARY_UNITS),
SizeUnit::Decimal => (1000.0, DECIMAL_UNITS),
};
let negative = size < 0;
let abs_size = size.unsigned_abs();
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
if abs_size < base as u64 {
let prefix = if negative { "-" } else { "" };
return format!("{prefix}{abs_size} bytes");
}
#[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])
}
#[must_use]
pub fn decimal(size: u64) -> String {
#[allow(clippy::cast_possible_wrap)]
format_size(size as i64, SizeUnit::Decimal, 1)
}
#[must_use]
pub fn decimal_with_precision(size: u64, precision: usize) -> String {
#[allow(clippy::cast_possible_wrap)]
format_size(size as i64, SizeUnit::Decimal, precision)
}
#[must_use]
pub fn binary(size: u64) -> String {
#[allow(clippy::cast_possible_wrap)]
format_size(size as i64, SizeUnit::Binary, 1)
}
#[must_use]
pub fn binary_with_precision(size: u64, precision: usize) -> String {
#[allow(clippy::cast_possible_wrap)]
format_size(size as i64, SizeUnit::Binary, precision)
}
#[must_use]
pub fn format_speed(bytes_per_second: f64, unit: SizeUnit, precision: usize) -> String {
if bytes_per_second.is_nan() {
return "NaN".to_string();
}
if bytes_per_second.is_infinite() {
let prefix = if bytes_per_second.is_sign_negative() {
"-"
} else {
""
};
return format!("{prefix}∞");
}
let (base, units): (f64, &[&str]) = match unit {
SizeUnit::Binary => (1024.0, BINARY_UNITS),
SizeUnit::Decimal => (1000.0, DECIMAL_UNITS),
};
let negative = bytes_per_second < 0.0;
let mut value = bytes_per_second.abs();
if value < base {
let prefix = if negative { "-" } else { "" };
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let int_value = value as u64;
return format!("{prefix}{int_value} bytes/s");
}
let mut unit_idx = 0;
while value >= base && unit_idx < units.len() - 1 {
value /= base;
unit_idx += 1;
}
let unit_str = units[unit_idx];
let speed_unit = if unit_str == "bytes" {
"bytes/s"
} else {
&format!("{unit_str}/s")
};
let prefix = if negative { "-" } else { "" };
format!("{prefix}{value:.precision$} {speed_unit}")
}
#[must_use]
pub fn decimal_speed(bytes_per_second: f64) -> String {
format_speed(bytes_per_second, SizeUnit::Decimal, 1)
}
#[must_use]
pub fn binary_speed(bytes_per_second: f64) -> String {
format_speed(bytes_per_second, SizeUnit::Binary, 1)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_decimal_bytes() {
assert_eq!(decimal(0), "0 bytes");
assert_eq!(decimal(1), "1 bytes");
assert_eq!(decimal(999), "999 bytes");
}
#[test]
fn test_decimal_kilobytes() {
assert_eq!(decimal(1_000), "1.0 kB");
assert_eq!(decimal(1_500), "1.5 kB");
assert_eq!(decimal(999_000), "999.0 kB");
}
#[test]
fn test_decimal_megabytes() {
assert_eq!(decimal(1_000_000), "1.0 MB");
assert_eq!(decimal(1_500_000), "1.5 MB");
assert_eq!(decimal(999_000_000), "999.0 MB");
}
#[test]
fn test_decimal_gigabytes() {
assert_eq!(decimal(1_000_000_000), "1.0 GB");
assert_eq!(decimal(1_500_000_000), "1.5 GB");
}
#[test]
fn test_decimal_terabytes() {
assert_eq!(decimal(1_000_000_000_000), "1.0 TB");
assert_eq!(decimal(1_500_000_000_000), "1.5 TB");
}
#[test]
fn test_binary_bytes() {
assert_eq!(binary(0), "0 bytes");
assert_eq!(binary(1), "1 bytes");
assert_eq!(binary(1023), "1023 bytes");
}
#[test]
fn test_binary_kibibytes() {
assert_eq!(binary(1_024), "1.0 KiB");
assert_eq!(binary(1_536), "1.5 KiB");
}
#[test]
fn test_binary_mebibytes() {
assert_eq!(binary(1_048_576), "1.0 MiB");
assert_eq!(binary(1_572_864), "1.5 MiB");
}
#[test]
fn test_binary_gibibytes() {
assert_eq!(binary(1_073_741_824), "1.0 GiB");
assert_eq!(binary(1_610_612_736), "1.5 GiB");
}
#[test]
fn test_precision() {
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 test_negative_size() {
assert_eq!(format_size(-1_000, SizeUnit::Decimal, 1), "-1.0 kB");
assert_eq!(format_size(-1_500_000, SizeUnit::Decimal, 1), "-1.5 MB");
}
#[test]
fn test_decimal_speed() {
assert_eq!(decimal_speed(500.0), "500 bytes/s");
assert_eq!(decimal_speed(1_500_000.0), "1.5 MB/s");
assert_eq!(decimal_speed(1_000_000_000.0), "1.0 GB/s");
}
#[test]
fn test_binary_speed() {
assert_eq!(binary_speed(512.0), "512 bytes/s");
assert_eq!(binary_speed(1_048_576.0), "1.0 MiB/s");
assert_eq!(binary_speed(1_073_741_824.0), "1.0 GiB/s");
}
#[test]
fn test_speed_precision() {
assert_eq!(format_speed(1_234_567.0, SizeUnit::Decimal, 2), "1.23 MB/s");
assert_eq!(format_speed(1_234_567.0, SizeUnit::Binary, 2), "1.18 MiB/s");
}
#[test]
fn test_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");
assert_eq!(decimal(1_000_000_000_000_000_000), "1.0 EB");
assert_eq!(binary(1_152_921_504_606_846_976), "1.0 EiB");
}
#[test]
fn test_speed_nan_handling() {
assert_eq!(format_speed(f64::NAN, SizeUnit::Decimal, 1), "NaN");
}
#[test]
fn test_speed_infinity_handling() {
assert_eq!(format_speed(f64::INFINITY, SizeUnit::Decimal, 1), "∞");
assert_eq!(format_speed(f64::NEG_INFINITY, SizeUnit::Decimal, 1), "-∞");
}
}