intent_engine/
time_utils.rs1use crate::error::{IntentError, Result};
6use chrono::{DateTime, Duration, Utc};
7
8pub 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
67pub fn parse_date_filter(input: &str) -> std::result::Result<DateTime<Utc>, String> {
71 let input = input.trim();
72
73 if let Ok(dt) = parse_duration(input) {
75 return Ok(dt);
76 }
77
78 if let Ok(date) = chrono::NaiveDate::parse_from_str(input, "%Y-%m-%d") {
80 let dt = chrono::TimeZone::from_utc_datetime(&Utc, &date.and_hms_opt(0, 0, 0).unwrap());
81 return Ok(dt);
82 }
83
84 Err(format!(
85 "Invalid date format '{}'. Use duration (7d, 1w) or date (2025-01-01)",
86 input
87 ))
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93
94 #[test]
95 fn test_parse_duration_days() {
96 let result = parse_duration("7d").unwrap();
97 let expected_diff = Duration::days(7);
98 let actual_diff = Utc::now() - result;
99
100 assert!((actual_diff - expected_diff).num_seconds().abs() <= 1);
102 }
103
104 #[test]
105 fn test_parse_duration_hours() {
106 let result = parse_duration("24h").unwrap();
107 let expected_diff = Duration::hours(24);
108 let actual_diff = Utc::now() - result;
109
110 assert!((actual_diff - expected_diff).num_seconds().abs() <= 1);
111 }
112
113 #[test]
114 fn test_parse_duration_minutes() {
115 let result = parse_duration("30m").unwrap();
116 let expected_diff = Duration::minutes(30);
117 let actual_diff = Utc::now() - result;
118
119 assert!((actual_diff - expected_diff).num_seconds().abs() <= 1);
120 }
121
122 #[test]
123 fn test_parse_duration_seconds() {
124 let result = parse_duration("10s").unwrap();
125 let expected_diff = Duration::seconds(10);
126 let actual_diff = Utc::now() - result;
127
128 assert!((actual_diff - expected_diff).num_seconds().abs() <= 1);
129 }
130
131 #[test]
132 fn test_parse_duration_weeks() {
133 let result = parse_duration("2w").unwrap();
134 let expected_diff = Duration::weeks(2);
135 let actual_diff = Utc::now() - result;
136
137 assert!((actual_diff - expected_diff).num_seconds().abs() <= 1);
138 }
139
140 #[test]
141 fn test_parse_duration_with_whitespace() {
142 let result = parse_duration(" 7d ").unwrap();
143 let expected_diff = Duration::days(7);
144 let actual_diff = Utc::now() - result;
145
146 assert!((actual_diff - expected_diff).num_seconds().abs() <= 1);
147 }
148
149 #[test]
150 fn test_parse_duration_invalid_number() {
151 let result = parse_duration("abc d");
152 assert!(matches!(result, Err(IntentError::InvalidInput(_))));
153 }
154
155 #[test]
156 fn test_parse_duration_invalid_unit() {
157 let result = parse_duration("7x");
158 assert!(matches!(result, Err(IntentError::InvalidInput(_))));
159
160 if let Err(IntentError::InvalidInput(msg)) = result {
161 assert!(msg.contains("Invalid duration unit"));
162 }
163 }
164
165 #[test]
166 fn test_parse_duration_too_short() {
167 let result = parse_duration("7");
168 assert!(matches!(result, Err(IntentError::InvalidInput(_))));
169 }
170
171 #[test]
172 fn test_parse_duration_empty() {
173 let result = parse_duration("");
174 assert!(matches!(result, Err(IntentError::InvalidInput(_))));
175 }
176
177 #[test]
178 fn test_parse_date_filter_duration() {
179 let result = parse_date_filter("7d");
180 assert!(result.is_ok());
181 let dt = result.unwrap();
182 let diff = Utc::now() - dt;
183 assert!((diff - Duration::days(7)).num_seconds().abs() <= 1);
184 }
185
186 #[test]
187 fn test_parse_date_filter_date() {
188 let result = parse_date_filter("2025-06-15");
189 assert!(result.is_ok());
190 let dt = result.unwrap();
191 assert_eq!(dt.format("%Y-%m-%d").to_string(), "2025-06-15");
192 }
193
194 #[test]
195 fn test_parse_date_filter_invalid() {
196 let result = parse_date_filter("not-a-date");
197 assert!(result.is_err());
198 assert!(result.unwrap_err().contains("Invalid date format"));
199 }
200}