Skip to content

Commit fdf6505

Browse files
authored
English parser for relative date expressions (#2)
This commit introduces the following changes: - Implement English parser for relative date expressions in en.rs - Add unit tests for the English parser, covering: - Keywords (today, tomorrow, day after tomorrow) - In N days expressions - This week and next week weekday expressions - Ordinal weekday of month expressions - Weekday parsing - 'This' and 'Next' modifiers - Number parsing (both digits and words)
1 parent 22f2919 commit fdf6505

File tree

3 files changed

+365
-1
lines changed

3 files changed

+365
-1
lines changed

bindings/python/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ fn parse(input: String, locale_name: String) -> PyResult<PyHumanDateExpr> {
1616
fn get_locale(locale_name: &String) -> PyResult<Locale> {
1717
match locale_name.as_ref() {
1818
"pt-BR" => Ok(Locale::BrazilianPortuguese),
19+
"en" => Ok(Locale::English),
1920
_ => Err(PyValueError::new_err(format!(
2021
"Unknown locale: {}",
2122
locale_name
Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
1+
use std::str::FromStr;
2+
3+
use chrono::{Month, Weekday};
4+
use winnow::{
5+
ascii::{digit1, space1},
6+
combinator::{alt, opt},
7+
error::ContextError,
8+
PResult, Parser,
9+
};
10+
11+
use crate::{HumanDateExpr, HumanDateKeyword, Ordinal};
12+
13+
pub struct HumanDateParserEnglishParser;
14+
15+
impl HumanDateParserEnglishParser {
16+
pub fn new() -> Self {
17+
HumanDateParserEnglishParser {}
18+
}
19+
}
20+
21+
impl Parser<&str, HumanDateExpr, ContextError> for HumanDateParserEnglishParser {
22+
fn parse_next(&mut self, input: &mut &str) -> PResult<HumanDateExpr> {
23+
let mut parser = alt((
24+
keyword.map(HumanDateExpr::Keyword),
25+
in_n_days.map(HumanDateExpr::InNDays),
26+
ordinal_weekday_of_month.map(|(ordinal, weekday, month)| {
27+
HumanDateExpr::OrdinalWeekdayOfMonth(ordinal, weekday, month)
28+
}),
29+
this_week_weekday.map(HumanDateExpr::ThisWeekWeekday),
30+
next_week_weekday.map(HumanDateExpr::NextWeekWeekday),
31+
));
32+
parser.parse_next(input)
33+
}
34+
}
35+
36+
fn keyword(input: &mut &str) -> PResult<HumanDateKeyword> {
37+
alt((
38+
"today".value(HumanDateKeyword::Today),
39+
"tomorrow".value(HumanDateKeyword::Tomorrow),
40+
"day after tomorrow".value(HumanDateKeyword::AfterTomorrow),
41+
))
42+
.parse_next(input)
43+
}
44+
45+
fn in_n_days(input: &mut &str) -> PResult<u64> {
46+
let (_, n, _) = (
47+
(alt(("in", "after")), space1),
48+
number,
49+
(space1, "day", opt('s')),
50+
)
51+
.parse_next(input)?;
52+
Ok(n)
53+
}
54+
55+
fn this_week_weekday(input: &mut &str) -> PResult<Weekday> {
56+
let (_, weekday) = (opt((this, space1)), weekday).parse_next(input)?;
57+
Ok(weekday)
58+
}
59+
60+
fn next_week_weekday(input: &mut &str) -> PResult<Weekday> {
61+
let (_, _, weekday) = (next, space1, weekday).parse_next(input)?;
62+
Ok(weekday)
63+
}
64+
65+
fn ordinal_weekday_of_month(input: &mut &str) -> PResult<(Ordinal, Weekday, Month)> {
66+
let (ordinal, _, weekday, _, _, _, month) =
67+
(ordinal, space1, weekday, space1, "of", space1, month).parse_next(input)?;
68+
Ok((ordinal, weekday, month))
69+
}
70+
71+
fn this(input: &mut &str) -> PResult<()> {
72+
alt(("this", "the current"))
73+
.void()
74+
.parse_next(input)
75+
}
76+
77+
fn next(input: &mut &str) -> PResult<()> {
78+
alt(("next", "the next", "the following"))
79+
.void()
80+
.parse_next(input)
81+
}
82+
83+
fn ordinal(input: &mut &str) -> PResult<Ordinal> {
84+
alt((
85+
"first".value(Ordinal::First),
86+
"second".value(Ordinal::Second),
87+
"third".value(Ordinal::Third),
88+
"fourth".value(Ordinal::Fourth),
89+
"fifth".value(Ordinal::Fifth),
90+
))
91+
.parse_next(input)
92+
}
93+
94+
fn number(input: &mut &str) -> PResult<u64> {
95+
alt((
96+
digit1.try_map(FromStr::from_str),
97+
"twenty".value(20),
98+
"nineteen".value(19),
99+
"eighteen".value(18),
100+
"seventeen".value(17),
101+
"sixteen".value(16),
102+
"fifteen".value(15),
103+
"fourteen".value(14),
104+
"thirteen".value(13),
105+
"twelve".value(12),
106+
"eleven".value(11),
107+
"ten".value(10),
108+
"nine".value(9),
109+
"eight".value(8),
110+
"seven".value(7),
111+
"six".value(6),
112+
"five".value(5),
113+
"four".value(4),
114+
"three".value(3),
115+
"two".value(2),
116+
"one".value(1),
117+
))
118+
.parse_next(input)
119+
}
120+
121+
fn weekday(input: &mut &str) -> PResult<Weekday> {
122+
alt((
123+
alt(("monday", "mon")).value(Weekday::Mon),
124+
alt(("tuesday", "tue")).value(Weekday::Tue),
125+
alt(("wednesday", "wed")).value(Weekday::Wed),
126+
alt(("thursday", "thu")).value(Weekday::Thu),
127+
alt(("friday", "fri")).value(Weekday::Fri),
128+
alt(("saturday", "sat")).value(Weekday::Sat),
129+
alt(("sunday", "sun")).value(Weekday::Sun),
130+
))
131+
.parse_next(input)
132+
}
133+
134+
fn month(input: &mut &str) -> PResult<Month> {
135+
alt((
136+
alt(("january", "jan")).value(Month::January),
137+
alt(("february", "feb")).value(Month::February),
138+
alt(("march", "mar")).value(Month::March),
139+
alt(("april", "apr")).value(Month::April),
140+
"may".value(Month::May),
141+
alt(("june", "jun")).value(Month::June),
142+
alt(("july", "jul")).value(Month::July),
143+
alt(("august", "aug")).value(Month::August),
144+
alt(("september", "sep")).value(Month::September),
145+
alt(("october", "oct")).value(Month::October),
146+
alt(("november", "nov")).value(Month::November),
147+
alt(("december", "dec")).value(Month::December),
148+
))
149+
.parse_next(input)
150+
}
151+
152+
#[cfg(test)]
153+
mod tests {
154+
use crate::{HumanDateExpr, HumanDateKeyword, Ordinal};
155+
use chrono::{Month, Weekday};
156+
use winnow::Parser;
157+
158+
use super::{next, number, this, weekday, HumanDateParserEnglishParser};
159+
160+
#[test]
161+
fn test_keywords() {
162+
let mut parser = HumanDateParserEnglishParser::new();
163+
assert_eq!(
164+
parser.parse_peek("today"),
165+
Ok(("", HumanDateExpr::Keyword(HumanDateKeyword::Today)))
166+
);
167+
assert_eq!(
168+
parser.parse_peek("tomorrow"),
169+
Ok(("", HumanDateExpr::Keyword(HumanDateKeyword::Tomorrow)))
170+
);
171+
assert_eq!(
172+
parser.parse_peek("day after tomorrow"),
173+
Ok(("", HumanDateExpr::Keyword(HumanDateKeyword::AfterTomorrow)))
174+
);
175+
}
176+
177+
#[test]
178+
fn test_in_n_days() {
179+
let mut parser = HumanDateParserEnglishParser::new();
180+
assert_eq!(
181+
parser.parse_peek("in 2 days"),
182+
Ok(("", HumanDateExpr::InNDays(2)))
183+
);
184+
assert_eq!(
185+
parser.parse_peek("after 2 days"),
186+
Ok(("", HumanDateExpr::InNDays(2)))
187+
);
188+
assert_eq!(
189+
parser.parse_peek("in two days"),
190+
Ok(("", HumanDateExpr::InNDays(2)))
191+
);
192+
assert_eq!(
193+
parser.parse_peek("after two days"),
194+
Ok(("", HumanDateExpr::InNDays(2)))
195+
);
196+
}
197+
198+
#[test]
199+
fn test_this_week_weekday() {
200+
let mut parser = HumanDateParserEnglishParser::new();
201+
assert_eq!(
202+
parser.parse_peek("this monday"),
203+
Ok(("", HumanDateExpr::ThisWeekWeekday(Weekday::Mon)))
204+
);
205+
assert_eq!(
206+
parser.parse_peek("tuesday"),
207+
Ok(("", HumanDateExpr::ThisWeekWeekday(Weekday::Tue)))
208+
);
209+
assert_eq!(
210+
parser.parse_peek("this wednesday"),
211+
Ok(("", HumanDateExpr::ThisWeekWeekday(Weekday::Wed)))
212+
);
213+
assert_eq!(
214+
parser.parse_peek("thursday"),
215+
Ok(("", HumanDateExpr::ThisWeekWeekday(Weekday::Thu)))
216+
);
217+
assert_eq!(
218+
parser.parse_peek("this friday"),
219+
Ok(("", HumanDateExpr::ThisWeekWeekday(Weekday::Fri)))
220+
);
221+
assert_eq!(
222+
parser.parse_peek("saturday"),
223+
Ok(("", HumanDateExpr::ThisWeekWeekday(Weekday::Sat)))
224+
);
225+
assert_eq!(
226+
parser.parse_peek("this sunday"),
227+
Ok(("", HumanDateExpr::ThisWeekWeekday(Weekday::Sun)))
228+
);
229+
}
230+
231+
#[test]
232+
fn test_next_week_weekday() {
233+
let mut parser = HumanDateParserEnglishParser::new();
234+
assert_eq!(
235+
parser.parse_peek("next monday"),
236+
Ok(("", HumanDateExpr::NextWeekWeekday(Weekday::Mon)))
237+
);
238+
assert_eq!(
239+
parser.parse_peek("next tuesday"),
240+
Ok(("", HumanDateExpr::NextWeekWeekday(Weekday::Tue)))
241+
);
242+
assert_eq!(
243+
parser.parse_peek("next wednesday"),
244+
Ok(("", HumanDateExpr::NextWeekWeekday(Weekday::Wed)))
245+
);
246+
assert_eq!(
247+
parser.parse_peek("next thursday"),
248+
Ok(("", HumanDateExpr::NextWeekWeekday(Weekday::Thu)))
249+
);
250+
assert_eq!(
251+
parser.parse_peek("next friday"),
252+
Ok(("", HumanDateExpr::NextWeekWeekday(Weekday::Fri)))
253+
);
254+
assert_eq!(
255+
parser.parse_peek("next saturday"),
256+
Ok(("", HumanDateExpr::NextWeekWeekday(Weekday::Sat)))
257+
);
258+
assert_eq!(
259+
parser.parse_peek("next sunday"),
260+
Ok(("", HumanDateExpr::NextWeekWeekday(Weekday::Sun)))
261+
);
262+
}
263+
264+
#[test]
265+
fn test_ordinal_weekday_of_month() {
266+
let mut parser = HumanDateParserEnglishParser::new();
267+
assert_eq!(
268+
parser.parse_peek("first sun of september"),
269+
Ok((
270+
"",
271+
HumanDateExpr::OrdinalWeekdayOfMonth(
272+
Ordinal::First,
273+
Weekday::Sun,
274+
Month::September
275+
)
276+
))
277+
);
278+
assert_eq!(
279+
parser.parse_peek("second thursday of september"),
280+
Ok((
281+
"",
282+
HumanDateExpr::OrdinalWeekdayOfMonth(
283+
Ordinal::Second,
284+
Weekday::Thu,
285+
Month::September
286+
)
287+
))
288+
);
289+
assert_eq!(
290+
parser.parse_peek("third sunday of september"),
291+
Ok((
292+
"",
293+
HumanDateExpr::OrdinalWeekdayOfMonth(
294+
Ordinal::Third,
295+
Weekday::Sun,
296+
Month::September
297+
)
298+
))
299+
);
300+
}
301+
302+
#[test]
303+
fn test_weekday() {
304+
assert_eq!(weekday.parse_peek("monday"), Ok(("", Weekday::Mon)));
305+
assert_eq!(weekday.parse_peek("mon"), Ok(("", Weekday::Mon)));
306+
assert_eq!(weekday.parse_peek("tuesday"), Ok(("", Weekday::Tue)));
307+
assert_eq!(weekday.parse_peek("tue"), Ok(("", Weekday::Tue)));
308+
assert_eq!(weekday.parse_peek("wednesday"), Ok(("", Weekday::Wed)));
309+
assert_eq!(weekday.parse_peek("wed"), Ok(("", Weekday::Wed)));
310+
assert_eq!(weekday.parse_peek("thursday"), Ok(("", Weekday::Thu)));
311+
assert_eq!(weekday.parse_peek("thu"), Ok(("", Weekday::Thu)));
312+
assert_eq!(weekday.parse_peek("friday"), Ok(("", Weekday::Fri)));
313+
assert_eq!(weekday.parse_peek("fri"), Ok(("", Weekday::Fri)));
314+
assert_eq!(weekday.parse_peek("saturday"), Ok(("", Weekday::Sat)));
315+
assert_eq!(weekday.parse_peek("sat"), Ok(("", Weekday::Sat)));
316+
assert_eq!(weekday.parse_peek("sunday"), Ok(("", Weekday::Sun)));
317+
assert_eq!(weekday.parse_peek("sun"), Ok(("", Weekday::Sun)));
318+
}
319+
320+
#[test]
321+
fn test_this() {
322+
assert_eq!(this.parse_peek("this"), Ok(("", ())));
323+
assert_eq!(this.parse_peek("the current"), Ok(("", ())));
324+
}
325+
326+
#[test]
327+
fn test_next() {
328+
assert_eq!(next.parse_peek("next"), Ok(("", ())));
329+
assert_eq!(next.parse_peek("the next"), Ok(("", ())));
330+
assert_eq!(next.parse_peek("the following"), Ok(("", ())));
331+
}
332+
333+
#[test]
334+
fn test_number() {
335+
assert_eq!(number(&mut "1"), Ok(1));
336+
assert_eq!(number(&mut "01"), Ok(1));
337+
assert_eq!(number(&mut "one"), Ok(1));
338+
assert_eq!(number(&mut "two"), Ok(2));
339+
assert_eq!(number(&mut "three"), Ok(3));
340+
assert_eq!(number(&mut "four"), Ok(4));
341+
assert_eq!(number(&mut "five"), Ok(5));
342+
assert_eq!(number(&mut "six"), Ok(6));
343+
assert_eq!(number(&mut "seven"), Ok(7));
344+
assert_eq!(number(&mut "eight"), Ok(8));
345+
assert_eq!(number(&mut "nine"), Ok(9));
346+
assert_eq!(number(&mut "ten"), Ok(10));
347+
assert_eq!(number(&mut "eleven"), Ok(11));
348+
assert_eq!(number(&mut "twelve"), Ok(12));
349+
assert_eq!(number(&mut "thirteen"), Ok(13));
350+
assert_eq!(number(&mut "fourteen"), Ok(14));
351+
assert_eq!(number(&mut "fifteen"), Ok(15));
352+
assert_eq!(number(&mut "sixteen"), Ok(16));
353+
assert_eq!(number(&mut "seventeen"), Ok(17));
354+
assert_eq!(number(&mut "eighteen"), Ok(18));
355+
assert_eq!(number(&mut "nineteen"), Ok(19));
356+
assert_eq!(number(&mut "twenty"), Ok(20));
357+
}
358+
}
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
1-
use pt_br::HumanDateParserBrazillianPortugueseParser;
21
use winnow::{error::ContextError, Parser};
32

43
use crate::HumanDateExpr;
54

5+
pub mod en;
66
pub mod pt_br;
77

8+
use en::HumanDateParserEnglishParser;
9+
use pt_br::HumanDateParserBrazillianPortugueseParser;
10+
811
pub enum Locale {
912
BrazilianPortuguese,
13+
English,
1014
}
1115

1216
impl Locale {
1317
pub fn parser(&self) -> Box<dyn Parser<&str, HumanDateExpr, ContextError>> {
1418
match self {
1519
Self::BrazilianPortuguese => Box::new(HumanDateParserBrazillianPortugueseParser::new()),
20+
Self::English => Box::new(HumanDateParserEnglishParser::new()),
1621
}
1722
}
1823
}

0 commit comments

Comments
 (0)