Skip to main content

temps_core/language/
german.rs

1use winnow::{
2    Parser,
3    ascii::{Caseless, multispace0, multispace1},
4    combinator::{alt, delimited, opt, preceded},
5};
6
7use crate::{
8    DayReference, DayTime, Direction, LanguageParser, RelativeTime, Result, StandardDate, Time,
9    TimeExpression, TimeUnit, Weekday, WeekdayModifier, common, error::ParseErrorExt, time_utils,
10};
11
12/// Parser for German natural language time expressions.
13///
14/// German nouns (e.g., "Sekunden", "Minuten") are matched case-sensitively
15/// to follow German orthographic rules, while abbreviations (e.g., "sek", "min")
16/// are matched case-insensitively for convenience.
17pub struct GermanParser;
18
19impl GermanParser {
20    fn parse_number(input: &mut &str) -> winnow::Result<i64> {
21        alt((
22            common::parse_digit_number,
23            alt((
24                "einem".value(1),
25                "einer".value(1),
26                "einen".value(1),
27                "eine".value(1),
28                "ein".value(1),
29            )),
30            alt((
31                "zwei".value(2),
32                "drei".value(3),
33                "vier".value(4),
34                "fünf".value(5),
35                "sechs".value(6),
36            )),
37            alt((
38                "sieben".value(7),
39                "acht".value(8),
40                "neun".value(9),
41                "zehn".value(10),
42            )),
43        ))
44        .parse_next(input)
45    }
46
47    fn parse_time_unit(input: &mut &str) -> winnow::Result<TimeUnit> {
48        alt((
49            alt((
50                "Sekunden".value(TimeUnit::Second),
51                "Sekunde".value(TimeUnit::Second),
52                Caseless("sek").value(TimeUnit::Second), // Abbreviations can be case-insensitive
53            )),
54            alt((
55                "Minuten".value(TimeUnit::Minute),
56                "Minute".value(TimeUnit::Minute),
57                Caseless("min").value(TimeUnit::Minute), // Abbreviations can be case-insensitive
58            )),
59            alt((
60                "Stunden".value(TimeUnit::Hour),
61                "Stunde".value(TimeUnit::Hour),
62                Caseless("std").value(TimeUnit::Hour), // Abbreviations can be case-insensitive
63            )),
64            alt((
65                "Tagen".value(TimeUnit::Day),
66                "Tage".value(TimeUnit::Day),
67                "Tag".value(TimeUnit::Day),
68            )),
69            alt((
70                "Wochen".value(TimeUnit::Week),
71                "Woche".value(TimeUnit::Week),
72            )),
73            alt((
74                "Monaten".value(TimeUnit::Month),
75                "Monate".value(TimeUnit::Month),
76                "Monat".value(TimeUnit::Month),
77            )),
78            alt((
79                "Jahren".value(TimeUnit::Year),
80                "Jahre".value(TimeUnit::Year),
81                "Jahr".value(TimeUnit::Year),
82            )),
83        ))
84        .parse_next(input)
85    }
86
87    fn parse_relative_past(input: &mut &str) -> winnow::Result<TimeExpression> {
88        preceded(
89            Caseless("vor"),
90            preceded(
91                multispace1,
92                (Self::parse_number, multispace1, Self::parse_time_unit),
93            ),
94        )
95        .map(|(amount, _, unit)| {
96            TimeExpression::Relative(RelativeTime {
97                amount,
98                unit,
99                direction: Direction::Past,
100            })
101        })
102        .parse_next(input)
103    }
104
105    fn parse_relative_future(input: &mut &str) -> winnow::Result<TimeExpression> {
106        preceded(
107            Caseless("in"),
108            preceded(
109                multispace1,
110                (Self::parse_number, multispace1, Self::parse_time_unit),
111            ),
112        )
113        .map(|(amount, _, unit)| {
114            TimeExpression::Relative(RelativeTime {
115                amount,
116                unit,
117                direction: Direction::Future,
118            })
119        })
120        .parse_next(input)
121    }
122
123    fn parse_now(input: &mut &str) -> winnow::Result<TimeExpression> {
124        Caseless("jetzt")
125            .value(TimeExpression::Now)
126            .parse_next(input)
127    }
128
129    fn parse_iso_datetime(input: &mut &str) -> winnow::Result<TimeExpression> {
130        common::parse_iso_datetime(input)
131    }
132
133    fn parse_weekday(input: &mut &str) -> winnow::Result<Weekday> {
134        alt((
135            alt((
136                "Montag".value(Weekday::Monday),
137                Caseless("mo").value(Weekday::Monday), // Abbreviations can be case-insensitive
138            )),
139            alt((
140                "Dienstag".value(Weekday::Tuesday),
141                Caseless("di").value(Weekday::Tuesday), // Abbreviations can be case-insensitive
142            )),
143            alt((
144                "Mittwoch".value(Weekday::Wednesday),
145                Caseless("mi").value(Weekday::Wednesday), // Abbreviations can be case-insensitive
146            )),
147            alt((
148                "Donnerstag".value(Weekday::Thursday),
149                Caseless("do").value(Weekday::Thursday), // Abbreviations can be case-insensitive
150            )),
151            alt((
152                "Freitag".value(Weekday::Friday),
153                Caseless("fr").value(Weekday::Friday), // Abbreviations can be case-insensitive
154            )),
155            alt((
156                "Samstag".value(Weekday::Saturday),
157                Caseless("sa").value(Weekday::Saturday), // Abbreviations can be case-insensitive
158            )),
159            alt((
160                "Sonntag".value(Weekday::Sunday),
161                Caseless("so").value(Weekday::Sunday), // Abbreviations can be case-insensitive
162            )),
163        ))
164        .parse_next(input)
165    }
166
167    fn parse_day_shortcuts(input: &mut &str) -> winnow::Result<DayReference> {
168        alt((
169            Caseless("heute").value(DayReference::Today),
170            Caseless("gestern").value(DayReference::Yesterday),
171            Caseless("morgen").value(DayReference::Tomorrow),
172        ))
173        .parse_next(input)
174    }
175
176    fn parse_weekday_modifier(input: &mut &str) -> winnow::Result<WeekdayModifier> {
177        alt((
178            alt((
179                "letzten".value(WeekdayModifier::Last),
180                "letzte".value(WeekdayModifier::Last),
181            )),
182            alt((
183                "nächsten".value(WeekdayModifier::Next),
184                "nächste".value(WeekdayModifier::Next),
185            )),
186        ))
187        .parse_next(input)
188    }
189
190    fn parse_modified_weekday(input: &mut &str) -> winnow::Result<DayReference> {
191        (
192            Self::parse_weekday_modifier,
193            multispace1,
194            Self::parse_weekday,
195        )
196            .map(|(modifier, _, day)| DayReference::Weekday {
197                day,
198                modifier: Some(modifier),
199            })
200            .parse_next(input)
201    }
202
203    fn parse_simple_weekday(input: &mut &str) -> winnow::Result<DayReference> {
204        Self::parse_weekday
205            .map(|day| DayReference::Weekday {
206                day,
207                modifier: None,
208            })
209            .parse_next(input)
210    }
211
212    fn parse_day_reference(input: &mut &str) -> winnow::Result<TimeExpression> {
213        alt((
214            Self::parse_day_shortcuts,
215            Self::parse_modified_weekday,
216            Self::parse_simple_weekday,
217        ))
218        .map(TimeExpression::Day)
219        .parse_next(input)
220    }
221
222    fn parse_time_digits(input: &mut &str) -> winnow::Result<(u8, u8, u8)> {
223        (
224            common::parse_two_digit_number,
225            ':',
226            common::parse_two_digit_number,
227            opt(preceded(':', common::parse_two_digit_number)).map(|second| second.unwrap_or(0)),
228        )
229            .verify_map(|(hour, _, minute, second)| {
230                time_utils::is_valid_24_hour_time(hour, minute, second)
231                    .then_some((hour, minute, second))
232            })
233            .parse_next(input)
234    }
235
236    fn parse_time(input: &mut &str) -> winnow::Result<TimeExpression> {
237        (
238            Self::parse_time_digits,
239            opt(preceded(multispace1, Caseless("uhr"))),
240        )
241            .map(|((hour, minute, second), _)| {
242                TimeExpression::Time(Time {
243                    hour,
244                    minute,
245                    second,
246                    meridiem: None, // German typically uses 24-hour format
247                })
248            })
249            .parse_next(input)
250    }
251
252    fn parse_day_at_time(input: &mut &str) -> winnow::Result<TimeExpression> {
253        (
254            alt((
255                Self::parse_day_shortcuts,
256                Self::parse_modified_weekday,
257                Self::parse_simple_weekday,
258            )),
259            multispace1,
260            Caseless("um"),
261            multispace1,
262            Self::parse_time_digits,
263            opt(preceded(multispace1, Caseless("uhr"))),
264        )
265            .map(|(day, _, _, _, time_digits, _)| {
266                TimeExpression::DayTime(DayTime {
267                    day,
268                    time: Time {
269                        hour: time_digits.0,
270                        minute: time_digits.1,
271                        second: time_digits.2,
272                        meridiem: None,
273                    },
274                })
275            })
276            .parse_next(input)
277    }
278
279    fn parse_date_format(input: &mut &str) -> winnow::Result<TimeExpression> {
280        // DD.MM.YYYY (German format)
281        (
282            common::parse_two_digit_number,
283            '.',
284            common::parse_two_digit_number,
285            '.',
286            common::parse_four_digit_number,
287        )
288            .verify_map(|(day, _, month, _, year)| {
289                time_utils::is_valid_calendar_date(year, month, day)
290                    .then_some(TimeExpression::Date(StandardDate { day, month, year }))
291            })
292            .parse_next(input)
293    }
294}
295
296impl LanguageParser for GermanParser {
297    fn parse(&self, input: &str) -> Result<TimeExpression> {
298        delimited(
299            multispace0,
300            alt((
301                Self::parse_iso_datetime,
302                Self::parse_date_format,
303                Self::parse_day_at_time,
304                Self::parse_now,
305                Self::parse_day_reference,
306                Self::parse_time,
307                Self::parse_relative_past,
308                Self::parse_relative_future,
309            )),
310            multispace0,
311        )
312        .parse(input)
313        .map_err(|e| e.to_temps_error(input))
314    }
315}