use chumsky::{error::Rich, prelude::*};
use crate::{
DayReference, DayTime, Direction, LanguageParser, RelativeTime, Result, StandardDate, Time,
TimeExpression, TimeUnit, Weekday, WeekdayModifier,
common::{
ParserError, TokenInput, digit_number, four_digit_number, iso_datetime, opt_space,
phrases_ci, phrases_cs, punct, space, token_stream, two_digit_number, word_ci,
},
error::rich_errors_to_temps_error,
lexer::lex,
time_utils,
};
pub struct GermanParser;
fn number<'t, 's: 't, I>() -> impl Parser<'t, I, i64, ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
choice((
digit_number(),
phrases_cs([
("ein", 1i64),
("eine", 1),
("einem", 1),
("einen", 1),
("einer", 1),
("zwei", 2),
("drei", 3),
("vier", 4),
("fünf", 5),
("sechs", 6),
("sieben", 7),
("acht", 8),
("neun", 9),
("zehn", 10),
]),
))
.labelled("Zahl")
}
fn time_unit<'t, 's: 't, I>() -> impl Parser<'t, I, TimeUnit, ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
choice((
phrases_cs([
("Sekunde", TimeUnit::Second),
("Sekunden", TimeUnit::Second),
("Minute", TimeUnit::Minute),
("Minuten", TimeUnit::Minute),
("Stunde", TimeUnit::Hour),
("Stunden", TimeUnit::Hour),
("Tag", TimeUnit::Day),
("Tage", TimeUnit::Day),
("Tagen", TimeUnit::Day),
("Woche", TimeUnit::Week),
("Wochen", TimeUnit::Week),
("Monat", TimeUnit::Month),
("Monate", TimeUnit::Month),
("Monaten", TimeUnit::Month),
("Jahr", TimeUnit::Year),
("Jahre", TimeUnit::Year),
("Jahren", TimeUnit::Year),
]),
phrases_ci([
("sek", TimeUnit::Second),
("min", TimeUnit::Minute),
("std", TimeUnit::Hour),
]),
))
.labelled("Zeiteinheit")
}
fn weekday<'t, 's: 't, I>() -> impl Parser<'t, I, Weekday, ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
choice((
phrases_cs([
("Montag", Weekday::Monday),
("Dienstag", Weekday::Tuesday),
("Mittwoch", Weekday::Wednesday),
("Donnerstag", Weekday::Thursday),
("Freitag", Weekday::Friday),
("Samstag", Weekday::Saturday),
("Sonntag", Weekday::Sunday),
]),
phrases_ci([
("mo", Weekday::Monday),
("di", Weekday::Tuesday),
("mi", Weekday::Wednesday),
("do", Weekday::Thursday),
("fr", Weekday::Friday),
("sa", Weekday::Saturday),
("so", Weekday::Sunday),
]),
))
.labelled("Wochentag")
}
fn day_shortcuts<'t, 's: 't, I>() -> impl Parser<'t, I, DayReference, ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
phrases_ci([
("heute", DayReference::Today),
("gestern", DayReference::Yesterday),
("morgen", DayReference::Tomorrow),
("übermorgen", DayReference::DayAfterTomorrow),
("vorgestern", 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("letzte").to(WeekdayModifier::Last),
word_ci("letzten").to(WeekdayModifier::Last),
word_ci("nächste").to(WeekdayModifier::Next),
word_ci("nächsten").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((day_shortcuts(), modified_weekday(), simple_weekday()))
}
fn time_digits<'t, 's: 't, I>() -> impl Parser<'t, I, (u8, u8, u8), 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())
.try_map(|((hour, minute), second), span| {
let second = second.unwrap_or(0);
if time_utils::is_valid_24_hour_time(hour, minute, second) {
Ok((hour, minute, second))
} else {
Err(Rich::custom(span, "invalid time"))
}
})
}
fn uhr_suffix<'t, 's: 't, I>() -> impl Parser<'t, I, (), ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
space().ignore_then(word_ci("uhr")).or_not().ignored()
}
fn time_expr<'t, 's: 't, I>() -> impl Parser<'t, I, TimeExpression, ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
time_digits()
.then_ignore(uhr_suffix())
.map(|(hour, minute, second)| {
TimeExpression::Time(Time {
hour,
minute,
second,
meridiem: None,
})
})
}
fn day_expr<'t, 's: 't, I>() -> impl Parser<'t, I, TimeExpression, ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
let um_time = word_ci("um")
.ignore_then(space())
.ignore_then(time_digits())
.then_ignore(uhr_suffix());
day_reference()
.then(space().ignore_then(um_time).or_not())
.map(|(day, time)| match time {
Some((hour, minute, second)) => TimeExpression::DayTime(DayTime {
day,
time: Time {
hour,
minute,
second,
meridiem: None,
},
}),
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>,
{
word_ci("vor")
.ignore_then(space())
.ignore_then(number())
.then_ignore(space())
.then(time_unit())
.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(number())
.then_ignore(space())
.then(time_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("jetzt").to(TimeExpression::Now)
}
fn date_format<'t, 's: 't, I>() -> impl Parser<'t, I, TimeExpression, ParserError<'t, 's>> + Clone
where
I: TokenInput<'t, 's>,
{
two_digit_number()
.then_ignore(punct('.'))
.then(two_digit_number())
.then_ignore(punct('.'))
.then(four_digit_number())
.try_map(|((day, month), year), span| {
if time_utils::is_valid_calendar_date(year, month, day) {
Ok(TimeExpression::Date(StandardDate { day, month, year }))
} else {
Err(Rich::custom(span, "invalid calendar 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("Datum (TT.MM.JJJJ)"),
day_expr().labelled("Tagesangabe, optional mit Uhrzeit"),
now_expr().labelled("`jetzt`"),
time_expr().labelled("Uhrzeit"),
relative_past().labelled("`vor <n> <Einheit>`"),
relative_future().labelled("`in <n> <Einheit>`"),
))
.padded_by(opt_space())
.then_ignore(end())
}
impl LanguageParser for GermanParser {
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))
}
}