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/// # Examples
46///
47/// ```
48/// use rskit_util::bytes::parse_bytes;
49/// assert_eq!(parse_bytes("512"), Some(512));
50/// assert_eq!(parse_bytes("1 KB"), Some(1024));
51/// assert_eq!(parse_bytes("5mb"), Some(5242880));
52/// ```
53pub fn parse_bytes(s: &str) -> Option<u64> {
54    let s = s.trim().to_lowercase();
55    let (num_part, unit_part) = s.split_at(s.find(|c: char| c.is_alphabetic()).unwrap_or(s.len()));
56    let unit = unit_part.trim();
57
58    let multiplier = match unit {
59        "b" | "" => 1_u128,
60        "kb" | "k" => 1024_u128,
61        "mb" | "m" => 1024_u128.pow(2),
62        "gb" | "g" => 1024_u128.pow(3),
63        "tb" | "t" => 1024_u128.pow(4),
64        "pb" | "p" => 1024_u128.pow(5),
65        _ => return None,
66    };
67
68    parse_decimal_scaled(num_part.trim(), multiplier).and_then(|bytes| u64::try_from(bytes).ok())
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn test_format_bytes() {
77        assert_eq!(format_bytes(0), "0 B");
78        assert_eq!(format_bytes(512), "512 B");
79        assert_eq!(format_bytes(1024), "1.00 KB");
80        assert_eq!(format_bytes(1024 * 1024 * 5), "5.00 MB");
81        assert_eq!(format_bytes(1024 * 1024 * 1024 * 2), "2.00 GB");
82    }
83
84    #[test]
85    fn test_parse_bytes() {
86        assert_eq!(parse_bytes("512"), Some(512));
87        assert_eq!(parse_bytes("512b"), Some(512));
88        assert_eq!(parse_bytes("1 KB"), Some(1024));
89        assert_eq!(parse_bytes("5mb"), Some(5_242_880));
90        assert_eq!(parse_bytes("2gb"), Some(2_147_483_648));
91        assert_eq!(parse_bytes("1.5 KB"), Some(1536));
92        assert_eq!(parse_bytes("-1 KB"), None);
93        assert_eq!(parse_bytes("18446744073709551616"), None);
94        assert_eq!(parse_bytes("invalid"), None);
95    }
96}