Skip to main content

mcpr_integrations/store/
duration.rs

1//! Human-friendly duration parsing for `--since` and `--before` CLI flags.
2//!
3//! Supports formats like `30m`, `1h`, `24h`, `7d`, `2w`. Used across all
4//! CLI observability commands to specify time windows.
5//!
6//! This is intentionally simple — no ISO 8601 duration parsing, no combined
7//! units (e.g., "1h30m"). Each value is a single number + unit suffix.
8
9use std::time::Duration;
10
11/// Parse a human-friendly duration string into a [`Duration`].
12///
13/// Supported suffixes:
14/// - `s` — seconds (e.g., `30s`)
15/// - `m` — minutes (e.g., `15m`)
16/// - `h` — hours (e.g., `2h`)
17/// - `d` — days (e.g., `7d`)
18/// - `w` — weeks (e.g., `2w`)
19///
20/// Returns `None` if the string is empty, has an unknown suffix, or the
21/// numeric part can't be parsed.
22///
23/// # Examples
24///
25/// ```
26/// use mcpr_integrations::store::duration::parse_duration;
27///
28/// assert_eq!(parse_duration("30m"), Some(std::time::Duration::from_secs(30 * 60)));
29/// assert_eq!(parse_duration("2h"), Some(std::time::Duration::from_secs(2 * 3600)));
30/// assert_eq!(parse_duration("7d"), Some(std::time::Duration::from_secs(7 * 86400)));
31/// assert_eq!(parse_duration("bad"), None);
32/// ```
33pub fn parse_duration(s: &str) -> Option<Duration> {
34    let s = s.trim();
35    if s.is_empty() {
36        return None;
37    }
38
39    // Split into numeric prefix and unit suffix.
40    let (num_str, multiplier) = if let Some(n) = s.strip_suffix('w') {
41        (n, 7 * 24 * 3600)
42    } else if let Some(n) = s.strip_suffix('d') {
43        (n, 24 * 3600)
44    } else if let Some(n) = s.strip_suffix('h') {
45        (n, 3600)
46    } else if let Some(n) = s.strip_suffix('m') {
47        (n, 60)
48    } else if let Some(n) = s.strip_suffix('s') {
49        (n, 1)
50    } else {
51        return None;
52    };
53
54    let num: u64 = num_str.trim().parse().ok()?;
55    Some(Duration::from_secs(num * multiplier))
56}
57
58/// Convert a duration to a unix millisecond cutoff timestamp.
59///
60/// Returns `now_ms - duration_ms`, suitable for `WHERE ts >= ?` queries.
61pub fn since_to_cutoff_ms(duration: Duration) -> i64 {
62    let now_ms = chrono::Utc::now().timestamp_millis();
63    now_ms - duration.as_millis() as i64
64}
65
66#[cfg(test)]
67#[allow(non_snake_case)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn parse_duration__seconds() {
73        assert_eq!(parse_duration("30s"), Some(Duration::from_secs(30)));
74    }
75
76    #[test]
77    fn parse_duration__minutes() {
78        assert_eq!(parse_duration("15m"), Some(Duration::from_secs(15 * 60)));
79    }
80
81    #[test]
82    fn parse_duration__hours() {
83        assert_eq!(parse_duration("2h"), Some(Duration::from_secs(2 * 3600)));
84    }
85
86    #[test]
87    fn parse_duration__days() {
88        assert_eq!(parse_duration("7d"), Some(Duration::from_secs(7 * 86400)));
89    }
90
91    #[test]
92    fn parse_duration__weeks() {
93        assert_eq!(
94            parse_duration("2w"),
95            Some(Duration::from_secs(2 * 7 * 86400))
96        );
97    }
98
99    #[test]
100    fn parse_duration__invalid_suffix() {
101        assert_eq!(parse_duration("10x"), None);
102    }
103
104    #[test]
105    fn parse_duration__invalid_number() {
106        assert_eq!(parse_duration("abch"), None);
107    }
108
109    #[test]
110    fn parse_duration__empty_string() {
111        assert_eq!(parse_duration(""), None);
112    }
113
114    #[test]
115    fn parse_duration__whitespace_handling() {
116        assert_eq!(parse_duration(" 5m "), Some(Duration::from_secs(5 * 60)));
117    }
118}