use chumsky::{error::Rich, prelude::*};
use crate::{
DayReference, DayTime, Direction, LanguageParser, Meridiem, RelativeTime, Result, StandardDate,
Time, TimeExpression, TimeUnit, Weekday, WeekdayModifier,
common::{
ParserError, TokenInput, digit_number, four_digit_number, iso_datetime, opt_space,
phrase_ci, phrases_ci, punct, space, token_stream, two_digit_number, word_ci,
},
error::rich_errors_to_temps_error,
lexer::lex,
time_utils,
};
pub struct EnglishParser;
fn number<'t, 's: 't, I>() -> impl Parser<'t, I, i64, ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
choice((
digit_number(),
phrases_ci([
("a", 1i64),
("an", 1),
("one", 1),
("two", 2),
("three", 3),
("four", 4),
("five", 5),
("six", 6),
("seven", 7),
("eight", 8),
("nine", 9),
("ten", 10),
("a couple", 2),
("a couple of", 2),
("couple of", 2),
("a few", 3),
("a dozen", 12),
]),
))
.labelled("number")
}
fn time_unit<'t, 's: 't, I>() -> impl Parser<'t, I, TimeUnit, ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
phrases_ci([
("second", TimeUnit::Second),
("seconds", TimeUnit::Second),
("sec", TimeUnit::Second),
("secs", TimeUnit::Second),
("s", TimeUnit::Second),
("minute", TimeUnit::Minute),
("minutes", TimeUnit::Minute),
("min", TimeUnit::Minute),
("mins", TimeUnit::Minute),
("m", TimeUnit::Minute),
("hour", TimeUnit::Hour),
("hours", TimeUnit::Hour),
("hr", TimeUnit::Hour),
("hrs", TimeUnit::Hour),
("h", TimeUnit::Hour),
("day", TimeUnit::Day),
("days", TimeUnit::Day),
("d", TimeUnit::Day),
("week", TimeUnit::Week),
("weeks", TimeUnit::Week),
("wk", TimeUnit::Week),
("wks", TimeUnit::Week),
("w", TimeUnit::Week),
("month", TimeUnit::Month),
("months", TimeUnit::Month),
("mo", TimeUnit::Month),
("mos", TimeUnit::Month),
("year", TimeUnit::Year),
("years", TimeUnit::Year),
("yr", TimeUnit::Year),
("yrs", TimeUnit::Year),
("y", TimeUnit::Year),
])
.labelled("time unit")
}
fn scaled_time_unit<'t, 's: 't, I>()
-> impl Parser<'t, I, (TimeUnit, i64), ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
choice((
phrases_ci([("fortnight", ()), ("fortnights", ())]).to((TimeUnit::Week, 2i64)),
time_unit().map(|unit| (unit, 1i64)),
))
.labelled("time unit")
}
fn amount_and_unit<'t, 's: 't, I>()
-> impl Parser<'t, I, (i64, TimeUnit), ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
number()
.then_ignore(space())
.then(scaled_time_unit())
.try_map(|(amount, (unit, per_unit)), span| {
amount
.checked_mul(per_unit)
.map(|amount| (amount, unit))
.ok_or_else(|| Rich::custom(span, "amount out of range"))
})
}
fn weekday<'t, 's: 't, I>() -> impl Parser<'t, I, Weekday, ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
phrases_ci([
("monday", Weekday::Monday),
("mon", Weekday::Monday),
("tuesday", Weekday::Tuesday),
("tue", Weekday::Tuesday),
("wednesday", Weekday::Wednesday),
("wed", Weekday::Wednesday),
("thursday", Weekday::Thursday),
("thu", Weekday::Thursday),
("friday", Weekday::Friday),
("fri", Weekday::Friday),
("saturday", Weekday::Saturday),
("sat", Weekday::Saturday),
("sunday", Weekday::Sunday),
("sun", Weekday::Sunday),
])
.labelled("weekday")
}
fn day_shortcuts<'t, 's: 't, I>() -> impl Parser<'t, I, DayReference, ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
phrases_ci([
("today", DayReference::Today),
("yesterday", DayReference::Yesterday),
("tomorrow", DayReference::Tomorrow),
("day after tomorrow", DayReference::DayAfterTomorrow),
("day before yesterday", DayReference::DayBeforeYesterday),
])
}
fn weekday_modifier<'t, 's: 't, I>()
-> impl Parser<'t, I, WeekdayModifier, ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
choice((
word_ci("last").to(WeekdayModifier::Last),
word_ci("next").to(WeekdayModifier::Next),
))
}
fn modified_weekday<'t, 's: 't, I>() -> impl Parser<'t, I, DayReference, ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
weekday_modifier()
.then_ignore(space())
.then(weekday())
.map(|(modifier, day)| DayReference::Weekday {
day,
modifier: Some(modifier),
})
}
fn simple_weekday<'t, 's: 't, I>() -> impl Parser<'t, I, DayReference, ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
weekday().map(|day| DayReference::Weekday {
day,
modifier: None,
})
}
fn day_reference<'t, 's: 't, I>() -> impl Parser<'t, I, DayReference, ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
choice((
the_day_reference(),
day_shortcuts(),
modified_weekday(),
this_weekday(),
weekend_ref(),
simple_weekday(),
))
}
fn meridiem<'t, 's: 't, I>() -> impl Parser<'t, I, Meridiem, ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
phrases_ci([
("am", Meridiem::AM),
("pm", Meridiem::PM),
("a.m.", Meridiem::AM),
("p.m.", Meridiem::PM),
])
.labelled("am/pm")
}
fn time_with_minutes<'t, 's: 't, I>()
-> impl Parser<'t, I, (u8, u8, u8, Option<Meridiem>), ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
two_digit_number()
.then_ignore(punct(':'))
.then(two_digit_number())
.then(punct(':').ignore_then(two_digit_number()).or_not())
.then(opt_space().ignore_then(meridiem()).or_not())
.try_map(|(((hour, minute), second), mer), span| {
let second = second.unwrap_or(0);
if time_utils::is_valid_time(hour, minute, second, mer) {
Ok((hour, minute, second, mer))
} else {
Err(Rich::custom(span, "invalid time"))
}
})
}
fn hour_meridiem<'t, 's: 't, I>()
-> impl Parser<'t, I, (u8, u8, u8, Option<Meridiem>), ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
two_digit_number()
.then(opt_space().ignore_then(meridiem()))
.try_map(|(hour, mer), span| {
if time_utils::is_valid_time(hour, 0, 0, Some(mer)) {
Ok((hour, 0, 0, Some(mer)))
} else {
Err(Rich::custom(span, "invalid time"))
}
})
}
fn time_digits<'t, 's: 't, I>()
-> impl Parser<'t, I, (u8, u8, u8, Option<Meridiem>), ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
choice((time_with_minutes(), hour_meridiem()))
}
fn raw_hour<'t, 's: 't, I>() -> impl Parser<'t, I, u8, ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
choice((
two_digit_number().try_map(|h, span| {
if h <= 23 {
Ok(h)
} else {
Err(Rich::custom(span, "hour must be 0-23"))
}
}),
word_ci("noon").to(12u8),
word_ci("midnight").to(0u8),
))
}
fn fractional_time<'t, 's: 't, I>()
-> impl Parser<'t, I, (u8, u8, u8, Option<Meridiem>), ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
let half_past = phrase_ci("half past")
.ignore_then(space())
.ignore_then(raw_hour())
.map(|h| (h, 30u8, 0u8, None::<Meridiem>));
let quarter_past = phrase_ci("quarter past")
.ignore_then(space())
.ignore_then(raw_hour())
.map(|h| (h, 15u8, 0u8, None::<Meridiem>));
let quarter_to = phrase_ci("quarter to")
.ignore_then(space())
.ignore_then(raw_hour())
.map(|h| {
if h == 0 {
(23u8, 45u8, 0u8, None::<Meridiem>)
} else {
(h - 1, 45u8, 0u8, None::<Meridiem>)
}
});
choice((half_past, quarter_past, quarter_to)).try_map(|(hour, minute, second, mer), span| {
if time_utils::is_valid_time(hour, minute, second, mer) {
Ok((hour, minute, second, mer))
} else {
Err(Rich::custom(span, "invalid time"))
}
})
}
fn time_expr<'t, 's: 't, I>() -> impl Parser<'t, I, TimeExpression, ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
choice((
fractional_time().map(|(hour, minute, second, meridiem)| {
TimeExpression::Time(Time {
hour,
minute,
second,
meridiem,
})
}),
time_digits().map(|(hour, minute, second, meridiem)| {
TimeExpression::Time(Time {
hour,
minute,
second,
meridiem,
})
}),
))
}
fn named_time<'t, 's: 't, I>() -> impl Parser<'t, I, TimeExpression, ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
choice((
word_ci("noon").to(TimeExpression::Time(Time {
hour: 12,
minute: 0,
second: 0,
meridiem: None,
})),
word_ci("midnight").to(TimeExpression::Time(Time {
hour: 0,
minute: 0,
second: 0,
meridiem: None,
})),
word_ci("teatime").to(TimeExpression::Time(Time {
hour: 16,
minute: 0,
second: 0,
meridiem: None,
})),
))
}
fn part_of_day<'t, 's: 't, I>() -> impl Parser<'t, I, Time, ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
choice((
word_ci("morning").to(Time {
hour: 8,
minute: 0,
second: 0,
meridiem: None,
}),
word_ci("afternoon").to(Time {
hour: 13,
minute: 0,
second: 0,
meridiem: None,
}),
word_ci("evening").to(Time {
hour: 18,
minute: 0,
second: 0,
meridiem: None,
}),
word_ci("night").to(Time {
hour: 20,
minute: 0,
second: 0,
meridiem: None,
}),
))
}
fn this_part_of_day<'t, 's: 't, I>()
-> impl Parser<'t, I, TimeExpression, ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
word_ci("this")
.ignore_then(space())
.ignore_then(part_of_day())
.map(|time| {
TimeExpression::DayTime(DayTime {
day: DayReference::Today,
time,
})
})
}
fn this_weekday<'t, 's: 't, I>() -> impl Parser<'t, I, DayReference, ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
word_ci("this")
.ignore_then(space())
.ignore_then(weekday())
.map(|day| DayReference::Weekday {
day,
modifier: None,
})
}
fn weekend_ref<'t, 's: 't, I>() -> impl Parser<'t, I, DayReference, ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
choice((
phrase_ci("this weekend").to(DayReference::Weekday {
day: Weekday::Saturday,
modifier: Some(WeekdayModifier::This),
}),
phrase_ci("next weekend").to(DayReference::Weekday {
day: Weekday::Saturday,
modifier: Some(WeekdayModifier::Next),
}),
))
}
fn standalone_daytime<'t, 's: 't, I>()
-> impl Parser<'t, I, TimeExpression, ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
choice((
word_ci("tonight").to(TimeExpression::DayTime(DayTime {
day: DayReference::Today,
time: Time {
hour: 20,
minute: 0,
second: 0,
meridiem: None,
},
})),
choice((
word_ci("eod"),
phrase_ci("end of day"),
phrase_ci("end of the day"),
))
.to(TimeExpression::DayTime(DayTime {
day: DayReference::Today,
time: Time {
hour: 17,
minute: 0,
second: 0,
meridiem: None,
},
})),
))
}
fn the_day_reference<'t, 's: 't, I>()
-> impl Parser<'t, I, DayReference, ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
word_ci("the").ignore_then(space()).ignore_then(phrases_ci([
("day after tomorrow", DayReference::DayAfterTomorrow),
("day before yesterday", DayReference::DayBeforeYesterday),
]))
}
fn fortnight<'t, 's: 't, I>() -> impl Parser<'t, I, TimeExpression, ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
word_ci("fortnight").to(TimeExpression::Relative(RelativeTime {
amount: 2,
unit: TimeUnit::Week,
direction: Direction::Future,
}))
}
fn later_expr<'t, 's: 't, I>() -> impl Parser<'t, I, TimeExpression, ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
word_ci("later")
.ignore_then(space().ignore_then(word_ci("today")).or_not())
.map(|today| match today {
Some(()) => TimeExpression::LaterToday,
None => TimeExpression::Relative(RelativeTime {
amount: 2,
unit: TimeUnit::Hour,
direction: Direction::Future,
}),
})
}
fn week_from_now<'t, 's: 't, I>() -> impl Parser<'t, I, TimeExpression, ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
choice((phrase_ci("a week from today"), phrase_ci("a week from now"))).to(
TimeExpression::Relative(RelativeTime {
amount: 1,
unit: TimeUnit::Week,
direction: Direction::Future,
}),
)
}
fn day_expr<'t, 's: 't, I>() -> impl Parser<'t, I, TimeExpression, ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
let at_time = word_ci("at")
.ignore_then(space())
.ignore_then(time_digits())
.map(|(hour, minute, second, meridiem)| Time {
hour,
minute,
second,
meridiem,
});
day_reference()
.then(
space()
.ignore_then(choice((at_time, part_of_day())))
.or_not(),
)
.map(|(day, time)| match time {
Some(time) => TimeExpression::DayTime(DayTime { day, time }),
None => TimeExpression::Day(day),
})
}
fn relative_past<'t, 's: 't, I>() -> impl Parser<'t, I, TimeExpression, ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
amount_and_unit()
.then_ignore(space())
.then_ignore(word_ci("ago"))
.map(|(amount, unit)| {
TimeExpression::Relative(RelativeTime {
amount,
unit,
direction: Direction::Past,
})
})
}
fn relative_future<'t, 's: 't, I>()
-> impl Parser<'t, I, TimeExpression, ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
word_ci("in")
.ignore_then(space())
.ignore_then(amount_and_unit())
.map(|(amount, unit)| {
TimeExpression::Relative(RelativeTime {
amount,
unit,
direction: Direction::Future,
})
})
}
fn now_expr<'t, 's: 't, I>() -> impl Parser<'t, I, TimeExpression, ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
word_ci("now").to(TimeExpression::Now)
}
fn date_format<'t, 's: 't, I>() -> impl Parser<'t, I, TimeExpression, ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
let separator = choice((punct('/').to('/'), punct('-').to('-')));
two_digit_number()
.then(separator.clone())
.then(two_digit_number())
.then(separator)
.then(four_digit_number())
.try_map(|((((day, first), month), second), year), span| {
if first == second && time_utils::is_valid_calendar_date(year, month, day) {
Ok(TimeExpression::Date(StandardDate { day, month, year }))
} else {
Err(Rich::custom(span, "invalid date"))
}
})
}
fn parser<'t, 's: 't, I>() -> impl Parser<'t, I, TimeExpression, ParserError<'t, 's>>
where
I: TokenInput<'t, 's>,
{
choice((
iso_datetime().labelled("ISO 8601 datetime"),
date_format().labelled("calendar date"),
day_expr().labelled("day, optionally with a time"),
now_expr().labelled("`now`"),
standalone_daytime().labelled("standalone (tonight, EOD)"),
this_part_of_day().labelled("this morning/afternoon/evening"),
named_time().labelled("named time (noon, midnight, teatime)"),
time_expr().labelled("time of day"),
relative_past().labelled("`<n> <unit> ago`"),
relative_future().labelled("`in <n> <unit>`"),
week_from_now().labelled("a week from now/today"),
fortnight().labelled("fortnight"),
later_expr().labelled("later/later today"),
))
.padded_by(opt_space())
.then_ignore(end())
}
impl LanguageParser for EnglishParser {
fn parse(&self, input: &str) -> Result<TimeExpression> {
let tokens = lex(input);
parser()
.parse(token_stream(input, &tokens))
.into_result()
.map_err(|errs| rich_errors_to_temps_error(input, errs))
}
}