Skip to main content

rskit_util/time/
duration.rs

1//! Duration string parsing and formatting.
2
3use crate::parse_decimal_scaled;
4use std::time::Duration;
5
6/// Formats a `Duration` into a human-readable string.
7///
8/// # Examples
9///
10/// ```
11/// use std::time::Duration;
12/// use rskit_util::time::format_duration;
13/// assert_eq!(format_duration(Duration::from_secs(5)), "5.00s");
14/// assert_eq!(format_duration(Duration::from_millis(152)), "152ms");
15/// ```
16#[must_use]
17pub fn format_duration(d: Duration) -> String {
18    let secs = d.as_secs_f64();
19    if secs >= 3600.0 {
20        format!("{:.2}h", secs / 3600.0)
21    } else if secs >= 60.0 {
22        format!("{:.2}m", secs / 60.0)
23    } else if secs >= 1.0 {
24        format!("{secs:.2}s")
25    } else if d.as_micros() >= 1000 {
26        format!("{}ms", d.as_millis())
27    } else if d.as_nanos() >= 1000 {
28        format!("{}μs", d.as_micros())
29    } else {
30        format!("{}ns", d.as_nanos())
31    }
32}
33
34/// Parses simple duration strings like `"5s"`, `"10m"`, `"1h"` into a `Duration`.
35/// Case-insensitive, supports optional space, and treats unit-less values as seconds.
36///
37/// # Examples
38///
39/// ```
40/// use std::time::Duration;
41/// use rskit_util::time::parse_duration;
42/// assert_eq!(parse_duration("5"), Some(Duration::from_secs(5)));
43/// assert_eq!(parse_duration("5s"), Some(Duration::from_secs(5)));
44/// assert_eq!(parse_duration("10m"), Some(Duration::from_secs(600)));
45/// ```
46#[must_use]
47pub fn parse_duration(s: &str) -> Option<Duration> {
48    let s = s.trim().to_lowercase();
49    let (num_part, unit_part) = s.split_at(s.find(|c: char| c.is_alphabetic()).unwrap_or(s.len()));
50    let unit = unit_part.trim();
51
52    let nanos_per_unit = match unit {
53        "ns" => 1_u128,
54        "us" | "μs" => 1_000,
55        "ms" => 1_000_000,
56        "s" | "" => 1_000_000_000,
57        "m" | "min" => 60 * 1_000_000_000,
58        "h" | "hr" => 3_600 * 1_000_000_000,
59        "d" | "day" => 86_400 * 1_000_000_000,
60        _ => return None,
61    };
62
63    let nanos = parse_decimal_scaled(num_part.trim(), nanos_per_unit)?;
64    if nanos > Duration::MAX.as_nanos() {
65        return None;
66    }
67    let secs = nanos / 1_000_000_000;
68    let subsec_nanos = nanos % 1_000_000_000;
69    Some(Duration::new(
70        u64::try_from(secs).ok()?,
71        u32::try_from(subsec_nanos).ok()?,
72    ))
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn test_format_duration() {
81        assert_eq!(format_duration(Duration::from_hours(2)), "2.00h");
82        assert_eq!(format_duration(Duration::from_secs(150)), "2.50m");
83        assert_eq!(format_duration(Duration::from_secs(5)), "5.00s");
84        assert_eq!(format_duration(Duration::from_millis(250)), "250ms");
85    }
86
87    #[test]
88    fn test_parse_duration() {
89        assert_eq!(parse_duration("5"), Some(Duration::from_secs(5)));
90        assert_eq!(parse_duration("5s"), Some(Duration::from_secs(5)));
91        assert_eq!(parse_duration("10m"), Some(Duration::from_mins(10)));
92        assert_eq!(parse_duration("1.5h"), Some(Duration::from_mins(90)));
93        assert_eq!(parse_duration("1.5ms"), Some(Duration::from_micros(1500)));
94        assert_eq!(parse_duration("-1s"), None);
95        assert_eq!(parse_duration("invalid"), None);
96    }
97}