Skip to main content

whichtime_sys/parsers/de/
casual_date.rs

1//! German casual date parser
2//!
3//! Handles German casual date expressions like:
4//! - "jetzt" (now), "heute" (today), "morgen" (tomorrow)
5//! - "heute Morgen" (this morning), "gestern Abend" (yesterday evening)
6//! - "letzte Nacht" (last night)
7
8use crate::components::Component;
9use crate::context::ParsingContext;
10use crate::error::Result;
11use crate::parsers::Parser;
12use crate::results::ParsedResult;
13use crate::types::Meridiem;
14use chrono::{Datelike, Duration, Timelike};
15use fancy_regex::Regex;
16use std::sync::LazyLock;
17
18static PATTERN: LazyLock<Regex> = LazyLock::new(|| {
19    Regex::new(
20        r"(?i)(?<![a-zA-ZäöüÄÖÜß])(jetzt|heute|morgen|übermorgen|uebermorgen|gestern|vorgestern|letzte\s*nacht)(?:\s*(morgen|vormittag|mittags?|nachmittag|abend|nacht|mitternacht))?(?:\s+um\s+(\d{1,2})(?:\s*uhr)?)?(?=\W|$)"
21    ).unwrap()
22});
23
24const DATE_GROUP: usize = 1;
25const TIME_GROUP: usize = 2;
26const HOUR_GROUP: usize = 3;
27
28/// German casual date parser
29pub struct DECasualDateParser;
30
31impl DECasualDateParser {
32    pub fn new() -> Self {
33        Self
34    }
35
36    /// Extract time components from a time keyword
37    fn extract_time_components(
38        components: &mut crate::components::FastComponents,
39        time_keyword: &str,
40    ) {
41        match time_keyword {
42            "morgen" => {
43                components.imply(Component::Hour, 6);
44                components.imply(Component::Minute, 0);
45                components.imply(Component::Second, 0);
46                components.assign(Component::Meridiem, Meridiem::AM as i32);
47            }
48            "vormittag" => {
49                components.imply(Component::Hour, 9);
50                components.imply(Component::Minute, 0);
51                components.imply(Component::Second, 0);
52                components.assign(Component::Meridiem, Meridiem::AM as i32);
53            }
54            "mittag" | "mittags" => {
55                components.imply(Component::Hour, 12);
56                components.imply(Component::Minute, 0);
57                components.imply(Component::Second, 0);
58                components.assign(Component::Meridiem, Meridiem::PM as i32);
59            }
60            "nachmittag" => {
61                components.imply(Component::Hour, 15);
62                components.imply(Component::Minute, 0);
63                components.imply(Component::Second, 0);
64                components.assign(Component::Meridiem, Meridiem::PM as i32);
65            }
66            "abend" => {
67                components.imply(Component::Hour, 18);
68                components.imply(Component::Minute, 0);
69                components.imply(Component::Second, 0);
70                components.assign(Component::Meridiem, Meridiem::PM as i32);
71            }
72            "nacht" => {
73                components.imply(Component::Hour, 22);
74                components.imply(Component::Minute, 0);
75                components.imply(Component::Second, 0);
76                components.assign(Component::Meridiem, Meridiem::PM as i32);
77            }
78            "mitternacht" => {
79                // Midnight is typically the next day if not specified
80                components.imply(Component::Hour, 0);
81                components.imply(Component::Minute, 0);
82                components.imply(Component::Second, 0);
83            }
84            _ => {}
85        }
86    }
87}
88
89impl Parser for DECasualDateParser {
90    fn name(&self) -> &'static str {
91        "DECasualDateParser"
92    }
93
94    fn should_apply(&self, _context: &ParsingContext) -> bool {
95        // This parser can always be applied for German locale
96        true
97    }
98
99    fn parse(&self, context: &ParsingContext) -> Result<Vec<ParsedResult>> {
100        let mut results = Vec::new();
101        let ref_date = context.reference.instant;
102
103        // Use fancy_regex find_iter equivalent
104        let mut start = 0;
105        while start < context.text.len() {
106            let search_text = &context.text[start..];
107            let captures = match PATTERN.captures(search_text) {
108                Ok(Some(caps)) => caps,
109                Ok(None) => break,
110                Err(_) => break,
111            };
112
113            let full_match = match captures.get(0) {
114                Some(m) => m,
115                None => break,
116            };
117
118            let match_start = start + full_match.start();
119            let match_end = start + full_match.end();
120            let _matched_text = full_match.as_str();
121
122            let date_keyword = captures
123                .get(DATE_GROUP)
124                .map(|m| m.as_str().to_lowercase())
125                .unwrap_or_default();
126
127            let time_keyword = captures.get(TIME_GROUP).map(|m| m.as_str().to_lowercase());
128
129            let explicit_hour: Option<i32> = captures
130                .get(HOUR_GROUP)
131                .and_then(|m| m.as_str().parse().ok());
132
133            let mut components = context.create_components();
134            let mut target_date = ref_date;
135
136            match date_keyword.as_str() {
137                "jetzt" => {
138                    components.assign(Component::Year, ref_date.year());
139                    components.assign(Component::Month, ref_date.month() as i32);
140                    components.assign(Component::Day, ref_date.day() as i32);
141                    components.assign(Component::Hour, ref_date.hour() as i32);
142                    components.assign(Component::Minute, ref_date.minute() as i32);
143                    components.assign(Component::Second, ref_date.second() as i32);
144                }
145                "heute" => {
146                    components.assign(Component::Year, ref_date.year());
147                    components.assign(Component::Month, ref_date.month() as i32);
148                    components.assign(Component::Day, ref_date.day() as i32);
149                }
150                "morgen" => {
151                    target_date = ref_date + Duration::days(1);
152                    components.assign(Component::Year, target_date.year());
153                    components.assign(Component::Month, target_date.month() as i32);
154                    components.assign(Component::Day, target_date.day() as i32);
155                }
156                "übermorgen" | "uebermorgen" => {
157                    target_date = ref_date + Duration::days(2);
158                    components.assign(Component::Year, target_date.year());
159                    components.assign(Component::Month, target_date.month() as i32);
160                    components.assign(Component::Day, target_date.day() as i32);
161                }
162                "gestern" => {
163                    target_date = ref_date - Duration::days(1);
164                    components.assign(Component::Year, target_date.year());
165                    components.assign(Component::Month, target_date.month() as i32);
166                    components.assign(Component::Day, target_date.day() as i32);
167                }
168                "vorgestern" => {
169                    target_date = ref_date - Duration::days(2);
170                    components.assign(Component::Year, target_date.year());
171                    components.assign(Component::Month, target_date.month() as i32);
172                    components.assign(Component::Day, target_date.day() as i32);
173                }
174                _ if date_keyword.contains("letzte") && date_keyword.contains("nacht") => {
175                    // "letzte Nacht" - last night
176                    if ref_date.hour() > 6 {
177                        target_date = ref_date - Duration::days(1);
178                    }
179                    components.assign(Component::Year, target_date.year());
180                    components.assign(Component::Month, target_date.month() as i32);
181                    components.assign(Component::Day, target_date.day() as i32);
182                    components.imply(Component::Hour, 0);
183                }
184                _ => {}
185            }
186
187            // Apply time component if present
188            if let Some(ref time_kw) = time_keyword {
189                Self::extract_time_components(&mut components, time_kw);
190            }
191
192            // If explicit hour is given (e.g., "heute Abend um 8"), override the implied hour
193            if let Some(hour) = explicit_hour {
194                // Adjust hour based on time-of-day context
195                let adjusted_hour = if let Some(ref time_kw) = time_keyword {
196                    match time_kw.as_str() {
197                        "abend" | "nacht" | "nachmittag" => {
198                            // PM context: if hour < 12, add 12
199                            if hour < 12 { hour + 12 } else { hour }
200                        }
201                        "morgen" | "vormittag" => {
202                            // AM context: keep as is
203                            if hour == 12 { 0 } else { hour }
204                        }
205                        _ => hour,
206                    }
207                } else {
208                    hour
209                };
210                components.assign(Component::Hour, adjusted_hour);
211                components.assign(Component::Minute, 0);
212            }
213
214            results.push(context.create_result(match_start, match_end, components, None));
215
216            start = match_end;
217        }
218
219        Ok(results)
220    }
221}
222
223impl Default for DECasualDateParser {
224    fn default() -> Self {
225        Self::new()
226    }
227}