Skip to main content

temps_core/language/
german.rs

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