1#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct DateWindow {
9 pub start: String,
10 pub end: String,
11}
12
13const MONTHS: [(&str, u32); 12] = [
14 ("january", 1),
15 ("february", 2),
16 ("march", 3),
17 ("april", 4),
18 ("may", 5),
19 ("june", 6),
20 ("july", 7),
21 ("august", 8),
22 ("september", 9),
23 ("october", 10),
24 ("november", 11),
25 ("december", 12),
26];
27
28fn days_in_month(year: i32, month: u32) -> u32 {
29 match month {
30 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
31 4 | 6 | 9 | 11 => 30,
32 2 if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) => 29,
33 _ => 28,
34 }
35}
36
37pub fn date_windows(query: &str) -> Vec<DateWindow> {
39 let lower = query.to_lowercase();
40 let mut windows = Vec::new();
41
42 let bytes = lower.as_bytes();
44 let mut i = 0;
45 while i + 10 <= bytes.len() {
46 let slice = &lower[i..i + 10];
47 let sep_ok = matches!(slice.as_bytes()[4], b'/' | b'-')
48 && slice.as_bytes()[7] == slice.as_bytes()[4];
49 if sep_ok
50 && slice[..4].chars().all(|c| c.is_ascii_digit())
51 && slice[5..7].chars().all(|c| c.is_ascii_digit())
52 && slice[8..10].chars().all(|c| c.is_ascii_digit())
53 {
54 let day = format!("{}-{}-{}", &slice[..4], &slice[5..7], &slice[8..10]);
55 windows.push(DateWindow {
56 start: format!("{day}T00:00:00Z"),
57 end: format!("{day}T23:59:59Z"),
58 });
59 i += 10;
60 continue;
61 }
62 i += 1;
63 }
64
65 let tokens: Vec<&str> = lower
67 .split(|c: char| !c.is_ascii_alphanumeric())
68 .filter(|t| !t.is_empty())
69 .collect();
70 for pair in tokens.windows(2) {
71 if let Some((_, month)) = MONTHS.iter().find(|(name, _)| *name == pair[0])
72 && pair[1].len() == 4
73 && pair[1].chars().all(|c| c.is_ascii_digit())
74 && let Ok(year) = pair[1].parse::<i32>()
75 {
76 windows.push(DateWindow {
77 start: format!("{year:04}-{month:02}-01T00:00:00Z"),
78 end: format!(
79 "{year:04}-{month:02}-{:02}T23:59:59Z",
80 days_in_month(year, *month)
81 ),
82 });
83 }
84 }
85 windows
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91
92 #[test]
93 fn parses_numeric_and_month_references() {
94 let w = date_windows("what did I say on 2023/05/20 about the trip");
95 assert_eq!(w.len(), 1);
96 assert_eq!(w[0].start, "2023-05-20T00:00:00Z");
97 let w = date_windows("decisions from May 2023 and June 2024?");
98 assert_eq!(w.len(), 2);
99 assert_eq!(w[0].end, "2023-05-31T23:59:59Z");
100 assert_eq!(w[1].end, "2024-06-30T23:59:59Z");
101 let w = date_windows("iso style 2024-02-29 leap day");
102 assert_eq!(w[0].start, "2024-02-29T00:00:00Z");
103 }
104
105 #[test]
106 fn plain_queries_have_no_windows() {
107 assert!(date_windows("what tools does mark prefer").is_empty());
108 assert!(date_windows("may I ask about the 2023 goals").is_empty());
109 }
110}