const DECIMAL_SUFFIXES: &[&str] = &["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
fn format_with_commas(n: u64) -> String {
let s = n.to_string();
let len = s.len();
if len <= 3 {
return s;
}
let mut result = String::with_capacity(len + (len - 1) / 3);
for (i, ch) in s.chars().enumerate() {
if i > 0 && (len - i) % 3 == 0 {
result.push(',');
}
result.push(ch);
}
result
}
fn format_float_with_commas(value: f64, precision: usize) -> String {
let formatted = format!("{value:.prec$}", prec = precision);
let (int_part, frac_part) = match formatted.split_once('.') {
Some((i, f)) => (i, Some(f)),
None => (formatted.as_str(), None),
};
let len = int_part.len();
let mut result = String::with_capacity(len + (len.saturating_sub(1)) / 3 + 1 + precision);
for (i, ch) in int_part.chars().enumerate() {
if i > 0 && (len - i) % 3 == 0 {
result.push(',');
}
result.push(ch);
}
if let Some(frac) = frac_part {
result.push('.');
result.push_str(frac);
}
result
}
fn base_pow_f64(base: u64, exp: u32) -> f64 {
(base as f64).powi(exp as i32)
}
fn to_str(size: u64, suffixes: &[&str], base: u64, precision: usize, separator: &str) -> String {
if size == 1 {
return "1 byte".to_string();
}
if size < base {
return format!("{} bytes", format_with_commas(size));
}
let size_f = size as f64;
let mut unit_f = 1.0_f64;
let mut suffix = suffixes.last().copied().unwrap_or("");
for (idx, s) in suffixes.iter().enumerate() {
let exp = (idx + 2) as u32;
unit_f = base_pow_f64(base, exp);
if size_f < unit_f {
suffix = s;
break;
}
}
let value = (base as f64) * size_f / unit_f;
format!(
"{}{}{}",
format_float_with_commas(value, precision),
separator,
suffix,
)
}
pub fn pick_unit_and_suffix(size: u64, suffixes: &[&str], base: u64) -> (u64, String) {
let mut unit = 1u64;
let mut chosen_suffix = suffixes.last().copied().unwrap_or("");
for (i, suffix) in suffixes.iter().enumerate() {
unit = base.pow(i as u32);
if size < unit.saturating_mul(base) {
chosen_suffix = suffix;
break;
}
}
(unit, chosen_suffix.to_string())
}
pub fn decimal(size: u64, precision: usize, separator: &str) -> String {
to_str(size, DECIMAL_SUFFIXES, 1000, precision, separator)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn commas_zero() {
assert_eq!(format_with_commas(0), "0");
}
#[test]
fn commas_small() {
assert_eq!(format_with_commas(999), "999");
}
#[test]
fn commas_thousands() {
assert_eq!(format_with_commas(1_000), "1,000");
}
#[test]
fn commas_millions() {
assert_eq!(format_with_commas(1_000_000), "1,000,000");
}
#[test]
fn commas_large() {
assert_eq!(format_with_commas(1_234_567_890), "1,234,567,890");
}
#[test]
fn decimal_zero_bytes() {
assert_eq!(decimal(0, 1, " "), "0 bytes");
}
#[test]
fn decimal_one_byte() {
assert_eq!(decimal(1, 1, " "), "1 byte");
}
#[test]
fn decimal_small_bytes() {
assert_eq!(decimal(999, 1, " "), "999 bytes");
}
#[test]
fn decimal_bytes_with_commas() {
assert_eq!(decimal(500, 1, " "), "500 bytes");
}
#[test]
fn decimal_exactly_1000() {
assert_eq!(decimal(1000, 1, " "), "1.0 kB");
}
#[test]
fn decimal_1500() {
assert_eq!(decimal(1500, 1, " "), "1.5 kB");
}
#[test]
fn decimal_30000() {
assert_eq!(decimal(30000, 1, " "), "30.0 kB");
}
#[test]
fn decimal_999999() {
assert_eq!(decimal(999_999, 1, " "), "1,000.0 kB");
}
#[test]
fn decimal_one_million() {
assert_eq!(decimal(1_000_000, 1, " "), "1.0 MB");
}
#[test]
fn decimal_1_500_000() {
assert_eq!(decimal(1_500_000, 1, " "), "1.5 MB");
}
#[test]
fn decimal_one_billion() {
assert_eq!(decimal(1_000_000_000, 1, " "), "1.0 GB");
}
#[test]
fn decimal_one_trillion() {
assert_eq!(decimal(1_000_000_000_000, 1, " "), "1.0 TB");
}
#[test]
fn decimal_precision_2() {
assert_eq!(decimal(30000, 2, " "), "30.00 kB");
}
#[test]
fn decimal_precision_0() {
assert_eq!(decimal(30000, 0, " "), "30 kB");
}
#[test]
fn decimal_empty_separator() {
assert_eq!(decimal(30000, 2, ""), "30.00kB");
}
#[test]
fn decimal_dash_separator() {
assert_eq!(decimal(30000, 1, "-"), "30.0-kB");
}
#[test]
fn decimal_petabyte() {
assert_eq!(decimal(1_000_000_000_000_000, 1, " "), "1.0 PB");
}
#[test]
fn decimal_exabyte() {
assert_eq!(decimal(1_000_000_000_000_000_000, 1, " "), "1.0 EB");
}
#[test]
fn pick_unit_small_size() {
let suffixes = &["kB", "MB", "GB", "TB"];
let (unit, suffix) = pick_unit_and_suffix(500, suffixes, 1000);
assert_eq!(unit, 1);
assert_eq!(suffix, "kB");
}
#[test]
fn pick_unit_medium_size() {
let suffixes = &["kB", "MB", "GB", "TB"];
let (unit, suffix) = pick_unit_and_suffix(1_500_000, suffixes, 1000);
assert_eq!(unit, 1_000_000);
assert_eq!(suffix, "GB");
}
#[test]
fn pick_unit_large_size() {
let suffixes = &["kB", "MB", "GB", "TB"];
let (unit, suffix) = pick_unit_and_suffix(5_000_000_000, suffixes, 1000);
assert_eq!(unit, 1_000_000_000);
assert_eq!(suffix, "TB");
}
#[test]
fn pick_unit_falls_through() {
let suffixes = &["kB", "MB"];
let (unit, suffix) = pick_unit_and_suffix(999_000_000_000, suffixes, 1000);
assert_eq!(unit, 1000);
assert_eq!(suffix, "MB");
}
}