1use chumsky::{error::Rich, prelude::*};
2
3use crate::{
4 DayReference, DayTime, Direction, LanguageParser, RelativeTime, Result, StandardDate, Time,
5 TimeExpression, TimeUnit, Weekday, WeekdayModifier,
6 common::{
7 ParserError, TokenInput, digit_number, four_digit_number, iso_datetime, opt_space,
8 phrases_ci, phrases_cs, punct, space, token_stream, two_digit_number, word_ci,
9 },
10 error::rich_errors_to_temps_error,
11 lexer::lex,
12 time_utils,
13};
14
15pub struct GermanParser;
21
22fn number<'t, 's: 't, I>() -> impl Parser<'t, I, i64, ParserError<'t, 's>> + Clone
23where
24 I: TokenInput<'t, 's>,
25{
26 choice((
27 digit_number(),
28 phrases_cs([
29 ("ein", 1i64),
30 ("eine", 1),
31 ("einem", 1),
32 ("einen", 1),
33 ("einer", 1),
34 ("zwei", 2),
35 ("drei", 3),
36 ("vier", 4),
37 ("fünf", 5),
38 ("sechs", 6),
39 ("sieben", 7),
40 ("acht", 8),
41 ("neun", 9),
42 ("zehn", 10),
43 ]),
44 ))
45 .labelled("Zahl")
46}
47
48fn time_unit<'t, 's: 't, I>() -> impl Parser<'t, I, TimeUnit, ParserError<'t, 's>> + Clone
49where
50 I: TokenInput<'t, 's>,
51{
52 choice((
53 phrases_cs([
55 ("Sekunde", TimeUnit::Second),
56 ("Sekunden", TimeUnit::Second),
57 ("Minute", TimeUnit::Minute),
58 ("Minuten", TimeUnit::Minute),
59 ("Stunde", TimeUnit::Hour),
60 ("Stunden", TimeUnit::Hour),
61 ("Tag", TimeUnit::Day),
62 ("Tage", TimeUnit::Day),
63 ("Tagen", TimeUnit::Day),
64 ("Woche", TimeUnit::Week),
65 ("Wochen", TimeUnit::Week),
66 ("Monat", TimeUnit::Month),
67 ("Monate", TimeUnit::Month),
68 ("Monaten", TimeUnit::Month),
69 ("Jahr", TimeUnit::Year),
70 ("Jahre", TimeUnit::Year),
71 ("Jahren", TimeUnit::Year),
72 ]),
73 phrases_ci([
74 ("sek", TimeUnit::Second),
75 ("min", TimeUnit::Minute),
76 ("std", TimeUnit::Hour),
77 ]),
78 ))
79 .labelled("Zeiteinheit")
80}
81
82fn weekday<'t, 's: 't, I>() -> impl Parser<'t, I, Weekday, ParserError<'t, 's>> + Clone
83where
84 I: TokenInput<'t, 's>,
85{
86 choice((
87 phrases_cs([
88 ("Montag", Weekday::Monday),
89 ("Dienstag", Weekday::Tuesday),
90 ("Mittwoch", Weekday::Wednesday),
91 ("Donnerstag", Weekday::Thursday),
92 ("Freitag", Weekday::Friday),
93 ("Samstag", Weekday::Saturday),
94 ("Sonntag", Weekday::Sunday),
95 ]),
96 phrases_ci([
97 ("mo", Weekday::Monday),
98 ("di", Weekday::Tuesday),
99 ("mi", Weekday::Wednesday),
100 ("do", Weekday::Thursday),
101 ("fr", Weekday::Friday),
102 ("sa", Weekday::Saturday),
103 ("so", Weekday::Sunday),
104 ]),
105 ))
106 .labelled("Wochentag")
107}
108
109fn day_shortcuts<'t, 's: 't, I>() -> impl Parser<'t, I, DayReference, ParserError<'t, 's>> + Clone
110where
111 I: TokenInput<'t, 's>,
112{
113 phrases_ci([
122 ("heute", DayReference::Today),
123 ("gestern", DayReference::Yesterday),
124 ("morgen", DayReference::Tomorrow),
125 ("übermorgen", DayReference::DayAfterTomorrow),
126 ("vorgestern", DayReference::DayBeforeYesterday),
127 ])
128}
129
130fn weekday_modifier<'t, 's: 't, I>()
131-> impl Parser<'t, I, WeekdayModifier, ParserError<'t, 's>> + Clone
132where
133 I: TokenInput<'t, 's>,
134{
135 choice((
136 word_ci("letzte").to(WeekdayModifier::Last),
137 word_ci("letzten").to(WeekdayModifier::Last),
138 word_ci("nächste").to(WeekdayModifier::Next),
139 word_ci("nächsten").to(WeekdayModifier::Next),
140 ))
141}
142
143fn modified_weekday<'t, 's: 't, I>() -> impl Parser<'t, I, DayReference, ParserError<'t, 's>> + Clone
144where
145 I: TokenInput<'t, 's>,
146{
147 weekday_modifier()
148 .then_ignore(space())
149 .then(weekday())
150 .map(|(modifier, day)| DayReference::Weekday {
151 day,
152 modifier: Some(modifier),
153 })
154}
155
156fn simple_weekday<'t, 's: 't, I>() -> impl Parser<'t, I, DayReference, ParserError<'t, 's>> + Clone
157where
158 I: TokenInput<'t, 's>,
159{
160 weekday().map(|day| DayReference::Weekday {
161 day,
162 modifier: None,
163 })
164}
165
166fn day_reference<'t, 's: 't, I>() -> impl Parser<'t, I, DayReference, ParserError<'t, 's>> + Clone
167where
168 I: TokenInput<'t, 's>,
169{
170 choice((day_shortcuts(), modified_weekday(), simple_weekday()))
174}
175
176fn time_digits<'t, 's: 't, I>() -> impl Parser<'t, I, (u8, u8, u8), ParserError<'t, 's>> + Clone
177where
178 I: TokenInput<'t, 's>,
179{
180 two_digit_number()
181 .then_ignore(punct(':'))
182 .then(two_digit_number())
183 .then(punct(':').ignore_then(two_digit_number()).or_not())
184 .try_map(|((hour, minute), second), span| {
185 let second = second.unwrap_or(0);
186 if time_utils::is_valid_24_hour_time(hour, minute, second) {
187 Ok((hour, minute, second))
188 } else {
189 Err(Rich::custom(span, "invalid time"))
190 }
191 })
192}
193
194fn uhr_suffix<'t, 's: 't, I>() -> impl Parser<'t, I, (), ParserError<'t, 's>> + Clone
196where
197 I: TokenInput<'t, 's>,
198{
199 space().ignore_then(word_ci("uhr")).or_not().ignored()
200}
201
202fn time_expr<'t, 's: 't, I>() -> impl Parser<'t, I, TimeExpression, ParserError<'t, 's>> + Clone
203where
204 I: TokenInput<'t, 's>,
205{
206 time_digits()
207 .then_ignore(uhr_suffix())
208 .map(|(hour, minute, second)| {
209 TimeExpression::Time(Time {
210 hour,
211 minute,
212 second,
213 meridiem: None,
214 })
215 })
216}
217
218fn day_expr<'t, 's: 't, I>() -> impl Parser<'t, I, TimeExpression, ParserError<'t, 's>> + Clone
225where
226 I: TokenInput<'t, 's>,
227{
228 let um_time = word_ci("um")
229 .ignore_then(space())
230 .ignore_then(time_digits())
231 .then_ignore(uhr_suffix());
232
233 day_reference()
234 .then(space().ignore_then(um_time).or_not())
235 .map(|(day, time)| match time {
236 Some((hour, minute, second)) => TimeExpression::DayTime(DayTime {
237 day,
238 time: Time {
239 hour,
240 minute,
241 second,
242 meridiem: None,
243 },
244 }),
245 None => TimeExpression::Day(day),
246 })
247}
248
249fn relative_past<'t, 's: 't, I>() -> impl Parser<'t, I, TimeExpression, ParserError<'t, 's>> + Clone
250where
251 I: TokenInput<'t, 's>,
252{
253 word_ci("vor")
254 .ignore_then(space())
255 .ignore_then(number())
256 .then_ignore(space())
257 .then(time_unit())
258 .map(|(amount, unit)| {
259 TimeExpression::Relative(RelativeTime {
260 amount,
261 unit,
262 direction: Direction::Past,
263 })
264 })
265}
266
267fn relative_future<'t, 's: 't, I>()
268-> impl Parser<'t, I, TimeExpression, ParserError<'t, 's>> + Clone
269where
270 I: TokenInput<'t, 's>,
271{
272 word_ci("in")
273 .ignore_then(space())
274 .ignore_then(number())
275 .then_ignore(space())
276 .then(time_unit())
277 .map(|(amount, unit)| {
278 TimeExpression::Relative(RelativeTime {
279 amount,
280 unit,
281 direction: Direction::Future,
282 })
283 })
284}
285
286fn now_expr<'t, 's: 't, I>() -> impl Parser<'t, I, TimeExpression, ParserError<'t, 's>> + Clone
287where
288 I: TokenInput<'t, 's>,
289{
290 word_ci("jetzt").to(TimeExpression::Now)
291}
292
293fn date_format<'t, 's: 't, I>() -> impl Parser<'t, I, TimeExpression, ParserError<'t, 's>> + Clone
294where
295 I: TokenInput<'t, 's>,
296{
297 two_digit_number()
298 .then_ignore(punct('.'))
299 .then(two_digit_number())
300 .then_ignore(punct('.'))
301 .then(four_digit_number())
302 .try_map(|((day, month), year), span| {
303 if time_utils::is_valid_calendar_date(year, month, day) {
304 Ok(TimeExpression::Date(StandardDate { day, month, year }))
305 } else {
306 Err(Rich::custom(span, "invalid calendar date"))
307 }
308 })
309}
310
311fn parser<'t, 's: 't, I>() -> impl Parser<'t, I, TimeExpression, ParserError<'t, 's>>
312where
313 I: TokenInput<'t, 's>,
314{
315 choice((
321 iso_datetime().labelled("ISO 8601 datetime"),
322 date_format().labelled("Datum (TT.MM.JJJJ)"),
323 day_expr().labelled("Tagesangabe, optional mit Uhrzeit"),
324 now_expr().labelled("`jetzt`"),
325 time_expr().labelled("Uhrzeit"),
326 relative_past().labelled("`vor <n> <Einheit>`"),
327 relative_future().labelled("`in <n> <Einheit>`"),
328 ))
329 .padded_by(opt_space())
330 .then_ignore(end())
331}
332
333impl LanguageParser for GermanParser {
334 fn parse(&self, input: &str) -> Result<TimeExpression> {
335 let tokens = lex(input);
336 parser()
337 .parse(token_stream(input, &tokens))
338 .into_result()
339 .map_err(|errs| rich_errors_to_temps_error(input, errs))
340 }
341}