Skip to main content

whichtime_sys/parsers/nl/
casual_date.rs

1//! Dutch casual date parser
2//!
3//! Handles Dutch casual date expressions like:
4//! - "vandaag", "morgen", "gisteren"
5//! - "vanavond", "vanochtend"
6//! - "morgenavond", "gisterenmiddag"
7//! - "deze avond"
8
9use crate::components::Component;
10use crate::context::ParsingContext;
11use crate::error::Result;
12use crate::parsers::Parser;
13use crate::results::ParsedResult;
14use crate::types::Meridiem;
15use chrono::{Datelike, Duration, Timelike};
16use fancy_regex::Regex;
17use std::sync::LazyLock;
18
19static PATTERN: LazyLock<Regex> = LazyLock::new(|| {
20    Regex::new(
21        r"(?i)(?<![a-zA-Z])(nu|vandaag|overmorgen|eergisteren|van(?:ochtend|middag|avond|nacht)|morgen(?:ochtend|middag|avond|nacht)|gisteren(?:ochtend|middag|avond|nacht)|deze\s+(?:ochtend|middag|namiddag|avond|nacht)|morgen|gisteren)(?:\s+(?:om\s+)?(\d{1,2})(?::(\d{1,2}))?(?:\s*uhr|\s*uur)?)?(?=\W|$)"
22    ).unwrap()
23});
24
25const DATE_GROUP: usize = 1;
26const HOUR_GROUP: usize = 2;
27const MINUTE_GROUP: usize = 3;
28
29/// Dutch casual date parser
30pub struct NLCasualDateParser;
31
32impl NLCasualDateParser {
33    pub fn new() -> Self {
34        Self
35    }
36
37    fn assign_time(components: &mut crate::components::FastComponents, time_part: &str) {
38        match time_part {
39            "ochtend" | "vanochtend" => {
40                components.imply(Component::Hour, 6);
41                components.imply(Component::Minute, 0);
42                components.imply(Component::Second, 0);
43                components.assign(Component::Meridiem, Meridiem::AM as i32);
44            }
45            "middag" | "vanmiddag" => {
46                components.imply(Component::Hour, 12); // Noon
47                components.imply(Component::Minute, 0);
48                components.imply(Component::Second, 0);
49                components.assign(Component::Meridiem, Meridiem::PM as i32);
50            }
51            "namiddag" => {
52                components.imply(Component::Hour, 15); // Afternoon
53                components.imply(Component::Minute, 0);
54                components.imply(Component::Second, 0);
55                components.assign(Component::Meridiem, Meridiem::PM as i32);
56            }
57            "avond" | "vanavond" => {
58                components.imply(Component::Hour, 20);
59                components.imply(Component::Minute, 0);
60                components.imply(Component::Second, 0);
61                components.assign(Component::Meridiem, Meridiem::PM as i32);
62            }
63            "nacht" | "vannacht" => {
64                components.imply(Component::Hour, 22);
65                components.imply(Component::Minute, 0);
66                components.imply(Component::Second, 0);
67                components.assign(Component::Meridiem, Meridiem::PM as i32);
68            }
69            _ => {}
70        }
71    }
72}
73
74impl Parser for NLCasualDateParser {
75    fn name(&self) -> &'static str {
76        "NLCasualDateParser"
77    }
78
79    fn should_apply(&self, _context: &ParsingContext) -> bool {
80        true
81    }
82
83    fn parse(&self, context: &ParsingContext) -> Result<Vec<ParsedResult>> {
84        let mut results = Vec::new();
85        let ref_date = context.reference.instant;
86
87        let mut start = 0;
88        while start < context.text.len() {
89            let search_text = &context.text[start..];
90            let captures = match PATTERN.captures(search_text) {
91                Ok(Some(caps)) => caps,
92                Ok(None) => break,
93                Err(_) => break,
94            };
95
96            let full_match = match captures.get(0) {
97                Some(m) => m,
98                None => break,
99            };
100
101            let match_start = start + full_match.start();
102            let match_end = start + full_match.end();
103
104            let date_keyword = captures
105                .get(DATE_GROUP)
106                .map(|m| m.as_str().to_lowercase())
107                .unwrap_or_default();
108
109            let explicit_hour: Option<i32> = captures
110                .get(HOUR_GROUP)
111                .and_then(|m| m.as_str().parse().ok());
112
113            let explicit_minute: Option<i32> = captures
114                .get(MINUTE_GROUP)
115                .and_then(|m| m.as_str().parse().ok());
116
117            let mut components = context.create_components();
118            let mut target_date = ref_date;
119
120            // Logic to determine target date and time part
121            let (day_offset, time_part) = if date_keyword == "nu" || date_keyword == "vandaag" {
122                (0, None)
123            } else if date_keyword == "morgen" {
124                (1, None)
125            } else if date_keyword == "overmorgen" {
126                (2, None)
127            } else if date_keyword == "gisteren" {
128                (-1, None)
129            } else if date_keyword == "eergisteren" {
130                (-2, None)
131            } else if let Some(time_part) = date_keyword.strip_prefix("van") {
132                // vanochtend, vanmiddag, vanavond, vannacht -> Today
133                let part = if date_keyword == "vannacht" {
134                    "nacht"
135                } else {
136                    time_part
137                };
138                (0, Some(part.to_string()))
139            } else if let Some(time_part) = date_keyword.strip_prefix("morgen")
140                && date_keyword.len() > 6
141            {
142                // morgenochtend etc.
143                (1, Some(time_part.to_string()))
144            } else if let Some(time_part) = date_keyword.strip_prefix("gisteren")
145                && date_keyword.len() > 8
146            {
147                // gisterenochtend etc.
148                (-1, Some(time_part.to_string()))
149            } else if let Some(time_part) = date_keyword.strip_prefix("deze ") {
150                // deze avond
151                (0, Some(time_part.to_string()))
152            } else {
153                (0, None)
154            };
155
156            // Adjust day
157            if day_offset != 0 {
158                target_date = ref_date + Duration::days(day_offset);
159            }
160
161            components.assign(Component::Year, target_date.year());
162            components.assign(Component::Month, target_date.month() as i32);
163            components.assign(Component::Day, target_date.day() as i32);
164
165            if date_keyword == "nu" {
166                components.assign(Component::Hour, ref_date.hour() as i32);
167                components.assign(Component::Minute, ref_date.minute() as i32);
168                components.assign(Component::Second, ref_date.second() as i32);
169            }
170
171            // Apply implied time
172            if let Some(ref tp) = time_part {
173                Self::assign_time(&mut components, tp);
174            }
175
176            // Handle explicit hour override
177            if let Some(hour) = explicit_hour {
178                // If time part implies PM (avond, etc) and hour < 12, make it PM
179                let adjusted_hour =
180                    if let Some(ref tp) = time_part {
181                        match tp.as_str() {
182                            "avond" | "nacht" | "namiddag" | "vanavond" | "vannacht" => {
183                                if hour < 12 { hour + 12 } else { hour }
184                            }
185                            "ochtend" | "vanochtend" => {
186                                if hour == 12 {
187                                    0
188                                } else {
189                                    hour
190                                }
191                            }
192                            _ => hour,
193                        }
194                    } else {
195                        hour
196                    };
197                components.assign(Component::Hour, adjusted_hour);
198                components.assign(Component::Minute, explicit_minute.unwrap_or(0));
199            }
200
201            results.push(context.create_result(match_start, match_end, components, None));
202
203            start = match_end;
204        }
205
206        Ok(results)
207    }
208}
209
210impl Default for NLCasualDateParser {
211    fn default() -> Self {
212        Self::new()
213    }
214}