humfmt 0.2.0

Ergonomic human-readable formatting toolkit for Rust
Documentation
use core::fmt;

use super::{traits::BytesValue, BytesOptions};

const DECIMAL_SHORT: [&str; 7] = ["B", "KB", "MB", "GB", "TB", "PB", "EB"];
const BINARY_SHORT: [&str; 7] = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"];
const DECIMAL_LONG_SINGULAR: [&str; 7] = [
    "byte", "kilobyte", "megabyte", "gigabyte", "terabyte", "petabyte", "exabyte",
];
const DECIMAL_LONG_PLURAL: [&str; 7] = [
    "bytes",
    "kilobytes",
    "megabytes",
    "gigabytes",
    "terabytes",
    "petabytes",
    "exabytes",
];
const BINARY_LONG_SINGULAR: [&str; 7] = [
    "byte", "kibibyte", "mebibyte", "gibibyte", "tebibyte", "pebibyte", "exbibyte",
];
const BINARY_LONG_PLURAL: [&str; 7] = [
    "bytes",
    "kibibytes",
    "mebibytes",
    "gibibytes",
    "tebibytes",
    "pebibytes",
    "exbibytes",
];

pub fn format_bytes(
    f: &mut fmt::Formatter<'_>,
    value: BytesValue,
    options: &BytesOptions,
) -> fmt::Result {
    let raw = to_f64(value);
    let negative = raw.is_sign_negative();
    let abs = raw.abs();
    let base = if options.binary_value() {
        1024.0
    } else {
        1000.0
    };

    let (scaled, idx) = normalize_scaled(abs, base, options.precision_value());
    let rendered = render_scaled(scaled, options.precision_value());

    if negative {
        write!(f, "-")?;
    }

    if options.long_units_value() {
        let label = long_label(options.binary_value(), idx, scaled == 1.0);
        write!(f, "{rendered} {label}")
    } else {
        let suffix = short_label(options.binary_value(), idx);
        write!(f, "{rendered}{suffix}")
    }
}

fn to_f64(value: BytesValue) -> f64 {
    match value {
        BytesValue::Int(v) => v as f64,
        BytesValue::UInt(v) => v as f64,
    }
}

fn normalize_scaled(value: f64, base: f64, precision: u8) -> (f64, usize) {
    let mut scaled = value;
    let mut idx = 0;
    let max_idx = DECIMAL_SHORT.len() - 1;

    while scaled >= base && idx < max_idx {
        scaled /= base;
        idx += 1;
    }

    scaled = round_to(scaled, precision);

    if scaled >= base && idx < max_idx {
        scaled /= base;
        idx += 1;
    }

    (scaled, idx)
}

fn round_to(value: f64, precision: u8) -> f64 {
    let factor = pow10(precision);
    (((value * factor) + 0.5) as u128 as f64) / factor
}

fn pow10(precision: u8) -> f64 {
    let mut factor = 1.0;

    for _ in 0..precision {
        factor *= 10.0;
    }

    factor
}

fn render_scaled(value: f64, precision: u8) -> alloc::string::String {
    let mut out = if is_integer(value) {
        alloc::format!("{:.0}", value)
    } else {
        alloc::format!("{:.*}", precision as usize, value)
    };

    trim_trailing_zeroes(&mut out);
    out
}

fn is_integer(value: f64) -> bool {
    value == (value as u128) as f64
}

fn trim_trailing_zeroes(s: &mut alloc::string::String) {
    if !s.contains('.') {
        return;
    }

    while s.ends_with('0') {
        s.pop();
    }

    if s.ends_with('.') {
        s.pop();
    }
}

fn short_label(binary: bool, idx: usize) -> &'static str {
    if binary {
        BINARY_SHORT[idx]
    } else {
        DECIMAL_SHORT[idx]
    }
}

fn long_label(binary: bool, idx: usize, singular: bool) -> &'static str {
    match (binary, singular) {
        (false, true) => DECIMAL_LONG_SINGULAR[idx],
        (false, false) => DECIMAL_LONG_PLURAL[idx],
        (true, true) => BINARY_LONG_SINGULAR[idx],
        (true, false) => BINARY_LONG_PLURAL[idx],
    }
}