garudadidada 1.0.0

Tiny zero-dependency helpers to turn machine values (bytes, durations, ordinals) into human-readable strings.
Documentation
//! Tiny, zero-dependency helpers to turn machine values into human-readable strings.
//!
//! All functions use only the standard library and never panic on valid input.
//!
//! # Examples
//!
//! ```
//! use humanize_values::{format_bytes, format_duration, ordinal, pluralize};
//!
//! assert_eq!(format_bytes(1536), "1.5 KB");
//! assert_eq!(format_duration(3661), "1h 1m 1s");
//! assert_eq!(ordinal(21), "21st");
//! assert_eq!(pluralize(3, "file", "files"), "3 files");
//! ```

#![forbid(unsafe_code)]

const BYTE_UNITS: [&str; 9] = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];

/// Formats a byte count as a human-readable string using binary (1024-based) units.
///
/// # Examples
///
/// ```
/// # use humanize_values::format_bytes;
/// assert_eq!(format_bytes(0), "0 B");
/// assert_eq!(format_bytes(1023), "1023 B");
/// assert_eq!(format_bytes(1024), "1 KB");
/// assert_eq!(format_bytes(1536), "1.5 KB");
/// ```
pub fn format_bytes(bytes: u64) -> String {
    if bytes < 1024 {
        return format!("{bytes} B");
    }
    let mut value = bytes as f64;
    let mut unit = 0;
    while value >= 1024.0 && unit < BYTE_UNITS.len() - 1 {
        value /= 1024.0;
        unit += 1;
    }
    let rounded = (value * 10.0).round() / 10.0;
    if rounded.fract() == 0.0 {
        format!("{} {}", rounded as u64, BYTE_UNITS[unit])
    } else {
        format!("{rounded:.1} {}", BYTE_UNITS[unit])
    }
}

/// Formats a duration given in seconds as a compact `d`/`h`/`m`/`s` string.
///
/// # Examples
///
/// ```
/// # use humanize_values::format_duration;
/// assert_eq!(format_duration(0), "0s");
/// assert_eq!(format_duration(90), "1m 30s");
/// assert_eq!(format_duration(3661), "1h 1m 1s");
/// ```
pub fn format_duration(seconds: u64) -> String {
    if seconds == 0 {
        return "0s".to_string();
    }
    let days = seconds / 86_400;
    let hours = (seconds % 86_400) / 3_600;
    let minutes = (seconds % 3_600) / 60;
    let secs = seconds % 60;

    let mut parts = Vec::new();
    if days > 0 {
        parts.push(format!("{days}d"));
    }
    if hours > 0 {
        parts.push(format!("{hours}h"));
    }
    if minutes > 0 {
        parts.push(format!("{minutes}m"));
    }
    if secs > 0 {
        parts.push(format!("{secs}s"));
    }
    parts.join(" ")
}

/// Adds an English ordinal suffix to an integer.
///
/// # Examples
///
/// ```
/// # use humanize_values::ordinal;
/// assert_eq!(ordinal(1), "1st");
/// assert_eq!(ordinal(11), "11th");
/// assert_eq!(ordinal(22), "22nd");
/// ```
pub fn ordinal(n: i64) -> String {
    let abs = n.unsigned_abs();
    let last_two = abs % 100;
    let last_one = abs % 10;
    let suffix = if (11..=13).contains(&last_two) {
        "th"
    } else {
        match last_one {
            1 => "st",
            2 => "nd",
            3 => "rd",
            _ => "th",
        }
    };
    format!("{n}{suffix}")
}

/// Returns `"<count> <word>"`, choosing the singular or plural form based on `count`.
///
/// # Examples
///
/// ```
/// # use humanize_values::pluralize;
/// assert_eq!(pluralize(1, "file", "files"), "1 file");
/// assert_eq!(pluralize(3, "file", "files"), "3 files");
/// ```
pub fn pluralize(count: i64, singular: &str, plural: &str) -> String {
    let word = if count.abs() == 1 { singular } else { plural };
    format!("{count} {word}")
}

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

    #[test]
    fn bytes() {
        assert_eq!(format_bytes(0), "0 B");
        assert_eq!(format_bytes(512), "512 B");
        assert_eq!(format_bytes(1023), "1023 B");
        assert_eq!(format_bytes(1024), "1 KB");
        assert_eq!(format_bytes(1536), "1.5 KB");
        assert_eq!(format_bytes(1_048_576), "1 MB");
        assert_eq!(format_bytes(1_073_741_824), "1 GB");
    }

    #[test]
    fn duration() {
        assert_eq!(format_duration(0), "0s");
        assert_eq!(format_duration(45), "45s");
        assert_eq!(format_duration(90), "1m 30s");
        assert_eq!(format_duration(3661), "1h 1m 1s");
        assert_eq!(format_duration(90_061), "1d 1h 1m 1s");
    }

    #[test]
    fn ordinals() {
        assert_eq!(ordinal(1), "1st");
        assert_eq!(ordinal(2), "2nd");
        assert_eq!(ordinal(3), "3rd");
        assert_eq!(ordinal(4), "4th");
        assert_eq!(ordinal(11), "11th");
        assert_eq!(ordinal(12), "12th");
        assert_eq!(ordinal(13), "13th");
        assert_eq!(ordinal(21), "21st");
        assert_eq!(ordinal(101), "101st");
        assert_eq!(ordinal(111), "111th");
    }

    #[test]
    fn plurals() {
        assert_eq!(pluralize(1, "file", "files"), "1 file");
        assert_eq!(pluralize(3, "file", "files"), "3 files");
        assert_eq!(pluralize(0, "file", "files"), "0 files");
        assert_eq!(pluralize(2, "person", "people"), "2 people");
    }
}