intent_engine/
time_utils.rs

1//! Time utility functions
2//!
3//! Provides common time-related operations used across the codebase.
4
5use crate::error::{IntentError, Result};
6use chrono::{DateTime, Duration, Utc};
7
8/// Parse a duration string (e.g., "7d", "24h", "30m") into a DateTime
9///
10/// Supported units:
11/// - `d`: days
12/// - `h`: hours
13/// - `m`: minutes
14/// - `s`: seconds
15/// - `w`: weeks
16///
17/// # Arguments
18/// * `duration` - Duration string in format like "7d", "24h", "30m", "5w"
19///
20/// # Returns
21/// A DateTime representing the current time minus the specified duration
22///
23/// # Errors
24/// Returns InvalidInput error if:
25/// - Duration string is empty or too short
26/// - Number part is not a valid integer
27/// - Unit is not one of d/h/m/s/w
28///
29/// # Examples
30/// ```ignore
31/// use crate::time_utils::parse_duration;
32///
33/// let seven_days_ago = parse_duration("7d").unwrap();
34/// let one_week_ago = parse_duration("1w").unwrap();
35/// ```
36pub fn parse_duration(duration: &str) -> Result<DateTime<Utc>> {
37    let duration = duration.trim();
38
39    if duration.len() < 2 {
40        return Err(IntentError::InvalidInput(
41            "Duration must be in format like '7d', '24h', '30m', '5w', or '10s'".to_string(),
42        ));
43    }
44
45    let (num_str, unit) = duration.split_at(duration.len() - 1);
46    let num: i64 = num_str.parse().map_err(|_| {
47        IntentError::InvalidInput(format!("Invalid number in duration: '{}'", num_str))
48    })?;
49
50    let offset = match unit {
51        "d" => Duration::days(num),
52        "h" => Duration::hours(num),
53        "m" => Duration::minutes(num),
54        "s" => Duration::seconds(num),
55        "w" => Duration::weeks(num),
56        _ => {
57            return Err(IntentError::InvalidInput(format!(
58                "Invalid duration unit '{}'. Use 'd' (days), 'h' (hours), 'm' (minutes), 's' (seconds), or 'w' (weeks)",
59                unit
60            )))
61        }
62    };
63
64    Ok(Utc::now() - offset)
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn test_parse_duration_days() {
73        let result = parse_duration("7d").unwrap();
74        let expected_diff = Duration::days(7);
75        let actual_diff = Utc::now() - result;
76
77        // Allow 1 second tolerance for test execution time
78        assert!((actual_diff - expected_diff).num_seconds().abs() <= 1);
79    }
80
81    #[test]
82    fn test_parse_duration_hours() {
83        let result = parse_duration("24h").unwrap();
84        let expected_diff = Duration::hours(24);
85        let actual_diff = Utc::now() - result;
86
87        assert!((actual_diff - expected_diff).num_seconds().abs() <= 1);
88    }
89
90    #[test]
91    fn test_parse_duration_minutes() {
92        let result = parse_duration("30m").unwrap();
93        let expected_diff = Duration::minutes(30);
94        let actual_diff = Utc::now() - result;
95
96        assert!((actual_diff - expected_diff).num_seconds().abs() <= 1);
97    }
98
99    #[test]
100    fn test_parse_duration_seconds() {
101        let result = parse_duration("10s").unwrap();
102        let expected_diff = Duration::seconds(10);
103        let actual_diff = Utc::now() - result;
104
105        assert!((actual_diff - expected_diff).num_seconds().abs() <= 1);
106    }
107
108    #[test]
109    fn test_parse_duration_weeks() {
110        let result = parse_duration("2w").unwrap();
111        let expected_diff = Duration::weeks(2);
112        let actual_diff = Utc::now() - result;
113
114        assert!((actual_diff - expected_diff).num_seconds().abs() <= 1);
115    }
116
117    #[test]
118    fn test_parse_duration_with_whitespace() {
119        let result = parse_duration("  7d  ").unwrap();
120        let expected_diff = Duration::days(7);
121        let actual_diff = Utc::now() - result;
122
123        assert!((actual_diff - expected_diff).num_seconds().abs() <= 1);
124    }
125
126    #[test]
127    fn test_parse_duration_invalid_number() {
128        let result = parse_duration("abc d");
129        assert!(matches!(result, Err(IntentError::InvalidInput(_))));
130    }
131
132    #[test]
133    fn test_parse_duration_invalid_unit() {
134        let result = parse_duration("7x");
135        assert!(matches!(result, Err(IntentError::InvalidInput(_))));
136
137        if let Err(IntentError::InvalidInput(msg)) = result {
138            assert!(msg.contains("Invalid duration unit"));
139        }
140    }
141
142    #[test]
143    fn test_parse_duration_too_short() {
144        let result = parse_duration("7");
145        assert!(matches!(result, Err(IntentError::InvalidInput(_))));
146    }
147
148    #[test]
149    fn test_parse_duration_empty() {
150        let result = parse_duration("");
151        assert!(matches!(result, Err(IntentError::InvalidInput(_))));
152    }
153}