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