Skip to main content

rskit_util/
bytes.rs

1//! Formatting and parsing human-readable data sizes.
2
3use crate::parse_decimal_scaled;
4
5/// Formats a raw byte count into a human-readable string (e.g. `1048576` -> `"1.00 MB"`).
6/// Supported units: B, KB, MB, GB, TB, PB.
7///
8/// # Examples
9///
10/// ```
11/// use rskit_util::bytes::format_bytes;
12/// assert_eq!(format_bytes(1024), "1.00 KB");
13/// assert_eq!(format_bytes(1048576), "1.00 MB");
14/// ```
15pub fn format_bytes(bytes: u64) -> String {
16    const UNITS: [&str; 6] = ["B", "KB", "MB", "GB", "TB", "PB"];
17    const BASE: u64 = 1024;
18
19    let mut unit = 0;
20    let mut divisor = 1_u64;
21    while unit + 1 < UNITS.len() && bytes / divisor >= BASE {
22        unit += 1;
23        divisor = divisor.saturating_mul(BASE);
24    }
25
26    if unit == 0 {
27        format!("{bytes} B")
28    } else {
29        let whole = bytes / divisor;
30        let remainder = bytes % divisor;
31        let mut hundredths =
32            ((u128::from(remainder) * 100) + (u128::from(divisor) / 2)) / u128::from(divisor);
33        let mut whole = whole;
34        if hundredths == 100 {
35            whole = whole.saturating_add(1);
36            hundredths = 0;
37        }
38        format!("{whole}.{hundredths:02} {}", UNITS[unit])
39    }
40}
41
42/// Parses human-readable sizes (e.g. `"5MB"`, `"10KB"`, `"2GB"`) into a raw byte count (`u64`).
43/// Case-insensitive, supports optional space, and treats unit-less values as bytes.
44///
45/// All units are binary (1024-based). The two-letter (`kb`), single-letter (`k`), and
46/// binary-explicit (`ki`) spellings are accepted as aliases for the same power of 1024.
47///
48/// # Examples
49///
50/// ```
51/// use rskit_util::bytes::parse_bytes;
52/// assert_eq!(parse_bytes("512"), Some(512));
53/// assert_eq!(parse_bytes("1 KB"), Some(1024));
54/// assert_eq!(parse_bytes("5mb"), Some(5242880));
55/// assert_eq!(parse_bytes("2Mi"), Some(2 * 1024 * 1024));
56/// ```
57pub fn parse_bytes(s: &str) -> Option<u64> {
58    let s = s.trim().to_lowercase();
59    let (num_part, unit_part) = s.split_at(s.find(|c: char| c.is_alphabetic()).unwrap_or(s.len()));
60    let unit = unit_part.trim();
61
62    let multiplier = match unit {
63        "b" | "" => 1_u128,
64        "kb" | "k" | "ki" => 1024_u128,
65        "mb" | "m" | "mi" => 1024_u128.pow(2),
66        "gb" | "g" | "gi" => 1024_u128.pow(3),
67        "tb" | "t" | "ti" => 1024_u128.pow(4),
68        "pb" | "p" | "pi" => 1024_u128.pow(5),
69        _ => return None,
70    };
71
72    parse_decimal_scaled(num_part.trim(), multiplier).and_then(|bytes| u64::try_from(bytes).ok())
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn test_format_bytes() {
81        assert_eq!(format_bytes(0), "0 B");
82        assert_eq!(format_bytes(512), "512 B");
83        assert_eq!(format_bytes(1024), "1.00 KB");
84        assert_eq!(format_bytes(1024 * 1024 * 5), "5.00 MB");
85        assert_eq!(format_bytes(1024 * 1024 * 1024 * 2), "2.00 GB");
86    }
87
88    #[test]
89    fn test_parse_bytes() {
90        assert_eq!(parse_bytes("512"), Some(512));
91        assert_eq!(parse_bytes("512b"), Some(512));
92        assert_eq!(parse_bytes("1 KB"), Some(1024));
93        assert_eq!(parse_bytes("5mb"), Some(5_242_880));
94        assert_eq!(parse_bytes("2gb"), Some(2_147_483_648));
95        assert_eq!(parse_bytes("1.5 KB"), Some(1536));
96        assert_eq!(parse_bytes("2Mi"), Some(2 * 1024 * 1024));
97        assert_eq!(parse_bytes(" 1Ti "), Some(1024_u64.pow(4)));
98        assert_eq!(parse_bytes("-1 KB"), None);
99        assert_eq!(parse_bytes("18446744073709551616"), None);
100        assert_eq!(parse_bytes("invalid"), None);
101    }
102}