#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum UnitSystem {
#[default]
Iec,
Si,
}
const IEC_SUFFIXES: &[(u64, &str)] = &[
(1, "B"),
(1 << 10, "KiB"),
(1 << 20, "MiB"),
(1 << 30, "GiB"),
(1u64 << 40, "TiB"),
];
const SI_SUFFIXES: &[(u64, &str)] = &[
(1, "B"),
(1_000, "kB"),
(1_000_000, "MB"),
(1_000_000_000, "GB"),
(1_000_000_000_000, "TB"),
];
impl UnitSystem {
fn suffixes(self) -> &'static [(u64, &'static str)] {
match self {
UnitSystem::Iec => IEC_SUFFIXES,
UnitSystem::Si => SI_SUFFIXES,
}
}
#[must_use]
pub fn format_bytes(self, value: u64) -> String {
let table = self.suffixes();
let mut best = &table[0];
for entry in table {
if value >= entry.0 {
best = entry;
}
}
let scaled = value as f64 / best.0 as f64;
if best.0 == 1 {
format!("{}{}", value, best.1)
} else if scaled >= 100.0 {
format!("{scaled:.0}{}", best.1)
} else if scaled >= 10.0 {
format!("{scaled:.1}{}", best.1)
} else {
format!("{scaled:.2}{}", best.1)
}
}
#[must_use]
pub fn format_rate(self, bytes_per_sec: f64) -> String {
let v = bytes_per_sec.max(0.0) as u64;
format!("{}/s", self.format_bytes(v))
}
}
pub fn parse_size(s: &str, unit_system: UnitSystem) -> Option<u64> {
let s = s.trim();
if s.is_empty() {
return None;
}
let table = unit_system.suffixes();
let (num_part, mult): (&str, u64) = if let Some(pos) = s.find(|c: char| c.is_ascii_alphabetic())
{
let (num, suffix) = s.split_at(pos);
let m = match suffix.chars().next()?.to_ascii_uppercase() {
'B' => 1,
'K' => table[1].0,
'M' => table[2].0,
'G' => table[3].0,
'T' => table[4].0,
_ => return None,
};
(num, m)
} else {
(s, 1)
};
let n: f64 = num_part.trim().parse().ok()?;
if n < 0.0 || !n.is_finite() {
return None;
}
Some((n * mult as f64) as u64)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn format_iec_bytes() {
assert_eq!(UnitSystem::Iec.format_bytes(0), "0B");
assert_eq!(UnitSystem::Iec.format_bytes(512), "512B");
assert_eq!(UnitSystem::Iec.format_bytes(1024), "1.00KiB");
assert_eq!(UnitSystem::Iec.format_bytes(1_048_576), "1.00MiB");
assert_eq!(UnitSystem::Iec.format_bytes(47_185_920), "45.0MiB");
}
#[test]
fn format_si_bytes() {
assert_eq!(UnitSystem::Si.format_bytes(1000), "1.00kB");
assert_eq!(UnitSystem::Si.format_bytes(1_000_000), "1.00MB");
}
#[test]
fn parse_iec_default() {
assert_eq!(parse_size("1024", UnitSystem::Iec), Some(1024));
assert_eq!(parse_size("1K", UnitSystem::Iec), Some(1024));
assert_eq!(parse_size("1M", UnitSystem::Iec), Some(1_048_576));
assert_eq!(parse_size("1.5M", UnitSystem::Iec), Some(1_572_864));
}
#[test]
fn parse_si() {
assert_eq!(parse_size("1k", UnitSystem::Si), Some(1000));
assert_eq!(parse_size("1M", UnitSystem::Si), Some(1_000_000));
}
#[test]
fn parse_rejects_negative() {
assert_eq!(parse_size("-5M", UnitSystem::Iec), None);
}
#[test]
fn parse_rejects_garbage() {
assert_eq!(parse_size("abc", UnitSystem::Iec), None);
assert_eq!(parse_size("", UnitSystem::Iec), None);
}
#[test]
fn format_rate_appends_per_second() {
assert_eq!(UnitSystem::Iec.format_rate(1024.0), "1.00KiB/s");
}
}