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