Skip to main content

ruso_runtime/runtime/
duration.rs

1use std::time::Duration;
2
3use crate::runtime::error::RuntimeError;
4
5/// Parse a duration string with one of the supported suffixes:
6///
7/// | suffix | unit           |
8/// |--------|----------------|
9/// | `ms`   | milliseconds   |
10/// | `s`    | seconds        |
11/// | `m`    | minutes        |
12/// | `h`    | hours          |
13/// | `d`    | days           |
14///
15/// Whitespace between the number and the suffix is allowed. Earlier
16/// revisions only supported `ms` and `s`, which forced `5m` to be
17/// written as `300s` and made script-level timeouts awkward.
18pub fn parse_duration(input: &str) -> Result<Duration, RuntimeError> {
19    let trimmed = input.trim();
20    let (digits, unit) =
21        split_suffix(trimmed).ok_or_else(|| RuntimeError::InvalidDuration(input.to_string()))?;
22    let value: u64 = digits
23        .trim()
24        .parse()
25        .map_err(|_| RuntimeError::InvalidDuration(input.to_string()))?;
26    let scaled = match unit {
27        "ms" => Duration::from_millis(value),
28        "s" => Duration::from_secs(value),
29        "m" => Duration::from_secs(value.saturating_mul(60)),
30        "h" => Duration::from_secs(value.saturating_mul(3600)),
31        "d" => Duration::from_secs(value.saturating_mul(86_400)),
32        _ => return Err(RuntimeError::InvalidDuration(input.to_string())),
33    };
34    Ok(scaled)
35}
36
37/// Split `"500ms"` into `("500", "ms")`. Returns `None` if no recognised
38/// suffix is present. Longest-suffix match so `ms` wins over `s`.
39fn split_suffix(input: &str) -> Option<(&str, &str)> {
40    for suffix in ["ms", "s", "m", "h", "d"] {
41        if let Some(stripped) = input.strip_suffix(suffix) {
42            return Some((stripped, suffix));
43        }
44    }
45    None
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51
52    #[test]
53    fn ms_unit() {
54        assert_eq!(parse_duration("250ms").unwrap(), Duration::from_millis(250));
55    }
56
57    #[test]
58    fn s_unit() {
59        assert_eq!(parse_duration("3s").unwrap(), Duration::from_secs(3));
60    }
61
62    #[test]
63    fn m_unit() {
64        assert_eq!(parse_duration("5m").unwrap(), Duration::from_secs(300));
65    }
66
67    #[test]
68    fn h_unit() {
69        assert_eq!(parse_duration("2h").unwrap(), Duration::from_secs(7200));
70    }
71
72    #[test]
73    fn d_unit() {
74        assert_eq!(parse_duration("1d").unwrap(), Duration::from_secs(86_400));
75    }
76
77    #[test]
78    fn ms_wins_over_s_suffix() {
79        // `500ms` must not be parsed as `500m` + leftover `s`.
80        assert_eq!(parse_duration("500ms").unwrap(), Duration::from_millis(500));
81    }
82
83    #[test]
84    fn rejects_missing_suffix() {
85        assert!(parse_duration("42").is_err());
86    }
87
88    #[test]
89    fn rejects_unknown_suffix() {
90        assert!(parse_duration("5z").is_err());
91    }
92
93    #[test]
94    fn rejects_negative_value() {
95        assert!(parse_duration("-1s").is_err());
96    }
97}