Skip to main content

sova_core/
human.rs

1//! Human-readable sizes and durations (`"2 MiB"`, `"30s"`).
2
3use std::time::Duration;
4
5/// Parse `"2 MiB"`, `"512 KiB"`, `"1024"`, `"1MB"` → bytes.
6pub fn parse_bytes(input: &str) -> Result<usize, String> {
7    let s = input.trim();
8    if s.is_empty() {
9        return Err("empty size".into());
10    }
11    let (num_str, unit) = split_num_unit(s)?;
12    let n: f64 = num_str
13        .parse()
14        .map_err(|_| format!("invalid size number: {num_str}"))?;
15    if n < 0.0 {
16        return Err("size must be non-negative".into());
17    }
18    let mult = match unit {
19        "" | "b" | "B" => 1.0,
20        "k" | "K" | "kb" | "KB" | "KiB" => 1024.0,
21        "m" | "M" | "mb" | "MB" | "MiB" => 1024.0 * 1024.0,
22        "g" | "G" | "gb" | "GB" | "GiB" => 1024.0 * 1024.0 * 1024.0,
23        other => return Err(format!("unknown size unit: {other}")),
24    };
25    Ok((n * mult) as usize)
26}
27
28/// Parse `"30s"`, `"5m"`, `"1h"`, `"500ms"` → [`Duration`].
29pub fn parse_duration(input: &str) -> Result<Duration, String> {
30    let s = input.trim();
31    if s.is_empty() {
32        return Err("empty duration".into());
33    }
34    let (num_str, unit) = split_num_unit(s)?;
35    let n: f64 = num_str
36        .parse()
37        .map_err(|_| format!("invalid duration number: {num_str}"))?;
38    if n < 0.0 {
39        return Err("duration must be non-negative".into());
40    }
41    let secs = match unit {
42        "" | "s" | "sec" | "secs" | "second" | "seconds" => n,
43        "ms" | "millis" | "millisecond" | "milliseconds" => n / 1000.0,
44        "m" | "min" | "mins" | "minute" | "minutes" => n * 60.0,
45        "h" | "hr" | "hour" | "hours" => n * 3600.0,
46        other => return Err(format!("unknown duration unit: {other}")),
47    };
48    Ok(Duration::from_secs_f64(secs))
49}
50
51fn split_num_unit(s: &str) -> Result<(&str, &str), String> {
52    let end = s
53        .char_indices()
54        .find(|(_, c)| !(c.is_ascii_digit() || *c == '.'))
55        .map(|(i, _)| i)
56        .unwrap_or(s.len());
57    if end == 0 {
58        return Err(format!("missing number in `{s}`"));
59    }
60    Ok((&s[..end], s[end..].trim()))
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    #[test]
68    fn bytes_units() {
69        assert_eq!(parse_bytes("1024").unwrap(), 1024);
70        assert_eq!(parse_bytes("2 KiB").unwrap(), 2048);
71        assert_eq!(parse_bytes("1MiB").unwrap(), 1024 * 1024);
72        assert_eq!(parse_bytes("50 MiB").unwrap(), 50 * 1024 * 1024);
73    }
74
75    #[test]
76    fn duration_units() {
77        assert_eq!(parse_duration("30s").unwrap(), Duration::from_secs(30));
78        assert_eq!(parse_duration("500ms").unwrap(), Duration::from_millis(500));
79        assert_eq!(parse_duration("2m").unwrap(), Duration::from_secs(120));
80    }
81}