1use chrono::{Datelike, Duration, Local, NaiveDate, TimeZone, Utc};
4
5#[derive(Debug, Clone)]
7pub struct DateRange {
8 pub start: chrono::DateTime<Utc>,
9 pub end: chrono::DateTime<Utc>,
10}
11
12impl DateRange {
13 pub fn single_day(date: NaiveDate) -> Self {
14 Self {
15 start: Utc.from_utc_datetime(&date.and_hms_opt(0, 0, 0).expect("valid hms")),
16 end: Utc.from_utc_datetime(&date.and_hms_opt(23, 59, 59).expect("valid hms")),
17 }
18 }
19
20 pub fn month(year: i32, month: u32) -> Option<Self> {
21 let start_date = NaiveDate::from_ymd_opt(year, month, 1)?;
22 let end_date = if month == 12 {
23 NaiveDate::from_ymd_opt(year + 1, 1, 1)?.pred_opt()?
24 } else {
25 NaiveDate::from_ymd_opt(year, month + 1, 1)?.pred_opt()?
26 };
27
28 Some(Self {
29 start: Utc.from_utc_datetime(&start_date.and_hms_opt(0, 0, 0).expect("valid hms")),
30 end: Utc.from_utc_datetime(&end_date.and_hms_opt(23, 59, 59).expect("valid hms")),
31 })
32 }
33
34 pub fn year(year: i32) -> Option<Self> {
35 let start = NaiveDate::from_ymd_opt(year, 1, 1)?;
36 let end = NaiveDate::from_ymd_opt(year, 12, 31)?;
37
38 Some(Self {
39 start: Utc.from_utc_datetime(&start.and_hms_opt(0, 0, 0).expect("valid hms")),
40 end: Utc.from_utc_datetime(&end.and_hms_opt(23, 59, 59).expect("valid hms")),
41 })
42 }
43}
44
45pub struct DateParser;
47
48impl DateParser {
49 pub fn parse(input: &str) -> Option<DateRange> {
50 let s = input.trim().to_lowercase();
51 if s.is_empty() {
52 return None;
53 }
54 let today = Local::now().date_naive();
55
56 Self::parse_relative(&s, today)
57 .or_else(|| Self::parse_month_year(&s))
58 .or_else(|| Self::parse_year(&s))
59 .or_else(|| Self::parse_season(&s, today))
60 .or_else(|| Self::parse_month_only(&s, today))
61 .or_else(|| Self::parse_iso_date(&s))
62 }
63
64 fn parse_relative(input: &str, today: NaiveDate) -> Option<DateRange> {
65 match input {
66 "today" => Some(DateRange::single_day(today)),
67 "yesterday" => Some(DateRange::single_day(today - Duration::days(1))),
68 "this week" => {
69 let start = today - Duration::days(today.weekday().num_days_from_monday() as i64);
70 let end = start + Duration::days(6);
71 Some(DateRange {
72 start: Utc.from_utc_datetime(&start.and_hms_opt(0, 0, 0).expect("valid hms")),
73 end: Utc.from_utc_datetime(&end.and_hms_opt(23, 59, 59).expect("valid hms")),
74 })
75 }
76 "last week" => {
77 let this_week_start =
78 today - Duration::days(today.weekday().num_days_from_monday() as i64);
79 let start = this_week_start - Duration::days(7);
80 let end = this_week_start - Duration::days(1);
81 Some(DateRange {
82 start: Utc.from_utc_datetime(&start.and_hms_opt(0, 0, 0).expect("valid hms")),
83 end: Utc.from_utc_datetime(&end.and_hms_opt(23, 59, 59).expect("valid hms")),
84 })
85 }
86 "this month" => DateRange::month(today.year(), today.month()),
87 "last month" => {
88 let (year, month) = if today.month() == 1 {
89 (today.year() - 1, 12)
90 } else {
91 (today.year(), today.month() - 1)
92 };
93 DateRange::month(year, month)
94 }
95 "this year" => DateRange::year(today.year()),
96 "last year" => DateRange::year(today.year() - 1),
97 _ => None,
98 }
99 }
100
101 fn parse_month_year(input: &str) -> Option<DateRange> {
102 let parts: Vec<&str> = input.split_whitespace().collect();
103 if parts.len() != 2 {
104 return None;
105 }
106
107 let (month_str, year_str) = if parts[0].parse::<i32>().is_ok() {
108 (parts[1], parts[0])
109 } else {
110 (parts[0], parts[1])
111 };
112
113 let year: i32 = year_str.parse().ok()?;
114 let month = month_name_to_number(month_str)?;
115 DateRange::month(year, month)
116 }
117
118 fn parse_year(input: &str) -> Option<DateRange> {
119 let year: i32 = input.parse().ok()?;
120 if !(1900..=2100).contains(&year) {
121 return None;
122 }
123 DateRange::year(year)
124 }
125
126 fn parse_season(input: &str, today: NaiveDate) -> Option<DateRange> {
127 if let Some(season_name) = input.strip_prefix("last ") {
128 let (start_month, end_month) = season_bounds(season_name)?;
129 let year = if start_month > today.month() {
130 today.year() - 2
131 } else {
132 today.year() - 1
133 };
134 return Self::season_range(year, start_month, end_month);
135 }
136
137 if let Some(season_name) = input.strip_prefix("this ") {
138 let (start_month, end_month) = season_bounds(season_name)?;
139 return Self::season_range(today.year(), start_month, end_month);
140 }
141
142 let parts: Vec<&str> = input.split_whitespace().collect();
143 if parts.len() == 2 {
144 let (start_month, end_month) = season_bounds(parts[0])?;
145 let year: i32 = parts[1].parse().ok()?;
146 return Self::season_range(year, start_month, end_month);
147 }
148
149 None
150 }
151
152 fn season_range(year: i32, start_month: u32, end_month: u32) -> Option<DateRange> {
153 let (start_year, end_year) = if start_month > end_month {
154 (year, year + 1)
155 } else {
156 (year, year)
157 };
158
159 let start = NaiveDate::from_ymd_opt(start_year, start_month, 1)?;
160 let end_date = if end_month == 12 {
161 NaiveDate::from_ymd_opt(end_year + 1, 1, 1)?.pred_opt()?
162 } else {
163 NaiveDate::from_ymd_opt(end_year, end_month + 1, 1)?.pred_opt()?
164 };
165
166 Some(DateRange {
167 start: Utc.from_utc_datetime(&start.and_hms_opt(0, 0, 0).expect("valid hms")),
168 end: Utc.from_utc_datetime(&end_date.and_hms_opt(23, 59, 59).expect("valid hms")),
169 })
170 }
171
172 fn parse_month_only(input: &str, today: NaiveDate) -> Option<DateRange> {
173 let month = month_name_to_number(input)?;
174 DateRange::month(today.year(), month)
175 }
176
177 fn parse_iso_date(input: &str) -> Option<DateRange> {
178 let date = NaiveDate::parse_from_str(input, "%Y-%m-%d").ok()?;
179 Some(DateRange::single_day(date))
180 }
181}
182
183fn month_name_to_number(s: &str) -> Option<u32> {
184 match s {
185 "january" | "jan" => Some(1),
186 "february" | "feb" => Some(2),
187 "march" | "mar" => Some(3),
188 "april" | "apr" => Some(4),
189 "may" => Some(5),
190 "june" | "jun" => Some(6),
191 "july" | "jul" => Some(7),
192 "august" | "aug" => Some(8),
193 "september" | "sep" | "sept" => Some(9),
194 "october" | "oct" => Some(10),
195 "november" | "nov" => Some(11),
196 "december" | "dec" => Some(12),
197 _ => None,
198 }
199}
200
201fn season_bounds(s: &str) -> Option<(u32, u32)> {
202 match s {
203 "spring" => Some((3, 5)),
204 "summer" => Some((6, 8)),
205 "fall" | "autumn" => Some((9, 11)),
206 "winter" => Some((12, 2)),
207 _ => None,
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214 use chrono::Datelike;
215
216 #[test]
217 fn test_parse_month_year() {
218 let range = DateParser::parse("March 2019").expect("range");
219 assert_eq!(range.start.year(), 2019);
220 assert_eq!(range.start.month(), 3);
221 assert_eq!(range.end.month(), 3);
222 }
223
224 #[test]
225 fn test_parse_yesterday() {
226 let range = DateParser::parse("yesterday").expect("range");
227 assert!(range.end >= range.start);
228 }
229
230 #[test]
231 fn test_parse_last_summer() {
232 let range = DateParser::parse("last summer").expect("range");
233 assert!(range.end >= range.start);
234 }
235}