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 _ => Date::try_from(datespec)
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 range_opt = match ltoken.as_str() {
195 "ytd" => {
196 let start = self.base.year_start();
197 DateRange::new_opt(start, self.base.succ())
198 },
199 "this" => {
200 let Some(token) = datespec.next() else {
201 return Err(DateError::InvalidDaySpec(token.into()));
202 };
203 let ltoken = token.to_ascii_lowercase();
204 match ltoken.as_str() {
205 "week" => DateRange::new_opt(self.base.week_start(), self.base.week_end().succ()),
206 "month" => DateRange::new_opt(self.base.month_start(), self.base.month_end().succ()),
207 "year" => DateRange::new_opt(self.base.year_start(), self.base.year_end().succ()),
208 _ => return Err(DateError::InvalidDaySpec(token.into())),
209 }
210 },
211 "last" => {
212 let Some(token) = datespec.next() else {
213 return Err(DateError::InvalidDate);
214 };
215 let ltoken = token.to_ascii_lowercase();
216 match ltoken.as_str() {
217 "week" => {
218 let date = self.base.week_start();
219 DateRange::new_opt(date.pred().week_start(), date)
220 },
221 "month" => {
222 let date = self.base.month_start().pred().month_start();
223 DateRange::new_opt(date, date.month_end().succ())
224 },
225 "year" => {
226 let date = Date::new(self.base.year() - 1, 1, 1)?;
227 DateRange::new_opt(date, date.year_end().succ())
228 }
229 _ => return Err(DateError::InvalidDaySpec(token.into())),
230 }
231 }
232 _ => None
233 };
234
235 if let Some(date_range) = range_opt {
236 return Ok((date_range, ""));
237 }
238
239 let dparser = DateParser::new(self.base);
241 let Ok(start) = dparser.parse(<oken) else {
242 return Ok((self.base.into(), token));
243 };
244 if let Some(token) = datespec.next() {
245 let ltoken = token.to_ascii_lowercase();
246 if let Ok(end) = dparser.parse(<oken) {
247 let range =
248 DateRange::new_opt(start, end.succ()).ok_or(DateError::WrongDateOrder)?;
249 return Ok((range, ""));
250 }
251 else {
252 return Ok((start.into(), token));
253 }
254 }
255
256 Ok((start.into(), ""))
257 }
258
259 fn month_range(&self, token: &str) -> Option<DateRange> {
261 let month = month_from_name(token)?;
262 let this = self.base.month();
263 let year = self.base.year();
264 let year = if month < this { year } else { year - 1 };
265
266 let start = Date::new(year, month, 1).ok()?;
267 Some(DateRange { start, end: start.month_end().succ() })
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use once_cell::sync::Lazy;
274 use spectral::prelude::*;
275
276 use super::*;
277 use crate::date::{DateTime, Weekday};
278
279 static BASE_DATE: Lazy<Date> = Lazy::new(
280 || Date::new(2022, 11, 15).unwrap() );
282
283 #[test]
286 fn test_date_parse_today() {
287 let p = DateParser::default();
288 assert_that!(p.parse("today"))
289 .is_ok()
290 .is_equal_to(&Date::today());
291 }
292
293 #[test]
294 fn test_date_parse_yesterday() {
295 let p = DateParser::default();
296 assert_that!(p.parse("yesterday"))
297 .is_ok()
298 .is_equal_to(&Date::today().pred());
299 }
300
301 #[test]
302 fn test_date_parse_weekdays() {
303 let max_dur = DateTime::days(7);
304 #[rustfmt::skip]
305 let days: [&str; 7] = [
306 "sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"
307 ];
308 let today = Date::today();
309 let midnight = Date::today().day_end();
310 let p = DateParser::default();
311 days.iter().for_each(|&day| {
312 let date = p.parse(day);
313 assert_that!(date).named(day).is_ok().is_less_than(&today);
314
315 let end_of_date = date.unwrap().day_end();
316 assert_that!(midnight - end_of_date)
317 .named(day)
318 .is_ok()
319 .is_less_than_or_equal_to(&max_dur);
320 });
321 }
322
323 fn test_range_parser() -> RangeParser { RangeParser::new(*BASE_DATE) }
326
327 #[test]
328 fn test_parse_default() {
329 let p = RangeParser::default();
330 let expect: DateRange = DateRange::default();
331 assert_that!(p.parse_from_str(""))
332 .is_ok()
333 .is_equal_to(&expect);
334 }
335
336 #[test]
337 fn test_parse_new() {
338 let p = test_range_parser();
339 let expect: DateRange = (*BASE_DATE).into();
340 assert_that!(p.parse_from_str(""))
341 .is_ok()
342 .is_equal_to(&expect);
343 }
344
345 #[test]
348 fn test_parse_today() {
349 let p = test_range_parser();
350 let expect: DateRange = (*BASE_DATE).into();
351 assert_that!(p.parse_from_str("today"))
352 .is_ok()
353 .is_equal_to(&expect);
354 }
355
356 #[test]
357 fn test_parse_yesterday() {
358 let p = test_range_parser();
359 let expect: DateRange =
360 Date::new(2022, 11, 14).expect("Hardcoded date must work").into();
361
362 assert_that!(p.parse_from_str("yesterday"))
363 .is_ok()
364 .is_equal_to(&expect);
365 }
366
367 #[test]
368 fn test_parse_date() {
369 let p = RangeParser::default();
370 let expect: DateRange =
371 Date::new(2022, 10, 20).expect("Hardcoded date must work").into();
372
373 assert_that!(p.parse_from_str("2022-10-20"))
374 .is_ok()
375 .is_equal_to(&expect);
376 }
377
378 #[test]
379 fn test_parse_dayname() {
380 let p = test_range_parser();
381 assert_that!(p.parse_from_str("monday"))
382 .is_ok()
383 .is_equal_to(&BASE_DATE.find_previous(Weekday::Mon).into());
384 }
385
386 #[test]
387 fn test_parse_later_dayname() {
388 let p = test_range_parser();
389 assert_that!(p.parse_from_str("wednesday"))
390 .is_ok()
391 .is_equal_to(&BASE_DATE.find_previous(Weekday::Wed).into());
392 }
393
394 #[test]
397 fn test_dates_both_dates() {
398 let expected = DateRange::new(
399 Date::new(2021, 12, 1).unwrap(),
400 Date::new(2021, 12, 8).unwrap()
401 );
402
403 let p = test_range_parser();
404 assert_that!(p.parse_from_str("2021-12-01 2021-12-07"))
405 .is_ok()
406 .is_equal_to(&expected);
407 }
408
409 #[test]
410 fn test_dates_both_dates_desc() {
411 let expected = DateRange::new(
412 Date::new(2022, 11, 13).unwrap(),
413 BASE_DATE.succ()
414 );
415
416 let p = test_range_parser();
417 assert_that!(p.parse_from_str("sunday today"))
418 .is_ok()
419 .is_equal_to(&expected);
420 }
421
422 #[test]
425 fn test_month_name() {
426 let tests = [
427 ("january", Date::new(2022, 1, 1), Date::new(2022, 2, 1)),
428 ("jan", Date::new(2022, 1, 1), Date::new(2022, 2, 1)),
429 ("february", Date::new(2022, 2, 1), Date::new(2022, 3, 1)),
430 ("feb", Date::new(2022, 2, 1), Date::new(2022, 3, 1)),
431 ("march", Date::new(2022, 3, 1), Date::new(2022, 4, 1)),
432 ("mar", Date::new(2022, 3, 1), Date::new(2022, 4, 1)),
433 ("april", Date::new(2022, 4, 1), Date::new(2022, 5, 1)),
434 ("apr", Date::new(2022, 4, 1), Date::new(2022, 5, 1)),
435 ("may", Date::new(2022, 5, 1), Date::new(2022, 6, 1)),
436 ("june", Date::new(2022, 6, 1), Date::new(2022, 7, 1)),
437 ("jun", Date::new(2022, 6, 1), Date::new(2022, 7, 1)),
438 ("july", Date::new(2022, 7, 1), Date::new(2022, 8, 1)),
439 ("jul", Date::new(2022, 7, 1), Date::new(2022, 8, 1)),
440 ("august", Date::new(2022, 8, 1), Date::new(2022, 9, 1)),
441 ("aug", Date::new(2022, 8, 1), Date::new(2022, 9, 1)),
442 ("september", Date::new(2022, 9, 1), Date::new(2022, 10, 1)),
443 ("sep", Date::new(2022, 9, 1), Date::new(2022, 10, 1)),
444 ("october", Date::new(2022, 10, 1), Date::new(2022, 11, 1)),
445 ("oct", Date::new(2022, 10, 1), Date::new(2022, 11, 1)),
446 ("november", Date::new(2021, 11, 1), Date::new(2021, 12, 1)),
447 ("nov", Date::new(2021, 11, 1), Date::new(2021, 12, 1)),
448 ("december", Date::new(2021, 12, 1), Date::new(2022, 1, 1)),
449 ("dec", Date::new(2021, 12, 1), Date::new(2022, 1, 1))
450 ];
451
452 let p = test_range_parser();
453 for (name, start_opt, end_opt) in tests.iter() {
454 let start = start_opt.as_ref().unwrap();
455 let end = end_opt.as_ref().unwrap();
456 let expected = DateRange::new(*start, *end);
457 assert_that!(p.parse_from_str(name))
458 .named(&name)
459 .is_ok()
460 .is_equal_to(&expected);
461 }
462 }
463
464 #[test]
465 fn test_this_week() {
466 let expected = DateRange::new(
467 Date::new(2022, 11, 13).unwrap(),
468 Date::new(2022, 11, 20).unwrap()
469 );
470
471 let p = test_range_parser();
472 assert_that!(p.parse_from_str("this week"))
473 .is_ok()
474 .is_equal_to(&expected);
475 }
476
477 #[test]
478 fn test_this_month() {
479 let expected = DateRange::new(
480 Date::new(2022, 11, 1).unwrap(),
481 Date::new(2022, 12, 1).unwrap()
482 );
483
484 let p = test_range_parser();
485 assert_that!(p.parse_from_str("this month"))
486 .is_ok()
487 .is_equal_to(&expected);
488 }
489
490 #[test]
491 fn test_this_year() {
492 let expected = DateRange::new(
493 Date::new(2022, 1, 1).unwrap(),
494 Date::new(2023, 1, 1).unwrap()
495 );
496
497 let p = test_range_parser();
498 assert_that!(p.parse_from_str("this year"))
499 .is_ok()
500 .is_equal_to(&expected);
501 }
502
503 #[test]
504 fn test_ytd() {
505 let expected = DateRange::new(Date::new(2022, 1, 1).unwrap(), BASE_DATE.succ());
506
507 let p = test_range_parser();
508 assert_that!(p.parse_from_str("ytd"))
509 .is_ok()
510 .is_equal_to(&expected);
511 }
512
513 #[test]
514 fn test_last_week() {
515 let expected = DateRange::new(
516 Date::new(2022, 11, 6).unwrap(),
517 Date::new(2022, 11, 13).unwrap()
518 );
519
520 let p = test_range_parser();
521 assert_that!(p.parse_from_str("last week"))
522 .is_ok()
523 .is_equal_to(&expected);
524 }
525
526 #[test]
527 fn test_last_month() {
528 let expected = DateRange::new(
529 Date::new(2022, 10, 1).unwrap(),
530 Date::new(2022, 11, 1).unwrap()
531 );
532
533 let p = test_range_parser();
534 assert_that!(p.parse_from_str("last month"))
535 .is_ok()
536 .is_equal_to(&expected);
537 }
538
539 #[test]
540 fn test_last_year() {
541 let expected = DateRange::new(
542 Date::new(2021, 1, 1).unwrap(),
543 Date::new(2022, 1, 1).unwrap()
544 );
545
546 let p = test_range_parser();
547 assert_that!(p.parse_from_str("last year"))
548 .is_ok()
549 .is_equal_to(&expected);
550 }
551}