Skip to main content

whichtime_sys/parsers/es/
casual_date.rs

1//! Spanish casual date parser
2//!
3//! Handles Spanish casual date expressions like:
4//! - "ahora" (now), "hoy" (today), "mañana" (tomorrow), "ayer" (yesterday)
5//! - "esta mañana" (this morning), "ayer de noche" (last night)
6//! - "pasado mañana" (day after tomorrow), "anteayer" (day before yesterday)
7//! - Combined: "hoy a las 5PM", "esta noche a las 8", "mañana a mediodía"
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áéíóúüñÁÉÍÓÚÜÑ])(ahora|hoy|esta\s+mañana|esta\s+manana|esta\s+tarde|esta\s+noche|mañana|manana|ayer|pasado\s*mañana|pasado\s*manana|anteayer|antes\s*de\s*ayer|anoche)(?:\s+(?:de\s+)?(la\s+)?(mañana|manana|tarde|noche))?(?:\s+a(?:\s+las?)?\s+(\d{1,2})(?::(\d{2}))?(?:\s*(a\.?m\.?|p\.?m\.?))?|\s+a\s+(mediodía|mediodia))?(?=\W|$)"
22    ).unwrap()
23});
24
25const DATE_GROUP: usize = 1;
26const TIME_GROUP: usize = 3;
27const HOUR_GROUP: usize = 4;
28const MINUTE_GROUP: usize = 5;
29const MERIDIEM_GROUP: usize = 6;
30const MEDIODIA_GROUP: usize = 7;
31
32/// Spanish casual date parser
33pub struct ESCasualDateParser;
34
35impl ESCasualDateParser {
36    pub fn new() -> Self {
37        Self
38    }
39
40    fn extract_time_components(
41        components: &mut crate::components::FastComponents,
42        time_keyword: &str,
43    ) {
44        let lower = time_keyword.to_lowercase();
45        if lower.contains("mañana") || lower.contains("manana") {
46            // "mañana" as time of day = morning (not tomorrow)
47            components.imply(Component::Hour, 6);
48            components.imply(Component::Minute, 0);
49            components.assign(Component::Meridiem, Meridiem::AM as i32);
50        } else if lower.contains("tarde") {
51            components.imply(Component::Hour, 15);
52            components.imply(Component::Minute, 0);
53            components.assign(Component::Meridiem, Meridiem::PM as i32);
54        } else if lower.contains("noche") {
55            components.imply(Component::Hour, 22);
56            components.imply(Component::Minute, 0);
57            components.assign(Component::Meridiem, Meridiem::PM as i32);
58        }
59    }
60}
61
62impl Parser for ESCasualDateParser {
63    fn name(&self) -> &'static str {
64        "ESCasualDateParser"
65    }
66
67    fn should_apply(&self, _context: &ParsingContext) -> bool {
68        true
69    }
70
71    fn parse(&self, context: &ParsingContext) -> Result<Vec<ParsedResult>> {
72        let mut results = Vec::new();
73        let ref_date = context.reference.instant;
74
75        let mut start = 0;
76        while start < context.text.len() {
77            let search_text = &context.text[start..];
78            let captures = match PATTERN.captures(search_text) {
79                Ok(Some(caps)) => caps,
80                Ok(None) => break,
81                Err(_) => break,
82            };
83
84            let full_match = match captures.get(0) {
85                Some(m) => m,
86                None => break,
87            };
88
89            let match_start = start + full_match.start();
90            let match_end = start + full_match.end();
91
92            let date_keyword = captures
93                .get(DATE_GROUP)
94                .map(|m| m.as_str().to_lowercase())
95                .unwrap_or_default();
96
97            let time_keyword = captures.get(TIME_GROUP).map(|m| m.as_str().to_lowercase());
98
99            let mut components = context.create_components();
100            let mut target_date = ref_date;
101
102            match date_keyword.as_str() {
103                "ahora" => {
104                    components.assign(Component::Year, ref_date.year());
105                    components.assign(Component::Month, ref_date.month() as i32);
106                    components.assign(Component::Day, ref_date.day() as i32);
107                    components.assign(Component::Hour, ref_date.hour() as i32);
108                    components.assign(Component::Minute, ref_date.minute() as i32);
109                    components.assign(Component::Second, ref_date.second() as i32);
110                }
111                "hoy" => {
112                    components.assign(Component::Year, ref_date.year());
113                    components.assign(Component::Month, ref_date.month() as i32);
114                    components.assign(Component::Day, ref_date.day() as i32);
115                }
116                "mañana" | "manana" => {
117                    // "mañana" alone = tomorrow (not morning)
118                    target_date = ref_date + Duration::days(1);
119                    components.assign(Component::Year, target_date.year());
120                    components.assign(Component::Month, target_date.month() as i32);
121                    components.assign(Component::Day, target_date.day() as i32);
122                }
123                "ayer" => {
124                    target_date = ref_date - Duration::days(1);
125                    components.assign(Component::Year, target_date.year());
126                    components.assign(Component::Month, target_date.month() as i32);
127                    components.assign(Component::Day, target_date.day() as i32);
128                }
129                _ if date_keyword.contains("pasado")
130                    && (date_keyword.contains("mañana") || date_keyword.contains("manana")) =>
131                {
132                    // "pasado mañana" = day after tomorrow
133                    target_date = ref_date + Duration::days(2);
134                    components.assign(Component::Year, target_date.year());
135                    components.assign(Component::Month, target_date.month() as i32);
136                    components.assign(Component::Day, target_date.day() as i32);
137                }
138                "anteayer" => {
139                    // "anteayer" = day before yesterday
140                    target_date = ref_date - Duration::days(2);
141                    components.assign(Component::Year, target_date.year());
142                    components.assign(Component::Month, target_date.month() as i32);
143                    components.assign(Component::Day, target_date.day() as i32);
144                }
145                _ if date_keyword.contains("antes") && date_keyword.contains("ayer") => {
146                    // "antes de ayer" = day before yesterday
147                    target_date = ref_date - Duration::days(2);
148                    components.assign(Component::Year, target_date.year());
149                    components.assign(Component::Month, target_date.month() as i32);
150                    components.assign(Component::Day, target_date.day() as i32);
151                }
152                "anoche" => {
153                    if ref_date.hour() > 6 {
154                        target_date = ref_date - Duration::days(1);
155                    }
156                    components.assign(Component::Year, target_date.year());
157                    components.assign(Component::Month, target_date.month() as i32);
158                    components.assign(Component::Day, target_date.day() as i32);
159                    components.imply(Component::Hour, 22);
160                    components.assign(Component::Meridiem, Meridiem::PM as i32);
161                }
162                _ if date_keyword.contains("esta") && date_keyword.contains("noche") => {
163                    components.assign(Component::Year, ref_date.year());
164                    components.assign(Component::Month, ref_date.month() as i32);
165                    components.assign(Component::Day, ref_date.day() as i32);
166                    components.imply(Component::Hour, 22);
167                    components.assign(Component::Meridiem, Meridiem::PM as i32);
168                }
169                _ if date_keyword.contains("esta")
170                    && (date_keyword.contains("mañana") || date_keyword.contains("manana")) =>
171                {
172                    components.assign(Component::Year, ref_date.year());
173                    components.assign(Component::Month, ref_date.month() as i32);
174                    components.assign(Component::Day, ref_date.day() as i32);
175                    components.imply(Component::Hour, 6);
176                    components.assign(Component::Meridiem, Meridiem::AM as i32);
177                }
178                _ if date_keyword.contains("esta") && date_keyword.contains("tarde") => {
179                    components.assign(Component::Year, ref_date.year());
180                    components.assign(Component::Month, ref_date.month() as i32);
181                    components.assign(Component::Day, ref_date.day() as i32);
182                    components.imply(Component::Hour, 15);
183                    components.assign(Component::Meridiem, Meridiem::PM as i32);
184                }
185                _ => {
186                    start = match_end;
187                    continue;
188                }
189            }
190
191            // Apply time component if present (time of day modifier)
192            if let Some(ref time_kw) = time_keyword {
193                Self::extract_time_components(&mut components, time_kw);
194            }
195
196            // Handle explicit time expression: "a las X" or "a mediodía"
197            if captures.get(MEDIODIA_GROUP).is_some() {
198                // "a mediodía" = noon
199                components.assign(Component::Hour, 12);
200                components.assign(Component::Minute, 0);
201                components.assign(Component::Meridiem, Meridiem::PM as i32);
202            } else if let Some(hour_match) = captures.get(HOUR_GROUP) {
203                let hour_str = hour_match.as_str();
204                let mut hour: i32 = hour_str.parse().unwrap_or(0);
205
206                let minute: i32 = captures
207                    .get(MINUTE_GROUP)
208                    .map(|m| m.as_str().parse().unwrap_or(0))
209                    .unwrap_or(0);
210
211                // Handle AM/PM
212                let has_pm = captures
213                    .get(MERIDIEM_GROUP)
214                    .map(|m| m.as_str().to_lowercase().starts_with('p'))
215                    .unwrap_or(false);
216                let has_am = captures
217                    .get(MERIDIEM_GROUP)
218                    .map(|m| m.as_str().to_lowercase().starts_with('a'))
219                    .unwrap_or(false);
220
221                // Infer PM for night context
222                let infer_pm = time_keyword
223                    .as_ref()
224                    .map(|t| t.contains("noche"))
225                    .unwrap_or(false)
226                    || date_keyword.contains("noche");
227
228                if has_pm {
229                    if hour < 12 {
230                        hour += 12;
231                    }
232                    components.assign(Component::Meridiem, Meridiem::PM as i32);
233                } else if has_am {
234                    if hour == 12 {
235                        hour = 0;
236                    }
237                    components.assign(Component::Meridiem, Meridiem::AM as i32);
238                } else if infer_pm && hour <= 12 {
239                    if hour < 12 {
240                        hour += 12;
241                    }
242                    components.assign(Component::Meridiem, Meridiem::PM as i32);
243                }
244
245                components.assign(Component::Hour, hour);
246                components.assign(Component::Minute, minute);
247            }
248
249            results.push(context.create_result(match_start, match_end, components, None));
250
251            start = match_end;
252        }
253
254        Ok(results)
255    }
256}
257
258impl Default for ESCasualDateParser {
259    fn default() -> Self {
260        Self::new()
261    }
262}