Skip to main content

soar_utils/
bytes.rs

1use crate::error::{BytesError, BytesResult};
2
3/// Formats a number of bytes into a human-readable string.
4///
5/// This method converts a byte count into a string with appropriate units (B, KiB, MiB, etc.)
6/// and a specified level of precision.
7///
8/// # Arguments
9///
10/// * `bytes` - The number of bytes to format
11/// * `precision` - The number of decimal places to display
12///
13/// # Returns
14///
15/// A human-readable string representation of the byte count.
16///
17/// # Example
18///
19/// ```
20/// use soar_utils::bytes::format_bytes;
21///
22/// let bytes = 1024_u64.pow(2);
23/// let formatted = format_bytes(bytes, 2);
24///
25/// assert_eq!(formatted, "1.00 MiB");
26/// ```
27pub fn format_bytes(bytes: u64, precision: usize) -> String {
28    let unit = 1024.0;
29    let sizes = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"];
30
31    let idx = (bytes as f64).log(unit).floor() as usize;
32    let idx = idx.min(sizes.len() - 1);
33
34    format!(
35        "{:.*} {}",
36        precision,
37        bytes as f64 / unit.powi(idx.try_into().unwrap()),
38        sizes[idx]
39    )
40}
41
42/// Parses a human-readable byte string into a number of bytes.
43///
44/// This method converts a string with units (e.g., "1.00 MiB", "1KB") into a `u64` byte count.
45/// It supports both binary (KiB, MiB) and decimal (KB, MB) prefixes.
46///
47/// # Arguments
48///
49/// * `s` - The string to parse
50///
51/// # Returns
52///
53/// Returns the number of bytes as a `u64`, or a [`BytesError`] if the string is invalid.
54///
55/// # Errors
56///
57/// * [`BytesError::ParseFailed`] if the string has an invalid format or suffix.
58///
59/// # Example
60///
61/// ```
62/// use soar_utils::bytes::parse_bytes;
63///
64/// let bytes = parse_bytes("1.00 MiB").unwrap();
65///
66/// assert_eq!(bytes, 1024_u64.pow(2));
67/// ```
68pub fn parse_bytes(s: &str) -> BytesResult<u64> {
69    let mut size = s.trim().to_uppercase();
70
71    // If it's a number, just return it
72    if let Ok(v) = size.parse::<u64>() {
73        return Ok(v);
74    };
75
76    let prefixes = ["", "K", "M", "G", "T", "P", "E"];
77
78    let base: f64 = if size.ends_with("IB") {
79        size.truncate(size.len() - 2);
80        1024.0
81    } else if size.ends_with("B") {
82        size.truncate(size.len() - 1);
83        1000.0
84    } else {
85        return Err(BytesError::ParseFailed {
86            input: s.to_string(),
87            reason: "Invalid suffix".to_string(),
88        });
89    };
90
91    prefixes
92        .iter()
93        .enumerate()
94        .rev()
95        .find_map(|(i, p)| {
96            size.strip_suffix(p).and_then(|num| {
97                num.trim()
98                    .parse::<f64>()
99                    .ok()
100                    .map(|n| n * base.powi(i.try_into().unwrap()))
101                    .map(|n| n.round() as u64)
102            })
103        })
104        .ok_or_else(|| {
105            BytesError::ParseFailed {
106                input: s.to_string(),
107                reason: "Unrecognized size format".into(),
108            }
109        })
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn test_format_bytes_with_precisions() {
118        assert_eq!(format_bytes(1111, 0), "1 KiB");
119
120        assert_eq!(format_bytes(0, 0), "0 B");
121        assert_eq!(format_bytes(0, 3), "0.000 B");
122        assert_eq!(format_bytes(1023, 0), "1023 B");
123        assert_eq!(format_bytes(1023, 2), "1023.00 B");
124
125        assert_eq!(format_bytes(1024, 0), "1 KiB");
126        assert_eq!(format_bytes(1024, 1), "1.0 KiB");
127        assert_eq!(format_bytes(1536, 2), "1.50 KiB");
128        assert_eq!(format_bytes(2047, 3), "1.999 KiB");
129        assert_eq!(format_bytes(2048, 4), "2.0000 KiB");
130
131        assert_eq!(format_bytes(1024_u64.pow(2), 0), "1 MiB");
132        assert_eq!(format_bytes(3 * 1024_u64.pow(2) / 2, 2), "1.50 MiB");
133        assert_eq!(format_bytes(2 * 1024_u64.pow(2) - 1, 3), "2.000 MiB");
134
135        assert_eq!(format_bytes(1024_u64.pow(3), 2), "1.00 GiB");
136        assert_eq!(format_bytes(5 * 1024_u64.pow(3) / 2, 1), "2.5 GiB");
137
138        assert_eq!(format_bytes(1024_u64.pow(4), 3), "1.000 TiB");
139        assert_eq!(format_bytes(3 * 1024_u64.pow(4) / 2, 2), "1.50 TiB");
140
141        assert_eq!(format_bytes(1024_u64.pow(5), 0), "1 PiB");
142        assert_eq!(
143            format_bytes(1024_u64.pow(5) + 512 * 1024_u64.pow(4), 2),
144            "1.50 PiB"
145        );
146
147        assert_eq!(format_bytes(1024_u64.pow(6), 1), "1.0 EiB");
148        assert_eq!(
149            format_bytes(1024_u64.pow(6) + 512 * 1024_u64.pow(5), 3),
150            "1.500 EiB"
151        );
152    }
153
154    #[test]
155    fn test_parse_bytes() {
156        assert_eq!(parse_bytes("111").unwrap(), 111);
157
158        assert_eq!(parse_bytes("42").unwrap(), 42);
159        assert_eq!(parse_bytes(" 120 ").unwrap(), 120);
160
161        assert_eq!(parse_bytes("0B").unwrap(), 0);
162        assert_eq!(parse_bytes("1B").unwrap(), 1);
163        assert_eq!(parse_bytes("1023B").unwrap(), 1023);
164
165        assert_eq!(parse_bytes("1KiB").unwrap(), 1024);
166        assert_eq!(parse_bytes("1.50KiB").unwrap(), 3 * 1024 / 2);
167        assert_eq!(parse_bytes("1KB").unwrap(), 1000);
168        assert_eq!(parse_bytes("1.50KB").unwrap(), 3 * 1000 / 2);
169
170        assert_eq!(parse_bytes("1MiB").unwrap(), 1024_u64.pow(2));
171        assert_eq!(parse_bytes("1.50MiB").unwrap(), 3 * 1024_u64.pow(2) / 2);
172        assert_eq!(parse_bytes("1MB").unwrap(), 1000_u64.pow(2));
173        assert_eq!(parse_bytes("1.50MB").unwrap(), 3 * 1000_u64.pow(2) / 2);
174
175        assert_eq!(parse_bytes("1GiB").unwrap(), 1024_u64.pow(3));
176        assert_eq!(parse_bytes("1.50GiB").unwrap(), 3 * 1024_u64.pow(3) / 2);
177        assert_eq!(parse_bytes("1GB").unwrap(), 1000_u64.pow(3));
178        assert_eq!(parse_bytes("1.50GB").unwrap(), 3 * 1000_u64.pow(3) / 2);
179
180        assert_eq!(parse_bytes("1TiB").unwrap(), 1024_u64.pow(4));
181        assert_eq!(parse_bytes("1.50TiB").unwrap(), 3 * 1024_u64.pow(4) / 2);
182        assert_eq!(parse_bytes("1TB").unwrap(), 1000_u64.pow(4));
183        assert_eq!(parse_bytes("1.50TB").unwrap(), 3 * 1000_u64.pow(4) / 2);
184
185        assert_eq!(parse_bytes("1PiB").unwrap(), 1024_u64.pow(5));
186        assert_eq!(parse_bytes("1.50PiB").unwrap(), 3 * 1024_u64.pow(5) / 2);
187        assert_eq!(parse_bytes("1PB").unwrap(), 1000_u64.pow(5));
188        assert_eq!(parse_bytes("1.50PB").unwrap(), 3 * 1000_u64.pow(5) / 2);
189
190        assert_eq!(parse_bytes("1EiB").unwrap(), 1024_u64.pow(6));
191        assert_eq!(parse_bytes("1.50EiB").unwrap(), 3 * 1024_u64.pow(6) / 2);
192        assert_eq!(parse_bytes("1EB").unwrap(), 1000_u64.pow(6));
193        assert_eq!(parse_bytes("1.50EB").unwrap(), 3 * 1000_u64.pow(6) / 2);
194    }
195
196    #[test]
197    fn test_fail_parse_bytes() {
198        assert!(parse_bytes("1.xE").is_err());
199        assert!(parse_bytes("1.xEB").is_err());
200        assert!(parse_bytes("1.50FB").is_err());
201        assert!(parse_bytes("1LB ").is_err());
202        assert!(parse_bytes(" 1.50Li").is_err());
203        assert!(parse_bytes(" MiB ").is_err());
204        assert!(parse_bytes("MB").is_err());
205    }
206}