1use chumsky::{error::Rich, prelude::*, text};
2
3use crate::{
4 DayReference, DayTime, Direction, LanguageParser, Meridiem, RelativeTime, Result, StandardDate,
5 Time, TimeExpression, TimeUnit, Weekday, WeekdayModifier,
6 common::{
7 ParserError, digit_number, four_digit_number, iso_datetime, keyword_ci, two_digit_number,
8 },
9 error::rich_errors_to_temps_error,
10 time_utils,
11};
12
13pub struct EnglishParser;
15
16fn whitespace_required<'a>() -> impl Parser<'a, &'a str, (), ParserError<'a>> + Clone {
17 one_of(" \t\n\r")
18 .labelled("whitespace")
19 .repeated()
20 .at_least(1)
21 .ignored()
22}
23
24fn number<'a>() -> impl Parser<'a, &'a str, i64, ParserError<'a>> + Clone {
25 choice((
26 digit_number(),
27 keyword_ci("an").to(1),
28 keyword_ci("one").to(1),
29 keyword_ci("a").to(1),
30 keyword_ci("two").to(2),
31 keyword_ci("three").to(3),
32 keyword_ci("four").to(4),
33 keyword_ci("five").to(5),
34 keyword_ci("six").to(6),
35 keyword_ci("seven").to(7),
36 keyword_ci("eight").to(8),
37 keyword_ci("nine").to(9),
38 keyword_ci("ten").to(10),
39 ))
40 .labelled("number")
41}
42
43fn time_unit<'a>() -> impl Parser<'a, &'a str, TimeUnit, ParserError<'a>> + Clone {
44 choice((
45 choice((
46 keyword_ci("seconds").to(TimeUnit::Second),
47 keyword_ci("second").to(TimeUnit::Second),
48 keyword_ci("secs").to(TimeUnit::Second),
49 keyword_ci("sec").to(TimeUnit::Second),
50 )),
51 choice((
52 keyword_ci("minutes").to(TimeUnit::Minute),
53 keyword_ci("minute").to(TimeUnit::Minute),
54 keyword_ci("mins").to(TimeUnit::Minute),
55 keyword_ci("min").to(TimeUnit::Minute),
56 )),
57 choice((
58 keyword_ci("hours").to(TimeUnit::Hour),
59 keyword_ci("hour").to(TimeUnit::Hour),
60 keyword_ci("hrs").to(TimeUnit::Hour),
61 keyword_ci("hr").to(TimeUnit::Hour),
62 )),
63 choice((
64 keyword_ci("days").to(TimeUnit::Day),
65 keyword_ci("day").to(TimeUnit::Day),
66 )),
67 choice((
68 keyword_ci("weeks").to(TimeUnit::Week),
69 keyword_ci("week").to(TimeUnit::Week),
70 keyword_ci("wks").to(TimeUnit::Week),
71 keyword_ci("wk").to(TimeUnit::Week),
72 )),
73 choice((
74 keyword_ci("months").to(TimeUnit::Month),
75 keyword_ci("month").to(TimeUnit::Month),
76 keyword_ci("mos").to(TimeUnit::Month),
77 keyword_ci("mo").to(TimeUnit::Month),
78 )),
79 choice((
80 keyword_ci("years").to(TimeUnit::Year),
81 keyword_ci("year").to(TimeUnit::Year),
82 keyword_ci("yrs").to(TimeUnit::Year),
83 keyword_ci("yr").to(TimeUnit::Year),
84 )),
85 choice((
86 keyword_ci("s").to(TimeUnit::Second),
87 keyword_ci("h").to(TimeUnit::Hour),
88 keyword_ci("d").to(TimeUnit::Day),
89 keyword_ci("w").to(TimeUnit::Week),
90 keyword_ci("y").to(TimeUnit::Year),
91 keyword_ci("m").to(TimeUnit::Minute),
92 )),
93 ))
94 .labelled("time unit")
95}
96
97fn weekday<'a>() -> impl Parser<'a, &'a str, Weekday, ParserError<'a>> + Clone {
98 choice((
99 choice((
100 keyword_ci("monday").to(Weekday::Monday),
101 keyword_ci("mon").to(Weekday::Monday),
102 )),
103 choice((
104 keyword_ci("tuesday").to(Weekday::Tuesday),
105 keyword_ci("tue").to(Weekday::Tuesday),
106 )),
107 choice((
108 keyword_ci("wednesday").to(Weekday::Wednesday),
109 keyword_ci("wed").to(Weekday::Wednesday),
110 )),
111 choice((
112 keyword_ci("thursday").to(Weekday::Thursday),
113 keyword_ci("thu").to(Weekday::Thursday),
114 )),
115 choice((
116 keyword_ci("friday").to(Weekday::Friday),
117 keyword_ci("fri").to(Weekday::Friday),
118 )),
119 choice((
120 keyword_ci("saturday").to(Weekday::Saturday),
121 keyword_ci("sat").to(Weekday::Saturday),
122 )),
123 choice((
124 keyword_ci("sunday").to(Weekday::Sunday),
125 keyword_ci("sun").to(Weekday::Sunday),
126 )),
127 ))
128 .labelled("weekday")
129}
130
131fn day_shortcuts<'a>() -> impl Parser<'a, &'a str, DayReference, ParserError<'a>> + Clone {
132 choice((
133 keyword_ci("today").to(DayReference::Today),
134 keyword_ci("yesterday").to(DayReference::Yesterday),
135 keyword_ci("tomorrow").to(DayReference::Tomorrow),
136 ))
137}
138
139fn weekday_modifier<'a>() -> impl Parser<'a, &'a str, WeekdayModifier, ParserError<'a>> + Clone {
140 choice((
141 keyword_ci("last").to(WeekdayModifier::Last),
142 keyword_ci("next").to(WeekdayModifier::Next),
143 ))
144}
145
146fn modified_weekday<'a>() -> impl Parser<'a, &'a str, DayReference, ParserError<'a>> + Clone {
147 weekday_modifier()
148 .then_ignore(whitespace_required())
149 .then(weekday())
150 .map(|(modifier, day)| DayReference::Weekday {
151 day,
152 modifier: Some(modifier),
153 })
154}
155
156fn simple_weekday<'a>() -> impl Parser<'a, &'a str, DayReference, ParserError<'a>> + Clone {
157 weekday().map(|day| DayReference::Weekday {
158 day,
159 modifier: None,
160 })
161}
162
163fn day_reference<'a>() -> impl Parser<'a, &'a str, DayReference, ParserError<'a>> + Clone {
164 choice((day_shortcuts(), modified_weekday(), simple_weekday()))
165}
166
167fn meridiem<'a>() -> impl Parser<'a, &'a str, Meridiem, ParserError<'a>> + Clone {
168 choice((
169 keyword_ci("a.m.").to(Meridiem::AM),
170 keyword_ci("p.m.").to(Meridiem::PM),
171 keyword_ci("am").to(Meridiem::AM),
172 keyword_ci("pm").to(Meridiem::PM),
173 ))
174 .labelled("am/pm")
175}
176
177fn time_with_minutes<'a>()
178-> impl Parser<'a, &'a str, (u8, u8, u8, Option<Meridiem>), ParserError<'a>> + Clone {
179 two_digit_number()
180 .then_ignore(just(':'))
181 .then(two_digit_number())
182 .then(just(':').ignore_then(two_digit_number()).or_not())
183 .then(text::whitespace().ignore_then(meridiem()).or_not())
184 .try_map(|(((hour, minute), second), mer), span| {
185 let second = second.unwrap_or(0);
186 if time_utils::is_valid_time(hour, minute, second, mer) {
187 Ok((hour, minute, second, mer))
188 } else {
189 Err(Rich::custom(span, "invalid time"))
190 }
191 })
192}
193
194fn hour_meridiem<'a>()
195-> impl Parser<'a, &'a str, (u8, u8, u8, Option<Meridiem>), ParserError<'a>> + Clone {
196 two_digit_number()
197 .then(text::whitespace().ignore_then(meridiem()))
198 .try_map(|(hour, mer), span| {
199 if time_utils::is_valid_time(hour, 0, 0, Some(mer)) {
200 Ok((hour, 0, 0, Some(mer)))
201 } else {
202 Err(Rich::custom(span, "invalid time"))
203 }
204 })
205}
206
207fn time_digits<'a>()
208-> impl Parser<'a, &'a str, (u8, u8, u8, Option<Meridiem>), ParserError<'a>> + Clone {
209 choice((time_with_minutes(), hour_meridiem()))
210}
211
212fn time_expr<'a>() -> impl Parser<'a, &'a str, TimeExpression, ParserError<'a>> + Clone {
213 time_digits().map(|(hour, minute, second, meridiem)| {
214 TimeExpression::Time(Time {
215 hour,
216 minute,
217 second,
218 meridiem,
219 })
220 })
221}
222
223fn day_at_time<'a>() -> impl Parser<'a, &'a str, TimeExpression, ParserError<'a>> + Clone {
224 day_reference()
225 .then_ignore(whitespace_required())
226 .then_ignore(keyword_ci("at"))
227 .then_ignore(whitespace_required())
228 .then(time_digits())
229 .map(|(day, (hour, minute, second, meridiem))| {
230 TimeExpression::DayTime(DayTime {
231 day,
232 time: Time {
233 hour,
234 minute,
235 second,
236 meridiem,
237 },
238 })
239 })
240}
241
242fn relative_past<'a>() -> impl Parser<'a, &'a str, TimeExpression, ParserError<'a>> + Clone {
243 number()
244 .then_ignore(whitespace_required())
245 .then(time_unit())
246 .then_ignore(whitespace_required())
247 .then_ignore(keyword_ci("ago"))
248 .map(|(amount, unit)| {
249 TimeExpression::Relative(RelativeTime {
250 amount,
251 unit,
252 direction: Direction::Past,
253 })
254 })
255}
256
257fn relative_future<'a>() -> impl Parser<'a, &'a str, TimeExpression, ParserError<'a>> + Clone {
258 keyword_ci("in")
259 .ignore_then(whitespace_required())
260 .ignore_then(number())
261 .then_ignore(whitespace_required())
262 .then(time_unit())
263 .map(|(amount, unit)| {
264 TimeExpression::Relative(RelativeTime {
265 amount,
266 unit,
267 direction: Direction::Future,
268 })
269 })
270}
271
272fn now_expr<'a>() -> impl Parser<'a, &'a str, TimeExpression, ParserError<'a>> + Clone {
273 keyword_ci("now").to(TimeExpression::Now)
274}
275
276fn date_format<'a>() -> impl Parser<'a, &'a str, TimeExpression, ParserError<'a>> + Clone {
277 let iso_like = four_digit_number()
278 .then_ignore(just('-'))
279 .then(two_digit_number())
280 .then_ignore(just('-'))
281 .then(two_digit_number())
282 .try_map(|((year, month), day), span| {
283 if time_utils::is_valid_calendar_date(year, month, day) {
284 Ok(TimeExpression::Date(StandardDate { day, month, year }))
285 } else {
286 Err(Rich::custom(span, "invalid calendar date"))
287 }
288 });
289
290 let international = two_digit_number()
291 .then(one_of(['/', '-']))
292 .then(two_digit_number())
293 .then(one_of(['/', '-']))
294 .then(four_digit_number())
295 .try_map(|((((day, first), month), second), year), span| {
296 if first == second && time_utils::is_valid_calendar_date(year, month, day) {
297 Ok(TimeExpression::Date(StandardDate { day, month, year }))
298 } else {
299 Err(Rich::custom(span, "invalid date"))
300 }
301 });
302
303 choice((iso_like, international))
304}
305
306fn parser<'a>() -> impl Parser<'a, &'a str, TimeExpression, ParserError<'a>> {
307 choice((
308 iso_datetime().labelled("ISO 8601 datetime"),
309 date_format().labelled("calendar date"),
310 day_at_time().labelled("day with time"),
311 now_expr().labelled("`now`"),
312 day_reference()
313 .map(TimeExpression::Day)
314 .labelled("day reference"),
315 time_expr().labelled("time of day"),
316 relative_past().labelled("`<n> <unit> ago`"),
317 relative_future().labelled("`in <n> <unit>`"),
318 ))
319 .padded()
320 .then_ignore(end())
321}
322
323impl LanguageParser for EnglishParser {
324 fn parse(&self, input: &str) -> Result<TimeExpression> {
325 parser()
326 .parse(input)
327 .into_result()
328 .map_err(|errs| rich_errors_to_temps_error(input, errs))
329 }
330}