1use chrono::Weekday;
77
78use crate::date::{self, Date, DateError, DateRange};
79
80pub struct DateParser {
82 base: Date
83}
84
85impl Default for DateParser {
86 fn default() -> Self { Self { base: Date::today() } }
88}
89
90impl DateParser {
91 fn new(base: Date) -> Self { Self { base } }
95
96 pub fn parse(&self, datespec: &str) -> date::Result<Date> {
103 match datespec {
104 "today" => Ok(self.base),
105 "yesterday" => Ok(self.base.pred()),
106 "sunday" | "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday" => {
107 Ok(self.base.find_previous(weekday_from_str(datespec)?))
108 }
109 _ => datespec.parse()
110 }
111 }
112}
113
114pub struct RangeParser {
116 base: Date
117}
118
119impl Default for RangeParser {
120 fn default() -> Self { Self { base: Date::today() } }
122}
123
124fn weekday_from_str(day: &str) -> date::Result<Weekday> {
126 match day {
127 "sunday" => Ok(Weekday::Sun),
128 "monday" => Ok(Weekday::Mon),
129 "tuesday" => Ok(Weekday::Tue),
130 "wednesday" => Ok(Weekday::Wed),
131 "thursday" => Ok(Weekday::Thu),
132 "friday" => Ok(Weekday::Fri),
133 "saturday" => Ok(Weekday::Sat),
134 _ => Err(DateError::InvalidDaySpec(day.to_string()))
135 }
136}
137
138fn month_from_name(name: &str) -> Option<u32> {
139 let month = match name {
140 "january" | "jan" => 1,
141 "february" | "feb" => 2,
142 "march" | "mar" => 3,
143 "april" | "apr" => 4,
144 "may" => 5,
145 "june" | "jun" => 6,
146 "july" | "jul" => 7,
147 "august" | "aug" => 8,
148 "september" | "sep" | "sept" => 9,
149 "october" | "oct" => 10,
150 "november" | "nov" => 11,
151 "december" | "dec" => 12,
152 _ => return None
153 };
154 Some(month)
155}
156
157impl RangeParser {
158 #[cfg(test)]
160 pub fn new(base: Date) -> Self { Self { base } }
161
162 pub fn parse_from_str(&self, datespec: &str) -> date::Result<DateRange> {
169 if datespec.is_empty() { return Ok(self.base.into()); }
170 let mut iter = datespec.split_ascii_whitespace();
171 self.parse(&mut iter).map(|(r, _)| r)
172 }
173
174 pub fn parse<'a, I>(&self, datespec: &mut I) -> date::Result<(DateRange, &'a str)>
182 where
183 I: Iterator<Item = &'a str>
184 {
185 let Some(token) = datespec.next() else {
186 return Ok((self.base.into(), ""));
187 };
188 let ltoken = token.to_ascii_lowercase();
189 if let Some(range) = self.month_range(ltoken.as_str()) {
191 return Ok((range, ""));
192 }
193
194 let base = self.base;
195 let range_opt = match ltoken.as_str() {
196 "ytd" => {
197 let start = base.year_start();
198 DateRange::new_opt(start, base.succ())
199 }
200 "this" => {
201 let Some(token) = datespec.next() else {
202 return Err(DateError::InvalidDaySpec(token.into()));
203 };
204 let ltoken = token.to_ascii_lowercase();
205 match ltoken.as_str() {
206 "week" => DateRange::new_opt(base.week_start(), base.week_end().succ()),
207 "month" => DateRange::new_opt(base.month_start(), base.month_end().succ()),
208 "year" => DateRange::new_opt(base.year_start(), base.year_end().succ()),
209 _ => return Err(DateError::InvalidDaySpec(token.into()))
210 }
211 }
212 "last" => {
213 let Some(token) = datespec.next() else {
214 return Err(DateError::InvalidDate);
215 };
216 let ltoken = token.to_ascii_lowercase();
217 match ltoken.as_str() {
218 "week" => {
219 let date = base.week_start();
220 DateRange::new_opt(date.pred().week_start(), date)
221 }
222 "month" => {
223 let date = base.month_start().pred().month_start();
224 DateRange::new_opt(date, date.month_end().succ())
225 }
226 "year" => {
227 let date = Date::new(base.year() - 1, 1, 1)?;
228 DateRange::new_opt(date, date.year_end().succ())
229 }
230 _ => return Err(DateError::InvalidDaySpec(token.into()))
231 }
232 }
233 _ => None
234 };
235
236 if let Some(date_range) = range_opt {
237 return Ok((date_range, ""));
238 }
239
240 let dparser = DateParser::new(self.base);
242 let Ok(start) = dparser.parse(<oken) else {
243 return Ok((self.base.into(), token));
244 };
245 if let Some(token) = datespec.next() {
246 let ltoken = token.to_ascii_lowercase();
247 if let Ok(end) = dparser.parse(<oken) {
248 let range = DateRange::new_opt(start, end.succ())
249 .ok_or(DateError::WrongDateOrder)?;
250 return Ok((range, ""));
251 }
252 else {
253 return Ok((start.into(), token));
254 }
255 }
256
257 Ok((start.into(), ""))
258 }
259
260 fn month_range(&self, token: &str) -> Option<DateRange> {
262 let month = month_from_name(token)?;
263 let this = self.base.month();
264 let year = self.base.year();
265 let year = if month < this { year } else { year - 1 };
266
267 let start = Date::new(year, month, 1).ok()?;
268 Some(DateRange { start, end: start.month_end().succ() })
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use once_cell::sync::Lazy;
275 use assert2::{assert, let_assert};
276 use rstest::rstest;
277
278 use super::*;
279 use crate::date::{DateTime, Weekday};
280
281 static BASE_DATE: Lazy<Date> = Lazy::new(
282 || Date::new(2022, 11, 15).expect("Hardcoded value") );
284 static YESTERDAY: Lazy<Date> = Lazy::new(
285 ||Date::new(2022, 11, 14).expect("Hardcoded date must work")
286 );
287 static HARD_DATE: Lazy<Date> = Lazy::new(
288 ||Date::new(2022, 10, 20).expect("Hardcoded date must work")
289 );
290
291 #[rstest]
294 #[case("today", Date::today(), "today")]
295 #[case("yesterday", Date::today().pred(), "yesterday")]
296 fn test_date_parse(#[case]input: &str, #[case]expected: Date, #[case]msg: &str) {
297 let p = DateParser::default();
298 let_assert!(Ok(actual) = p.parse(input));
299 assert!(actual == expected, "{msg}");
300 }
301
302 #[test]
303 fn test_date_parse_weekdays() {
304 let max_dur = DateTime::days(7);
305 #[rustfmt::skip]
306 let days: [&str; 7] = [
307 "sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"
308 ];
309 let today = Date::today();
310 let midnight = Date::today().day_end();
311 let p = DateParser::default();
312 days.iter().for_each(|&day| {
313 let_assert!(Ok(date) = p.parse(day), "parse {day}");
314 assert!(date < today);
315
316 let end_of_date = date.day_end();
317 let_assert!(Ok(dur) = midnight - end_of_date, "end {day}");
318 assert!(dur <= max_dur);
319 });
320 }
321
322 fn test_range_parser() -> RangeParser { RangeParser::new(*BASE_DATE) }
325
326 #[test]
327 fn test_parse_default() {
328 let p = RangeParser::default();
329 let expect: DateRange = DateRange::default();
330 let_assert!(Ok(actual) = p.parse_from_str(""));
331 assert!(actual == expect);
332 }
333
334 #[rstest]
335 #[case("", (*BASE_DATE).into(), "new")]
336 #[case("today", (*BASE_DATE).into(), "today")]
337 #[case("yesterday", (*YESTERDAY).into(), "yesterday")]
338 #[case("2022-10-20", (*HARD_DATE).into(), "actual date")]
339 #[case("monday", DateRange::from(BASE_DATE.find_previous(Weekday::Mon)), "dayname")]
340 #[case("wednesday", DateRange::from(BASE_DATE.find_previous(Weekday::Wed)), "later dayname")]
341 fn test_date_range_parser_one_day(#[case]input: &str, #[case]expect: DateRange, #[case]msg: &str) {
342 let p = test_range_parser();
343 let_assert!(Ok(actual) = p.parse_from_str(input));
344 assert!(actual == expect, "{msg}");
345 }
346
347 #[test]
350 fn test_dates_both_dates() {
351 let_assert!(Ok(start) = Date::new(2021, 12, 1));
352 let_assert!(Ok(end) = Date::new(2021, 12, 8));
353 let expected = DateRange::new(start, end);
354
355 let p = test_range_parser();
356 let_assert!(Ok(actual) = p.parse_from_str("2021-12-01 2021-12-07"));
357 assert!(actual == expected);
358 }
359
360 #[test]
361 fn test_dates_both_dates_desc() {
362 let_assert!(Ok(start) = Date::new(2022, 11, 13));
363 let expected = DateRange::new(start, BASE_DATE.succ());
364
365 let p = test_range_parser();
366 let_assert!(Ok(actual) = p.parse_from_str("sunday today"));
367 assert!(actual == expected);
368 }
369
370 #[rstest]
373 #[case("january", Date::new(2022, 1, 1), Date::new(2022, 2, 1))]
374 #[case("jan", Date::new(2022, 1, 1), Date::new(2022, 2, 1))]
375 #[case("february", Date::new(2022, 2, 1), Date::new(2022, 3, 1))]
376 #[case("feb", Date::new(2022, 2, 1), Date::new(2022, 3, 1))]
377 #[case("march", Date::new(2022, 3, 1), Date::new(2022, 4, 1))]
378 #[case("mar", Date::new(2022, 3, 1), Date::new(2022, 4, 1))]
379 #[case("april", Date::new(2022, 4, 1), Date::new(2022, 5, 1))]
380 #[case("apr", Date::new(2022, 4, 1), Date::new(2022, 5, 1))]
381 #[case("may", Date::new(2022, 5, 1), Date::new(2022, 6, 1))]
382 #[case("june", Date::new(2022, 6, 1), Date::new(2022, 7, 1))]
383 #[case("jun", Date::new(2022, 6, 1), Date::new(2022, 7, 1))]
384 #[case("july", Date::new(2022, 7, 1), Date::new(2022, 8, 1))]
385 #[case("jul", Date::new(2022, 7, 1), Date::new(2022, 8, 1))]
386 #[case("august", Date::new(2022, 8, 1), Date::new(2022, 9, 1))]
387 #[case("aug", Date::new(2022, 8, 1), Date::new(2022, 9, 1))]
388 #[case("september", Date::new(2022, 9, 1), Date::new(2022, 10, 1))]
389 #[case("sep", Date::new(2022, 9, 1), Date::new(2022, 10, 1))]
390 #[case("october", Date::new(2022, 10, 1), Date::new(2022, 11, 1))]
391 #[case("oct", Date::new(2022, 10, 1), Date::new(2022, 11, 1))]
392 #[case("november", Date::new(2021, 11, 1), Date::new(2021, 12, 1))]
393 #[case("nov", Date::new(2021, 11, 1), Date::new(2021, 12, 1))]
394 #[case("december", Date::new(2021, 12, 1), Date::new(2022, 1, 1))]
395 #[case("dec", Date::new(2021, 12, 1), Date::new(2022, 1, 1))]
396 fn test_month_name(
397 #[case]name: &str,
398 #[case]start_opt: Result<Date, DateError>,
399 #[case]end_opt: Result<Date, DateError>
400 ) {
401 let p = test_range_parser();
402 let_assert!(Ok(start) = start_opt.as_ref());
403 let_assert!(Ok(end) = end_opt.as_ref());
404 let expected = DateRange::new(*start, *end);
405 let_assert!(Ok(actual) = p.parse_from_str(name));
406 assert!(actual == expected);
407 }
408
409 #[rstest]
410 #[case("this week", Date::new(2022, 11, 13), Date::new(2022, 11, 20))]
411 #[case("this month", Date::new(2022, 11, 1), Date::new(2022, 12, 1))]
412 #[case("this year", Date::new(2022, 1, 1), Date::new(2023, 1, 1))]
413 #[case("ytd", Date::new(2022, 1, 1), Ok(BASE_DATE.succ()))]
414 #[case("last week", Date::new(2022, 11, 6), Date::new(2022, 11, 13))]
415 #[case("last month", Date::new(2022, 10, 1), Date::new(2022, 11, 1))]
416 #[case("last year", Date::new(2021, 1, 1), Date::new(2022, 1, 1))]
417 fn test_special_range(
418 #[case]input: &str,
419 #[case]start_opt: Result<Date, DateError>,
420 #[case]end_opt: Result<Date, DateError>
421 ) {
422 let_assert!(Ok(start) = start_opt);
423 let_assert!(Ok(end) = end_opt);
424 let expected = DateRange::new(start, end);
425
426 let p = test_range_parser();
427 let_assert!(Ok(actual) = p.parse_from_str(input), "parsing '{input}'");
428 assert!(actual == expected);
429 }
430}