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