coding_agent_search/ui/
time_parser.rs1use chrono::{Duration, Local, LocalResult, NaiveDate, TimeZone, Utc};
2
3pub fn parse_time_input(input: &str) -> Option<i64> {
12 let input = input.trim().to_lowercase();
13 if input.is_empty() {
14 return None;
15 }
16
17 let now_utc = Utc::now();
18 let now_ms = now_utc.timestamp_millis();
19
20 if let Some(stripped) = input.strip_prefix('-') {
22 let val_str: String = stripped.chars().take_while(|c| c.is_numeric()).collect();
23 if let Ok(val) = val_str.parse::<i64>() {
24 let unit = stripped.trim_start_matches(&val_str).trim();
25 let duration = relative_duration(unit, val)?;
26 return subtract_duration_ms(now_utc, duration);
27 }
28 }
29
30 {
32 let val_str: String = input.chars().take_while(|c| c.is_numeric()).collect();
33 if !val_str.is_empty() {
34 let unit = input.trim_start_matches(&val_str).trim();
35 if !unit.is_empty()
36 && let Ok(val) = val_str.parse::<i64>()
37 {
38 let duration = relative_duration(unit, val);
39 if let Some(duration) = duration {
40 return subtract_duration_ms(now_utc, duration);
41 }
42 }
43 }
44 }
45
46 {
48 let parts: Vec<&str> = input.split_whitespace().collect();
49 if parts.len() == 3
50 && parts[2] == "ago"
51 && let Ok(val) = parts[0].parse::<i64>()
52 {
53 let duration = relative_duration(parts[1], val);
54 if let Some(duration) = duration {
55 return subtract_duration_ms(now_utc, duration);
56 }
57 }
58 if parts.len() == 2 && parts[1] == "ago" {
59 let val_str: String = parts[0].chars().take_while(|c| c.is_numeric()).collect();
60 if let Ok(val) = val_str.parse::<i64>() {
61 let unit = parts[0].trim_start_matches(&val_str);
62 let duration = relative_duration(unit, val);
63 if let Some(duration) = duration {
64 return subtract_duration_ms(now_utc, duration);
65 }
66 }
67 }
68 }
69
70 match input.as_str() {
72 "now" => return Some(now_ms),
73 "today" => {
74 let today = Local::now().date_naive();
75 return local_midnight_to_utc(today);
76 }
77 "yesterday" => {
78 let yesterday = Local::now()
79 .date_naive()
80 .checked_sub_signed(Duration::try_days(1)?)?;
81 return local_midnight_to_utc(yesterday);
82 }
83 _ => {}
84 }
85
86 if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(&input) {
88 return Some(dt.timestamp_millis());
89 }
90
91 if let Ok(date) = NaiveDate::parse_from_str(&input, "%Y-%m-%d")
93 .or_else(|_| NaiveDate::parse_from_str(&input, "%Y/%m/%d"))
94 {
95 return local_midnight_to_utc(date);
96 }
97
98 if let Ok(date) = NaiveDate::parse_from_str(&input, "%m/%d/%Y")
100 .or_else(|_| NaiveDate::parse_from_str(&input, "%m-%d-%Y"))
101 {
102 return local_midnight_to_utc(date);
103 }
104 if let Ok(n) = input.parse::<i64>() {
106 if n < 100_000_000_000 {
108 return n.checked_mul(1000);
109 }
110 return Some(n);
111 }
112
113 None
114}
115
116fn local_midnight_to_utc(date: NaiveDate) -> Option<i64> {
117 let dt = date.and_hms_opt(0, 0, 0)?;
118 let local = match Local.from_local_datetime(&dt) {
119 LocalResult::Single(value) => value,
120 LocalResult::Ambiguous(earliest, _) => earliest,
121 LocalResult::None => {
122 return Some(Utc.from_utc_datetime(&dt).timestamp_millis());
124 }
125 };
126 Some(local.with_timezone(&Utc).timestamp_millis())
127}
128
129fn relative_duration(unit: &str, val: i64) -> Option<Duration> {
130 match unit {
131 "d" | "day" | "days" => Duration::try_days(val),
132 "h" | "hr" | "hrs" | "hour" | "hours" => Duration::try_hours(val),
133 "m" | "min" | "mins" | "minute" | "minutes" => Duration::try_minutes(val),
134 "w" | "wk" | "wks" | "week" | "weeks" => Duration::try_weeks(val),
135 _ => None,
136 }
137}
138
139fn subtract_duration_ms(now_utc: chrono::DateTime<Utc>, duration: Duration) -> Option<i64> {
140 now_utc
141 .checked_sub_signed(duration)
142 .map(|value| value.timestamp_millis())
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn test_relative_time() {
151 let now = Utc::now().timestamp_millis();
152 let tolerance = 60 * 1000; let t1 = parse_time_input("-1h").unwrap();
156 let diff = now - t1;
157 assert!((diff - 3600 * 1000).abs() < tolerance);
158
159 let t2 = parse_time_input("-1d").unwrap();
161 let diff = now - t2;
162 assert!((diff - 86400 * 1000).abs() < tolerance);
163
164 let t3 = parse_time_input("7d").unwrap();
166 let diff = now - t3;
167 assert!((diff - 7 * 86400 * 1000).abs() < tolerance);
168
169 let t4 = parse_time_input("30 days ago").unwrap();
171 let diff = now - t4;
172 assert!((diff - 30 * 86400 * 1000).abs() < tolerance);
173
174 let t5 = parse_time_input("2 weeks ago").unwrap();
176 let diff = now - t5;
177 assert!((diff - 14 * 86400 * 1000).abs() < tolerance);
178 }
179
180 #[test]
181 fn test_relative_time_overflow_returns_none() {
182 let max = i64::MAX;
183 let inputs = [
184 format!("{max}d"),
185 format!("{max}h"),
186 format!("{max}m"),
187 format!("{max}w"),
188 format!("-{max}d"),
189 format!("{max} days ago"),
190 format!("{max}h ago"),
191 ];
192
193 for input in inputs {
194 assert_eq!(parse_time_input(&input), None, "{input}");
195 }
196
197 let duration = Duration::try_milliseconds(i64::MAX).unwrap();
198 assert_eq!(
199 subtract_duration_ms(chrono::DateTime::<Utc>::MIN_UTC, duration),
200 None
201 );
202 }
203
204 #[test]
205 fn test_keywords() {
206 assert!(parse_time_input("now").is_some());
207 let today = parse_time_input("today").unwrap();
208 let yesterday = parse_time_input("yesterday").unwrap();
209 assert!(today > yesterday);
210 let diff = today - yesterday;
211 let min = 23 * 60 * 60 * 1000;
212 let max = 25 * 60 * 60 * 1000;
213 assert!(
214 diff >= min && diff <= max,
215 "expected 23-25h difference due to DST, got {} ms",
216 diff
217 );
218 }
219
220 #[test]
221 fn test_date_formats() {
222 assert!(parse_time_input("2023-01-01").is_some());
224 assert!(parse_time_input("2023/01/01").is_some());
225 assert!(parse_time_input("01/01/2023").is_some());
226 assert!(parse_time_input("01-01-2023").is_some());
227 }
228
229 #[test]
230 fn test_numeric() {
231 let _sec = 1700000000;
232 let ms = 1700000000000;
233 assert_eq!(parse_time_input("1700000000").unwrap(), ms);
234 assert_eq!(parse_time_input("1700000000000").unwrap(), ms);
235 assert_eq!(parse_time_input(&i64::MIN.to_string()), None);
236 }
237}