Skip to main content

temps_core/language/
english.rs

1use winnow::{
2    Parser,
3    ascii::{Caseless, multispace0, multispace1},
4    combinator::{alt, delimited, opt, preceded},
5};
6
7use crate::{
8    DayReference, DayTime, Direction, LanguageParser, Meridiem, RelativeTime, Result, StandardDate,
9    Time, TimeExpression, TimeUnit, Weekday, WeekdayModifier, common, error::ParseErrorExt,
10    time_utils,
11};
12
13/// Parser for English natural language time expressions.
14pub struct EnglishParser;
15
16impl EnglishParser {
17    fn parse_number(input: &mut &str) -> winnow::Result<i64> {
18        alt((
19            common::parse_digit_number,
20            alt((
21                Caseless("an").value(1),
22                Caseless("a").value(1),
23                Caseless("one").value(1),
24                Caseless("two").value(2),
25                Caseless("three").value(3),
26                Caseless("four").value(4),
27                Caseless("five").value(5),
28            )),
29            alt((
30                Caseless("six").value(6),
31                Caseless("seven").value(7),
32                Caseless("eight").value(8),
33                Caseless("nine").value(9),
34                Caseless("ten").value(10),
35            )),
36        ))
37        .parse_next(input)
38    }
39
40    fn parse_time_unit(input: &mut &str) -> winnow::Result<TimeUnit> {
41        alt((
42            alt((
43                Caseless("seconds").value(TimeUnit::Second),
44                Caseless("second").value(TimeUnit::Second),
45                Caseless("secs").value(TimeUnit::Second),
46                Caseless("sec").value(TimeUnit::Second),
47                Caseless("s").value(TimeUnit::Second),
48            )),
49            alt((
50                Caseless("minutes").value(TimeUnit::Minute),
51                Caseless("minute").value(TimeUnit::Minute),
52                Caseless("mins").value(TimeUnit::Minute),
53                Caseless("min").value(TimeUnit::Minute),
54            )),
55            alt((
56                Caseless("hours").value(TimeUnit::Hour),
57                Caseless("hour").value(TimeUnit::Hour),
58                Caseless("hrs").value(TimeUnit::Hour),
59                Caseless("hr").value(TimeUnit::Hour),
60                Caseless("h").value(TimeUnit::Hour),
61            )),
62            alt((
63                Caseless("days").value(TimeUnit::Day),
64                Caseless("day").value(TimeUnit::Day),
65                Caseless("d").value(TimeUnit::Day),
66            )),
67            alt((
68                Caseless("weeks").value(TimeUnit::Week),
69                Caseless("week").value(TimeUnit::Week),
70                Caseless("wks").value(TimeUnit::Week),
71                Caseless("wk").value(TimeUnit::Week),
72                Caseless("w").value(TimeUnit::Week),
73            )),
74            alt((
75                Caseless("months").value(TimeUnit::Month),
76                Caseless("month").value(TimeUnit::Month),
77                Caseless("mos").value(TimeUnit::Month),
78                Caseless("mo").value(TimeUnit::Month),
79            )),
80            alt((
81                Caseless("years").value(TimeUnit::Year),
82                Caseless("year").value(TimeUnit::Year),
83                Caseless("yrs").value(TimeUnit::Year),
84                Caseless("yr").value(TimeUnit::Year),
85                Caseless("y").value(TimeUnit::Year),
86            )),
87            // Single-letter abbreviations last to avoid ambiguity
88            Caseless("m").value(TimeUnit::Minute),
89        ))
90        .parse_next(input)
91    }
92
93    fn parse_relative_past(input: &mut &str) -> winnow::Result<TimeExpression> {
94        (
95            Self::parse_number,
96            multispace1,
97            Self::parse_time_unit,
98            multispace1,
99            Caseless("ago"),
100        )
101            .map(|(amount, _, unit, _, _)| {
102                TimeExpression::Relative(RelativeTime {
103                    amount,
104                    unit,
105                    direction: Direction::Past,
106                })
107            })
108            .parse_next(input)
109    }
110
111    fn parse_relative_future(input: &mut &str) -> winnow::Result<TimeExpression> {
112        preceded(
113            (Caseless("in"), multispace1),
114            (Self::parse_number, multispace1, Self::parse_time_unit),
115        )
116        .map(|(amount, _, unit)| {
117            TimeExpression::Relative(RelativeTime {
118                amount,
119                unit,
120                direction: Direction::Future,
121            })
122        })
123        .parse_next(input)
124    }
125
126    fn parse_now(input: &mut &str) -> winnow::Result<TimeExpression> {
127        Caseless("now").value(TimeExpression::Now).parse_next(input)
128    }
129
130    fn parse_iso_datetime(input: &mut &str) -> winnow::Result<TimeExpression> {
131        common::parse_iso_datetime(input)
132    }
133
134    fn parse_weekday(input: &mut &str) -> winnow::Result<Weekday> {
135        alt((
136            alt((
137                Caseless("monday").value(Weekday::Monday),
138                Caseless("mon").value(Weekday::Monday),
139            )),
140            alt((
141                Caseless("tuesday").value(Weekday::Tuesday),
142                Caseless("tue").value(Weekday::Tuesday),
143            )),
144            alt((
145                Caseless("wednesday").value(Weekday::Wednesday),
146                Caseless("wed").value(Weekday::Wednesday),
147            )),
148            alt((
149                Caseless("thursday").value(Weekday::Thursday),
150                Caseless("thu").value(Weekday::Thursday),
151            )),
152            alt((
153                Caseless("friday").value(Weekday::Friday),
154                Caseless("fri").value(Weekday::Friday),
155            )),
156            alt((
157                Caseless("saturday").value(Weekday::Saturday),
158                Caseless("sat").value(Weekday::Saturday),
159            )),
160            alt((
161                Caseless("sunday").value(Weekday::Sunday),
162                Caseless("sun").value(Weekday::Sunday),
163            )),
164        ))
165        .parse_next(input)
166    }
167
168    fn parse_day_shortcuts(input: &mut &str) -> winnow::Result<DayReference> {
169        alt((
170            Caseless("today").value(DayReference::Today),
171            Caseless("yesterday").value(DayReference::Yesterday),
172            Caseless("tomorrow").value(DayReference::Tomorrow),
173        ))
174        .parse_next(input)
175    }
176
177    fn parse_weekday_modifier(input: &mut &str) -> winnow::Result<WeekdayModifier> {
178        alt((
179            Caseless("last").value(WeekdayModifier::Last),
180            Caseless("next").value(WeekdayModifier::Next),
181        ))
182        .parse_next(input)
183    }
184
185    fn parse_modified_weekday(input: &mut &str) -> winnow::Result<DayReference> {
186        (
187            Self::parse_weekday_modifier,
188            multispace1,
189            Self::parse_weekday,
190        )
191            .map(|(modifier, _, day)| DayReference::Weekday {
192                day,
193                modifier: Some(modifier),
194            })
195            .parse_next(input)
196    }
197
198    fn parse_simple_weekday(input: &mut &str) -> winnow::Result<DayReference> {
199        Self::parse_weekday
200            .map(|day| DayReference::Weekday {
201                day,
202                modifier: None,
203            })
204            .parse_next(input)
205    }
206
207    fn parse_day_reference(input: &mut &str) -> winnow::Result<TimeExpression> {
208        alt((
209            Self::parse_day_shortcuts,
210            Self::parse_modified_weekday,
211            Self::parse_simple_weekday,
212        ))
213        .map(TimeExpression::Day)
214        .parse_next(input)
215    }
216
217    fn parse_meridiem(input: &mut &str) -> winnow::Result<Meridiem> {
218        alt((
219            alt((
220                Caseless("am").value(Meridiem::AM),
221                Caseless("a.m.").value(Meridiem::AM),
222            )),
223            alt((
224                Caseless("pm").value(Meridiem::PM),
225                Caseless("p.m.").value(Meridiem::PM),
226            )),
227        ))
228        .parse_next(input)
229    }
230
231    fn parse_time_digits(input: &mut &str) -> winnow::Result<(u8, u8, u8, Option<Meridiem>)> {
232        alt((Self::parse_time_with_minutes, Self::parse_hour_meridiem)).parse_next(input)
233    }
234
235    fn parse_time_with_minutes(input: &mut &str) -> winnow::Result<(u8, u8, u8, Option<Meridiem>)> {
236        (
237            common::parse_two_digit_number,
238            ':',
239            common::parse_two_digit_number,
240            opt(preceded(':', common::parse_two_digit_number)).map(|second| second.unwrap_or(0)),
241            opt(preceded(multispace0, Self::parse_meridiem)),
242        )
243            .verify_map(|(hour, _, minute, second, meridiem)| {
244                time_utils::is_valid_time(hour, minute, second, meridiem)
245                    .then_some((hour, minute, second, meridiem))
246            })
247            .parse_next(input)
248    }
249
250    fn parse_hour_meridiem(input: &mut &str) -> winnow::Result<(u8, u8, u8, Option<Meridiem>)> {
251        (
252            common::parse_two_digit_number,
253            multispace0,
254            Self::parse_meridiem,
255        )
256            .verify_map(|(hour, _, meridiem)| {
257                time_utils::is_valid_time(hour, 0, 0, Some(meridiem)).then_some((
258                    hour,
259                    0,
260                    0,
261                    Some(meridiem),
262                ))
263            })
264            .parse_next(input)
265    }
266
267    fn parse_time(input: &mut &str) -> winnow::Result<TimeExpression> {
268        Self::parse_time_digits
269            .map(|(hour, minute, second, meridiem)| {
270                TimeExpression::Time(Time {
271                    hour,
272                    minute,
273                    second,
274                    meridiem,
275                })
276            })
277            .parse_next(input)
278    }
279
280    fn parse_day_at_time(input: &mut &str) -> winnow::Result<TimeExpression> {
281        (
282            alt((
283                Self::parse_day_shortcuts,
284                Self::parse_modified_weekday,
285                Self::parse_simple_weekday,
286            )),
287            preceded(
288                multispace1,
289                preceded(
290                    Caseless("at"),
291                    preceded(multispace1, Self::parse_time_digits),
292                ),
293            ),
294        )
295            .map(|(day, (hour, minute, second, meridiem))| {
296                TimeExpression::DayTime(DayTime {
297                    day,
298                    time: Time {
299                        hour,
300                        minute,
301                        second,
302                        meridiem,
303                    },
304                })
305            })
306            .parse_next(input)
307    }
308
309    fn parse_date_format(input: &mut &str) -> winnow::Result<TimeExpression> {
310        alt((
311            // YYYY-MM-DD
312            (
313                common::parse_four_digit_number,
314                '-',
315                common::parse_two_digit_number,
316                '-',
317                common::parse_two_digit_number,
318            )
319                .verify_map(|(year, _, month, _, day)| {
320                    time_utils::is_valid_calendar_date(year, month, day)
321                        .then_some(TimeExpression::Date(StandardDate { day, month, year }))
322                }),
323            // DD/MM/YYYY or DD-MM-YYYY (International format)
324            (
325                common::parse_two_digit_number,
326                alt(('/', '-')),
327                common::parse_two_digit_number,
328                alt(('/', '-')),
329                common::parse_four_digit_number,
330            )
331                .verify_map(|(day, first_separator, month, second_separator, year)| {
332                    (first_separator == second_separator
333                        && time_utils::is_valid_calendar_date(year, month, day))
334                    .then_some(TimeExpression::Date(StandardDate { day, month, year }))
335                }),
336        ))
337        .parse_next(input)
338    }
339}
340
341impl LanguageParser for EnglishParser {
342    fn parse(&self, input: &str) -> Result<TimeExpression> {
343        delimited(
344            multispace0,
345            alt((
346                Self::parse_iso_datetime,
347                Self::parse_date_format,
348                Self::parse_day_at_time,
349                Self::parse_now,
350                Self::parse_day_reference,
351                Self::parse_time,
352                Self::parse_relative_past,
353                Self::parse_relative_future,
354            )),
355            multispace0,
356        )
357        .parse(input)
358        .map_err(|e| e.to_temps_error(input))
359    }
360}