1use chumsky::{error::Rich, prelude::*};
2
3use crate::{
4 DayReference, DayTime, Direction, LanguageParser, Meridiem, RelativeTime, Result, StandardDate,
5 Time, TimeExpression, TimeUnit, Weekday, WeekdayModifier,
6 common::{
7 ParserError, TokenInput, digit_number, four_digit_number, iso_datetime, opt_space,
8 phrase_ci, phrases_ci, punct, space, token_stream, two_digit_number, word_ci,
9 },
10 error::rich_errors_to_temps_error,
11 lexer::lex,
12 time_utils,
13};
14
15pub struct EnglishParser;
17
18fn number<'t, 's: 't, I>() -> impl Parser<'t, I, i64, ParserError<'t, 's>> + Clone
19where
20 I: TokenInput<'t, 's>,
21{
22 choice((
23 digit_number(),
24 phrases_ci([
25 ("a", 1i64),
26 ("an", 1),
27 ("one", 1),
28 ("two", 2),
29 ("three", 3),
30 ("four", 4),
31 ("five", 5),
32 ("six", 6),
33 ("seven", 7),
34 ("eight", 8),
35 ("nine", 9),
36 ("ten", 10),
37 ("a couple", 2),
38 ("a couple of", 2),
39 ("couple of", 2),
40 ("a few", 3),
41 ("a dozen", 12),
42 ]),
43 ))
44 .labelled("number")
45}
46
47fn time_unit<'t, 's: 't, I>() -> impl Parser<'t, I, TimeUnit, ParserError<'t, 's>> + Clone
48where
49 I: TokenInput<'t, 's>,
50{
51 phrases_ci([
52 ("second", TimeUnit::Second),
53 ("seconds", TimeUnit::Second),
54 ("sec", TimeUnit::Second),
55 ("secs", TimeUnit::Second),
56 ("s", TimeUnit::Second),
57 ("minute", TimeUnit::Minute),
58 ("minutes", TimeUnit::Minute),
59 ("min", TimeUnit::Minute),
60 ("mins", TimeUnit::Minute),
61 ("m", TimeUnit::Minute),
62 ("hour", TimeUnit::Hour),
63 ("hours", TimeUnit::Hour),
64 ("hr", TimeUnit::Hour),
65 ("hrs", TimeUnit::Hour),
66 ("h", TimeUnit::Hour),
67 ("day", TimeUnit::Day),
68 ("days", TimeUnit::Day),
69 ("d", TimeUnit::Day),
70 ("week", TimeUnit::Week),
71 ("weeks", TimeUnit::Week),
72 ("wk", TimeUnit::Week),
73 ("wks", TimeUnit::Week),
74 ("w", TimeUnit::Week),
75 ("month", TimeUnit::Month),
76 ("months", TimeUnit::Month),
77 ("mo", TimeUnit::Month),
78 ("mos", TimeUnit::Month),
79 ("year", TimeUnit::Year),
80 ("years", TimeUnit::Year),
81 ("yr", TimeUnit::Year),
82 ("yrs", TimeUnit::Year),
83 ("y", TimeUnit::Year),
84 ])
85 .labelled("time unit")
86}
87
88fn scaled_time_unit<'t, 's: 't, I>()
95-> impl Parser<'t, I, (TimeUnit, i64), ParserError<'t, 's>> + Clone
96where
97 I: TokenInput<'t, 's>,
98{
99 choice((
100 phrases_ci([("fortnight", ()), ("fortnights", ())]).to((TimeUnit::Week, 2i64)),
101 time_unit().map(|unit| (unit, 1i64)),
102 ))
103 .labelled("time unit")
104}
105
106fn amount_and_unit<'t, 's: 't, I>()
113-> impl Parser<'t, I, (i64, TimeUnit), ParserError<'t, 's>> + Clone
114where
115 I: TokenInput<'t, 's>,
116{
117 number()
118 .then_ignore(space())
119 .then(scaled_time_unit())
120 .try_map(|(amount, (unit, per_unit)), span| {
121 amount
122 .checked_mul(per_unit)
123 .map(|amount| (amount, unit))
124 .ok_or_else(|| Rich::custom(span, "amount out of range"))
125 })
126}
127
128fn weekday<'t, 's: 't, I>() -> impl Parser<'t, I, Weekday, ParserError<'t, 's>> + Clone
129where
130 I: TokenInput<'t, 's>,
131{
132 phrases_ci([
133 ("monday", Weekday::Monday),
134 ("mon", Weekday::Monday),
135 ("tuesday", Weekday::Tuesday),
136 ("tue", Weekday::Tuesday),
137 ("wednesday", Weekday::Wednesday),
138 ("wed", Weekday::Wednesday),
139 ("thursday", Weekday::Thursday),
140 ("thu", Weekday::Thursday),
141 ("friday", Weekday::Friday),
142 ("fri", Weekday::Friday),
143 ("saturday", Weekday::Saturday),
144 ("sat", Weekday::Saturday),
145 ("sunday", Weekday::Sunday),
146 ("sun", Weekday::Sunday),
147 ])
148 .labelled("weekday")
149}
150
151fn day_shortcuts<'t, 's: 't, I>() -> impl Parser<'t, I, DayReference, ParserError<'t, 's>> + Clone
152where
153 I: TokenInput<'t, 's>,
154{
155 phrases_ci([
156 ("today", DayReference::Today),
157 ("yesterday", DayReference::Yesterday),
158 ("tomorrow", DayReference::Tomorrow),
159 ("day after tomorrow", DayReference::DayAfterTomorrow),
160 ("day before yesterday", DayReference::DayBeforeYesterday),
161 ])
162}
163
164fn weekday_modifier<'t, 's: 't, I>()
165-> impl Parser<'t, I, WeekdayModifier, ParserError<'t, 's>> + Clone
166where
167 I: TokenInput<'t, 's>,
168{
169 choice((
170 word_ci("last").to(WeekdayModifier::Last),
171 word_ci("next").to(WeekdayModifier::Next),
172 ))
173}
174
175fn modified_weekday<'t, 's: 't, I>() -> impl Parser<'t, I, DayReference, ParserError<'t, 's>> + Clone
176where
177 I: TokenInput<'t, 's>,
178{
179 weekday_modifier()
180 .then_ignore(space())
181 .then(weekday())
182 .map(|(modifier, day)| DayReference::Weekday {
183 day,
184 modifier: Some(modifier),
185 })
186}
187
188fn simple_weekday<'t, 's: 't, I>() -> impl Parser<'t, I, DayReference, ParserError<'t, 's>> + Clone
189where
190 I: TokenInput<'t, 's>,
191{
192 weekday().map(|day| DayReference::Weekday {
193 day,
194 modifier: None,
195 })
196}
197
198fn day_reference<'t, 's: 't, I>() -> impl Parser<'t, I, DayReference, ParserError<'t, 's>> + Clone
199where
200 I: TokenInput<'t, 's>,
201{
202 choice((
213 the_day_reference(),
214 day_shortcuts(),
215 modified_weekday(),
216 this_weekday(),
217 weekend_ref(),
218 simple_weekday(),
219 ))
220}
221
222fn meridiem<'t, 's: 't, I>() -> impl Parser<'t, I, Meridiem, ParserError<'t, 's>> + Clone
223where
224 I: TokenInput<'t, 's>,
225{
226 phrases_ci([
227 ("am", Meridiem::AM),
228 ("pm", Meridiem::PM),
229 ("a.m.", Meridiem::AM),
230 ("p.m.", Meridiem::PM),
231 ])
232 .labelled("am/pm")
233}
234
235fn time_with_minutes<'t, 's: 't, I>()
236-> impl Parser<'t, I, (u8, u8, u8, Option<Meridiem>), ParserError<'t, 's>> + Clone
237where
238 I: TokenInput<'t, 's>,
239{
240 two_digit_number()
241 .then_ignore(punct(':'))
242 .then(two_digit_number())
243 .then(punct(':').ignore_then(two_digit_number()).or_not())
244 .then(opt_space().ignore_then(meridiem()).or_not())
245 .try_map(|(((hour, minute), second), mer), span| {
246 let second = second.unwrap_or(0);
247 if time_utils::is_valid_time(hour, minute, second, mer) {
248 Ok((hour, minute, second, mer))
249 } else {
250 Err(Rich::custom(span, "invalid time"))
251 }
252 })
253}
254
255fn hour_meridiem<'t, 's: 't, I>()
256-> impl Parser<'t, I, (u8, u8, u8, Option<Meridiem>), ParserError<'t, 's>> + Clone
257where
258 I: TokenInput<'t, 's>,
259{
260 two_digit_number()
261 .then(opt_space().ignore_then(meridiem()))
262 .try_map(|(hour, mer), span| {
263 if time_utils::is_valid_time(hour, 0, 0, Some(mer)) {
264 Ok((hour, 0, 0, Some(mer)))
265 } else {
266 Err(Rich::custom(span, "invalid time"))
267 }
268 })
269}
270
271fn time_digits<'t, 's: 't, I>()
272-> impl Parser<'t, I, (u8, u8, u8, Option<Meridiem>), ParserError<'t, 's>> + Clone
273where
274 I: TokenInput<'t, 's>,
275{
276 choice((time_with_minutes(), hour_meridiem()))
277}
278
279fn raw_hour<'t, 's: 't, I>() -> impl Parser<'t, I, u8, ParserError<'t, 's>> + Clone
281where
282 I: TokenInput<'t, 's>,
283{
284 choice((
285 two_digit_number().try_map(|h, span| {
286 if h <= 23 {
287 Ok(h)
288 } else {
289 Err(Rich::custom(span, "hour must be 0-23"))
290 }
291 }),
292 word_ci("noon").to(12u8),
293 word_ci("midnight").to(0u8),
294 ))
295}
296
297fn fractional_time<'t, 's: 't, I>()
299-> impl Parser<'t, I, (u8, u8, u8, Option<Meridiem>), ParserError<'t, 's>> + Clone
300where
301 I: TokenInput<'t, 's>,
302{
303 let half_past = phrase_ci("half past")
304 .ignore_then(space())
305 .ignore_then(raw_hour())
306 .map(|h| (h, 30u8, 0u8, None::<Meridiem>));
307
308 let quarter_past = phrase_ci("quarter past")
309 .ignore_then(space())
310 .ignore_then(raw_hour())
311 .map(|h| (h, 15u8, 0u8, None::<Meridiem>));
312
313 let quarter_to = phrase_ci("quarter to")
314 .ignore_then(space())
315 .ignore_then(raw_hour())
316 .map(|h| {
317 if h == 0 {
318 (23u8, 45u8, 0u8, None::<Meridiem>)
319 } else {
320 (h - 1, 45u8, 0u8, None::<Meridiem>)
321 }
322 });
323
324 choice((half_past, quarter_past, quarter_to)).try_map(|(hour, minute, second, mer), span| {
325 if time_utils::is_valid_time(hour, minute, second, mer) {
326 Ok((hour, minute, second, mer))
327 } else {
328 Err(Rich::custom(span, "invalid time"))
329 }
330 })
331}
332
333fn time_expr<'t, 's: 't, I>() -> impl Parser<'t, I, TimeExpression, ParserError<'t, 's>> + Clone
334where
335 I: TokenInput<'t, 's>,
336{
337 choice((
338 fractional_time().map(|(hour, minute, second, meridiem)| {
339 TimeExpression::Time(Time {
340 hour,
341 minute,
342 second,
343 meridiem,
344 })
345 }),
346 time_digits().map(|(hour, minute, second, meridiem)| {
347 TimeExpression::Time(Time {
348 hour,
349 minute,
350 second,
351 meridiem,
352 })
353 }),
354 ))
355}
356
357fn named_time<'t, 's: 't, I>() -> impl Parser<'t, I, TimeExpression, ParserError<'t, 's>> + Clone
358where
359 I: TokenInput<'t, 's>,
360{
361 choice((
362 word_ci("noon").to(TimeExpression::Time(Time {
363 hour: 12,
364 minute: 0,
365 second: 0,
366 meridiem: None,
367 })),
368 word_ci("midnight").to(TimeExpression::Time(Time {
369 hour: 0,
370 minute: 0,
371 second: 0,
372 meridiem: None,
373 })),
374 word_ci("teatime").to(TimeExpression::Time(Time {
375 hour: 16,
376 minute: 0,
377 second: 0,
378 meridiem: None,
379 })),
380 ))
381}
382
383fn part_of_day<'t, 's: 't, I>() -> impl Parser<'t, I, Time, ParserError<'t, 's>> + Clone
386where
387 I: TokenInput<'t, 's>,
388{
389 choice((
390 word_ci("morning").to(Time {
391 hour: 8,
392 minute: 0,
393 second: 0,
394 meridiem: None,
395 }),
396 word_ci("afternoon").to(Time {
397 hour: 13,
398 minute: 0,
399 second: 0,
400 meridiem: None,
401 }),
402 word_ci("evening").to(Time {
403 hour: 18,
404 minute: 0,
405 second: 0,
406 meridiem: None,
407 }),
408 word_ci("night").to(Time {
409 hour: 20,
410 minute: 0,
411 second: 0,
412 meridiem: None,
413 }),
414 ))
415}
416
417fn this_part_of_day<'t, 's: 't, I>()
419-> impl Parser<'t, I, TimeExpression, ParserError<'t, 's>> + Clone
420where
421 I: TokenInput<'t, 's>,
422{
423 word_ci("this")
424 .ignore_then(space())
425 .ignore_then(part_of_day())
426 .map(|time| {
427 TimeExpression::DayTime(DayTime {
428 day: DayReference::Today,
429 time,
430 })
431 })
432}
433
434fn this_weekday<'t, 's: 't, I>() -> impl Parser<'t, I, DayReference, ParserError<'t, 's>> + Clone
436where
437 I: TokenInput<'t, 's>,
438{
439 word_ci("this")
440 .ignore_then(space())
441 .ignore_then(weekday())
442 .map(|day| DayReference::Weekday {
443 day,
444 modifier: None,
445 })
446}
447
448fn weekend_ref<'t, 's: 't, I>() -> impl Parser<'t, I, DayReference, ParserError<'t, 's>> + Clone
450where
451 I: TokenInput<'t, 's>,
452{
453 choice((
454 phrase_ci("this weekend").to(DayReference::Weekday {
455 day: Weekday::Saturday,
456 modifier: Some(WeekdayModifier::This),
457 }),
458 phrase_ci("next weekend").to(DayReference::Weekday {
459 day: Weekday::Saturday,
460 modifier: Some(WeekdayModifier::Next),
461 }),
462 ))
463}
464
465fn standalone_daytime<'t, 's: 't, I>()
467-> impl Parser<'t, I, TimeExpression, ParserError<'t, 's>> + Clone
468where
469 I: TokenInput<'t, 's>,
470{
471 choice((
472 word_ci("tonight").to(TimeExpression::DayTime(DayTime {
473 day: DayReference::Today,
474 time: Time {
475 hour: 20,
476 minute: 0,
477 second: 0,
478 meridiem: None,
479 },
480 })),
481 choice((
482 word_ci("eod"),
483 phrase_ci("end of day"),
484 phrase_ci("end of the day"),
485 ))
486 .to(TimeExpression::DayTime(DayTime {
487 day: DayReference::Today,
488 time: Time {
489 hour: 17,
490 minute: 0,
491 second: 0,
492 meridiem: None,
493 },
494 })),
495 ))
496}
497
498fn the_day_reference<'t, 's: 't, I>()
504-> impl Parser<'t, I, DayReference, ParserError<'t, 's>> + Clone
505where
506 I: TokenInput<'t, 's>,
507{
508 word_ci("the").ignore_then(space()).ignore_then(phrases_ci([
509 ("day after tomorrow", DayReference::DayAfterTomorrow),
510 ("day before yesterday", DayReference::DayBeforeYesterday),
511 ]))
512}
513
514fn fortnight<'t, 's: 't, I>() -> impl Parser<'t, I, TimeExpression, ParserError<'t, 's>> + Clone
521where
522 I: TokenInput<'t, 's>,
523{
524 word_ci("fortnight").to(TimeExpression::Relative(RelativeTime {
525 amount: 2,
526 unit: TimeUnit::Week,
527 direction: Direction::Future,
528 }))
529}
530
531fn later_expr<'t, 's: 't, I>() -> impl Parser<'t, I, TimeExpression, ParserError<'t, 's>> + Clone
533where
534 I: TokenInput<'t, 's>,
535{
536 word_ci("later")
539 .ignore_then(space().ignore_then(word_ci("today")).or_not())
540 .map(|today| match today {
541 Some(()) => TimeExpression::LaterToday,
542 None => TimeExpression::Relative(RelativeTime {
543 amount: 2,
544 unit: TimeUnit::Hour,
545 direction: Direction::Future,
546 }),
547 })
548}
549
550fn week_from_now<'t, 's: 't, I>() -> impl Parser<'t, I, TimeExpression, ParserError<'t, 's>> + Clone
552where
553 I: TokenInput<'t, 's>,
554{
555 choice((phrase_ci("a week from today"), phrase_ci("a week from now"))).to(
556 TimeExpression::Relative(RelativeTime {
557 amount: 1,
558 unit: TimeUnit::Week,
559 direction: Direction::Future,
560 }),
561 )
562}
563
564fn day_expr<'t, 's: 't, I>() -> impl Parser<'t, I, TimeExpression, ParserError<'t, 's>> + Clone
573where
574 I: TokenInput<'t, 's>,
575{
576 let at_time = word_ci("at")
577 .ignore_then(space())
578 .ignore_then(time_digits())
579 .map(|(hour, minute, second, meridiem)| Time {
580 hour,
581 minute,
582 second,
583 meridiem,
584 });
585
586 day_reference()
587 .then(
588 space()
589 .ignore_then(choice((at_time, part_of_day())))
590 .or_not(),
591 )
592 .map(|(day, time)| match time {
593 Some(time) => TimeExpression::DayTime(DayTime { day, time }),
594 None => TimeExpression::Day(day),
595 })
596}
597
598fn relative_past<'t, 's: 't, I>() -> impl Parser<'t, I, TimeExpression, ParserError<'t, 's>> + Clone
599where
600 I: TokenInput<'t, 's>,
601{
602 amount_and_unit()
603 .then_ignore(space())
604 .then_ignore(word_ci("ago"))
605 .map(|(amount, unit)| {
606 TimeExpression::Relative(RelativeTime {
607 amount,
608 unit,
609 direction: Direction::Past,
610 })
611 })
612}
613
614fn relative_future<'t, 's: 't, I>()
615-> impl Parser<'t, I, TimeExpression, ParserError<'t, 's>> + Clone
616where
617 I: TokenInput<'t, 's>,
618{
619 word_ci("in")
620 .ignore_then(space())
621 .ignore_then(amount_and_unit())
622 .map(|(amount, unit)| {
623 TimeExpression::Relative(RelativeTime {
624 amount,
625 unit,
626 direction: Direction::Future,
627 })
628 })
629}
630
631fn now_expr<'t, 's: 't, I>() -> impl Parser<'t, I, TimeExpression, ParserError<'t, 's>> + Clone
632where
633 I: TokenInput<'t, 's>,
634{
635 word_ci("now").to(TimeExpression::Now)
636}
637
638fn date_format<'t, 's: 't, I>() -> impl Parser<'t, I, TimeExpression, ParserError<'t, 's>> + Clone
639where
640 I: TokenInput<'t, 's>,
641{
642 let separator = choice((punct('/').to('/'), punct('-').to('-')));
645
646 two_digit_number()
647 .then(separator.clone())
648 .then(two_digit_number())
649 .then(separator)
650 .then(four_digit_number())
651 .try_map(|((((day, first), month), second), year), span| {
652 if first == second && time_utils::is_valid_calendar_date(year, month, day) {
653 Ok(TimeExpression::Date(StandardDate { day, month, year }))
654 } else {
655 Err(Rich::custom(span, "invalid date"))
656 }
657 })
658}
659
660fn parser<'t, 's: 't, I>() -> impl Parser<'t, I, TimeExpression, ParserError<'t, 's>>
661where
662 I: TokenInput<'t, 's>,
663{
664 choice((
674 iso_datetime().labelled("ISO 8601 datetime"),
675 date_format().labelled("calendar date"),
676 day_expr().labelled("day, optionally with a time"),
677 now_expr().labelled("`now`"),
678 standalone_daytime().labelled("standalone (tonight, EOD)"),
679 this_part_of_day().labelled("this morning/afternoon/evening"),
680 named_time().labelled("named time (noon, midnight, teatime)"),
681 time_expr().labelled("time of day"),
682 relative_past().labelled("`<n> <unit> ago`"),
683 relative_future().labelled("`in <n> <unit>`"),
684 week_from_now().labelled("a week from now/today"),
685 fortnight().labelled("fortnight"),
686 later_expr().labelled("later/later today"),
687 ))
688 .padded_by(opt_space())
689 .then_ignore(end())
690}
691
692impl LanguageParser for EnglishParser {
693 fn parse(&self, input: &str) -> Result<TimeExpression> {
694 let tokens = lex(input);
695 parser()
696 .parse(token_stream(input, &tokens))
697 .into_result()
698 .map_err(|errs| rich_errors_to_temps_error(input, errs))
699 }
700}