cdx 0.1.24

Library and application for text file manipulation and command line data mining, a little like the gnu textutils
//! format numbers into human readable abbreviations

/// format pi as 22/7
/// default is 3. Given f64, max useful is 16.
#[must_use]
pub fn format_rational(num: f64, digits: u32) -> String {
    let (x, y) = get_rational(num, digits);
    format!("{x}/{y}")
}

pub(crate) fn get_rational(num: f64, digits: u32) -> (i64, u64) {
    if num == 0.0 {
        (0, 1)
    } else if num == 1.0 {
        (1, 1)
    } else if num < 0.0 {
        let (x, y) = get_rational2(-num, digits);
        (-x.cast_signed(), y)
    } else {
        let (x, y) = get_rational2(num, digits);
        (x.cast_signed(), y)
    }
}

fn get_rational2(num: f64, digits: u32) -> (u64, u64) {
    assert!(num > 0.0);
    assert!(num != 1.0);
    let slop = 10.0f64.powi(-(digits.cast_signed()));
    if num <= 1.0 { get_rational3(num, slop) } else { get_rational4(num, slop) }
}

fn good_enough(numer: f64, denom: f64, num: f64, slop: f64) -> bool {
    let guess = numer / denom;
    (num - guess).abs() <= slop * num
}

fn get_rational3(num: f64, slop: f64) -> (u64, u64) {
    assert!(num > 0.0);
    assert!(num < 1.0);

    let mut numer = 1.0;
    loop {
        let denom = (numer / num).round();
        if good_enough(numer, denom, num, slop) {
            return (numer.round() as u64, denom.round() as u64);
        }
        numer += 1.0;
    }
}

fn get_rational4(num: f64, slop: f64) -> (u64, u64) {
    assert!(num > 1.0);
    let mut denom = 1.0;
    loop {
        let numer = (num * denom).round();
        if good_enough(numer, denom, num, slop) {
            return (numer.round() as u64, denom.round() as u64);
        }
        denom += 1.0;
    }
}

#[must_use]
/// Format f64 into something short and readable, like 42K or 1.5M
/// Value is rounded to the nearest integer.
///
/// Returned string is at most 5 characters, including optional `-` sign
/// 0-999 are themselves
/// The other formats are 1K, 1.2K, 12K, 123K
/// Where K == 1024, and M, G, T, P, E, Z, Y, R, Q each increase by a factor of 1024
/// The number represented by the string is never less than the original number
/// e.g 1000 = "1K", 1024="1K", 1025="1.1K"
pub fn format_power2f(num: f64) -> String {
    if num < 0.0 {
        let mut ret = "-".to_string();
        ret.push_str(&format_inner(-num, false));
        ret
    } else {
        format_inner(num, false)
    }
}

#[must_use]
/// Format f64 into something short and readable, like 42k or 1.5m
/// Value is rounded to the nearest integer.
///
/// Returned string is at most 5 characters, including optional `-` sign
/// 0-999 are themselves
/// The other formats are 1k, 1.2k, 12k, 123k
/// Where K == 1000, and m, g, t, p, e, z, y, r, q each increase by a factor of 1000
/// The number represented by the string is never less than the original number
/// e.g 1000 = "1k", 1001="1.1k"
pub fn format_power10f(num: f64) -> String {
    if num < 0.0 {
        let mut ret = "-".to_string();
        ret.push_str(&format_inner(-num, true));
        ret
    } else {
        format_inner(num, true)
    }
}

#[must_use]
/// as `format_power2f`
pub fn format_power2(num: u64) -> String {
    #[expect(clippy::cast_precision_loss, reason = "precision loss ok")]
    format_power2f(num as f64)
}

#[must_use]
/// as `format_power10f`
pub fn format_power10(num: u64) -> String {
    #[expect(clippy::cast_precision_loss, reason = "precision loss ok")]
    format_power10f(num as f64)
}

#[must_use]
fn format_inner(num: f64, is_10: bool) -> String {
    if num < 1000.0 {
        return format!("{num:.0}");
    }
    let mut curr_exp = 1f64;
    let mut exp =
        if is_10 { crate::num::P10_LETTERS.iter() } else { crate::num::P2_LETTERS.iter() };
    let bump = if is_10 { 1000.0 } else { 1024.0 };

    loop {
        let e = exp.next().unwrap();
        if num <= (999.0 * curr_exp) {
            if num > (9.9 * curr_exp) {
                return format!("{:.0}{}", (num / curr_exp).ceil(), *e as char);
            }
            if num > curr_exp {
                return format!("{:.1}{}", (num * 10.0 / curr_exp).ceil() / 10.0, *e as char);
            }
            return format!("1{}", *e as char);
        }
        if *e == b'Q' {
            return format!("{:.0}Q", num / curr_exp);
        }
        curr_exp *= bump;
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_format2() {
        assert_eq!(format_power2(0), "0");
        assert_eq!(format_power2(1), "1");
        assert_eq!(format_power2f(-1f64), "-1");
        assert_eq!(format_power2(999), "999");
        assert_eq!(format_power2(1000), "1K");
        assert_eq!(format_power2(1024), "1K");
        assert_eq!(format_power2(1025), "1.1K");
        assert_eq!(format_power2(10137), "9.9K");
        assert_eq!(format_power2(10138), "10K");
        assert_eq!(format_power2(97 * 1024), "97K");
        assert_eq!(format_power2(97 * 1024 + 1), "98K");
        assert_eq!(format_power2(999 * 1024), "999K");
        assert_eq!(format_power2(999 * 1024 + 1), "1M");
    }
    #[test]
    fn test_format10() {
        assert_eq!(format_power10(0), "0");
        assert_eq!(format_power10(1), "1");
        assert_eq!(format_power10f(-1f64), "-1");
        assert_eq!(format_power10(999), "999");
        assert_eq!(format_power10(1000), "1k");
        assert_eq!(format_power10(1001), "1.1k");
        assert_eq!(format_power10(9900), "9.9k");
        assert_eq!(format_power10(9901), "10k");
        assert_eq!(format_power10(10000), "10k");
        assert_eq!(format_power10(97000), "97k");
        assert_eq!(format_power10(97001), "98k");
        assert_eq!(format_power10(999_000), "999k");
        assert_eq!(format_power10(999_001), "1m");
    }
}