Skip to main content

whichtime_sys/parsers/common/
weekday.rs

1//! Multi-locale weekday parser: Monday, next Friday, last Tuesday, etc.
2//!
3//! Handles weekday expressions across all supported locales.
4
5use crate::components::Component;
6use crate::context::ParsingContext;
7use crate::dictionaries::{Locale, RelativeModifier, Weekday};
8use crate::error::Result;
9use crate::parsers::Parser;
10use crate::results::ParsedResult;
11use crate::scanner::TokenType;
12use chrono::{Datelike, Duration};
13use regex::Regex;
14use std::sync::LazyLock;
15
16// Locale-specific patterns
17static EN_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
18    Regex::new(r"(?i)(?:^|\W)(?:(this|next|last|past|previous)\s+)?(sun(?:day)?|mon(?:day)?|tue(?:s(?:day)?)?|wed(?:nesday)?|thu(?:rs(?:day)?)?|fri(?:day)?|sat(?:urday)?)(?:\W|$)").unwrap()
19});
20
21static DE_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
22    Regex::new(r"(?i)(?:^|\W)(?:(dieser?|diese[nms]?|nächster?|nächste[nms]?|naechster?|naechste[nms]?|letzter?|letzte[nms]?|kommender?|kommende[nms]?|vergangener?|vergangene[nms]?|voriger?|vorige[nms]?)\s+)?(sonntag|so|montag|mo|dienstag|di|mittwoch|mi|donnerstag|do|freitag|fr|samstag|sa)(?:\W|$)").unwrap()
23});
24
25static ES_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
26    Regex::new(r"(?i)(?:^|\W)(?:(?:el\s+)?(este|próximo|proximo|pasado|último|ultimo)\s+)?(domingo|lunes|martes|miércoles|miercoles|jueves|viernes|sábado|sabado)(?:\W|$)").unwrap()
27});
28
29static FR_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
30    Regex::new(r"(?i)(?:^|\W)(?:(ce|prochain|dernier|passé|passee)\s+)?(dimanche|lundi|mardi|mercredi|jeudi|vendredi|samedi)(?:\W|$)").unwrap()
31});
32
33static IT_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
34    // Italian supports both "modifier weekday" and "weekday modifier" orders
35    Regex::new(r"(?i)(?:^|\W)(?:(?:(questo|prossimo|scorso|passato)\s+)?(domenica|lunedì|lunedi|martedì|martedi|mercoledì|mercoledi|giovedì|giovedi|venerdì|venerdi|sabato)(?:\s+(prossimo|prossima|scorso|scorsa|passato|passata))?)(?:\W|$)").unwrap()
36});
37
38static JA_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
39    Regex::new(
40        r"(?:(今週|来週|先週|前週)の?)?(日曜日?|月曜日?|火曜日?|水曜日?|木曜日?|金曜日?|土曜日?)",
41    )
42    .unwrap()
43});
44
45static NL_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
46    Regex::new(r"(?i)(?:^|\W)(?:(deze|volgende|vorige|afgelopen|komende)\s+)?(zondag|maandag|dinsdag|woensdag|donderdag|vrijdag|zaterdag)(?:\W|$)").unwrap()
47});
48
49static PT_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
50    Regex::new(r"(?i)(?:^|\W)(?:(este|próximo|proximo|passado|último|ultimo)\s+)?(domingo|segunda(?:-feira)?|terça(?:-feira)?|terca(?:-feira)?|quarta(?:-feira)?|quinta(?:-feira)?|sexta(?:-feira)?|sábado|sabado)(?:\W|$)").unwrap()
51});
52
53static RU_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
54    Regex::new(r"(?i)(?:^|\W)(?:(?:в\s+)?(этот|эту|следующий|следующую|прошлый|прошлую|предыдущий|предыдущую)\s+)?(воскресенье|понедельник|вторник|среду?|четверг|пятницу?|субботу?)(?:\W|$)").unwrap()
55});
56
57static SV_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
58    Regex::new(r"(?i)(?:^|\W)(?:(?:på\s+)?(denna|nästa|nasta|förra|forra|kommande)\s+)?(söndag|sondag|måndag|mandag|tisdag|onsdag|torsdag|fredag|lördag|lordag)(?:\W|$)").unwrap()
59});
60
61static UK_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
62    Regex::new(r"(?i)(?:^|\W)(?:(?:у\s+|в\s+)?(цей|цю|наступний|наступну|минулий|минулу|попередній|попередню)\s+)?(неділю?|понеділок|вівторок|середу?|четвер|п'ятницю?|п'ятниця|субот[уа]?)(?:\W|$)").unwrap()
63});
64
65static ZH_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
66    Regex::new(r"(这个?|這個?|下个?|下個?|上个?|上個?)?(星期[日一二三四五六]|周[日一二三四五六]|週[日一二三四五六]|礼拜[日天一二三四五六]|禮拜[日天一二三四五六])").unwrap()
67});
68
69/// Multi-locale weekday parser
70pub struct MultiLocaleWeekdayParser {
71    locale: Locale,
72}
73
74impl MultiLocaleWeekdayParser {
75    pub fn new(locale: Locale) -> Self {
76        Self { locale }
77    }
78
79    fn get_pattern(&self) -> &'static Regex {
80        match self.locale {
81            Locale::En => &EN_PATTERN,
82            Locale::De => &DE_PATTERN,
83            Locale::Es => &ES_PATTERN,
84            Locale::Fr => &FR_PATTERN,
85            Locale::It => &IT_PATTERN,
86            Locale::Ja => &JA_PATTERN,
87            Locale::Nl => &NL_PATTERN,
88            Locale::Pt => &PT_PATTERN,
89            Locale::Ru => &RU_PATTERN,
90            Locale::Sv => &SV_PATTERN,
91            Locale::Uk => &UK_PATTERN,
92            Locale::Zh => &ZH_PATTERN,
93        }
94    }
95
96    fn lookup_weekday(&self, text: &str) -> Option<Weekday> {
97        let lower = text.to_lowercase();
98        match self.locale {
99            Locale::En => crate::dictionaries::en::get_weekday(&lower),
100            Locale::De => crate::dictionaries::de::get_weekday(&lower),
101            Locale::Es => crate::dictionaries::es::get_weekday(&lower),
102            Locale::Fr => crate::dictionaries::fr::get_weekday(&lower),
103            Locale::It => crate::dictionaries::it::get_weekday(&lower),
104            Locale::Ja => crate::dictionaries::ja::get_weekday(text)
105                .or_else(|| crate::dictionaries::ja::get_weekday(&lower)),
106            Locale::Nl => crate::dictionaries::nl::get_weekday(&lower),
107            Locale::Pt => crate::dictionaries::pt::get_weekday(&lower),
108            Locale::Ru => crate::dictionaries::ru::get_weekday(&lower),
109            Locale::Sv => crate::dictionaries::sv::get_weekday(&lower),
110            Locale::Uk => crate::dictionaries::uk::get_weekday(&lower),
111            Locale::Zh => crate::dictionaries::zh::get_weekday(text)
112                .or_else(|| crate::dictionaries::zh::get_weekday(&lower)),
113        }
114    }
115
116    fn lookup_relative_modifier(&self, text: &str) -> Option<RelativeModifier> {
117        let lower = text.to_lowercase();
118        match self.locale {
119            Locale::En => crate::dictionaries::en::get_relative_modifier(&lower),
120            Locale::De => crate::dictionaries::de::get_relative_modifier(&lower),
121            Locale::Es => crate::dictionaries::es::get_relative_modifier(&lower),
122            Locale::Fr => crate::dictionaries::fr::get_relative_modifier(&lower),
123            Locale::It => crate::dictionaries::it::get_relative_modifier(&lower),
124            Locale::Ja => crate::dictionaries::ja::get_relative_modifier(text)
125                .or_else(|| crate::dictionaries::ja::get_relative_modifier(&lower)),
126            Locale::Nl => crate::dictionaries::nl::get_relative_modifier(&lower),
127            Locale::Pt => crate::dictionaries::pt::get_relative_modifier(&lower),
128            Locale::Ru => crate::dictionaries::ru::get_relative_modifier(&lower),
129            Locale::Sv => crate::dictionaries::sv::get_relative_modifier(&lower),
130            Locale::Uk => crate::dictionaries::uk::get_relative_modifier(&lower),
131            Locale::Zh => crate::dictionaries::zh::get_relative_modifier(text)
132                .or_else(|| crate::dictionaries::zh::get_relative_modifier(&lower)),
133        }
134    }
135}
136
137impl Parser for MultiLocaleWeekdayParser {
138    fn name(&self) -> &'static str {
139        "MultiLocaleWeekdayParser"
140    }
141
142    fn should_apply(&self, context: &ParsingContext) -> bool {
143        context.has_token_type(TokenType::Weekday)
144    }
145
146    fn parse(&self, context: &ParsingContext) -> Result<Vec<ParsedResult>> {
147        let mut results = Vec::new();
148        let pattern = self.get_pattern();
149        let ref_date = context.reference.instant;
150        let ref_weekday = ref_date.weekday().num_days_from_sunday();
151
152        for mat in pattern.find_iter(context.text) {
153            let matched_text = mat.as_str();
154            let index = mat.start();
155
156            let Some(caps) = pattern.captures(matched_text) else {
157                continue;
158            };
159
160            // Group 1: Pre-modifier (before weekday), Group 2: Weekday, Group 3: Post-modifier (Italian)
161            let pre_modifier_str = caps.get(1).map(|m| m.as_str());
162            let weekday_str = caps.get(2).map(|m| m.as_str()).unwrap_or_default();
163            let post_modifier_str = caps.get(3).map(|m| m.as_str());
164
165            let Some(weekday) = self.lookup_weekday(weekday_str) else {
166                continue;
167            };
168
169            // Use pre-modifier if available, otherwise use post-modifier (Italian word order)
170            let modifier = pre_modifier_str
171                .and_then(|s| self.lookup_relative_modifier(s))
172                .or_else(|| post_modifier_str.and_then(|s| self.lookup_relative_modifier(s)));
173
174            // Calculate days offset
175            let days_offset = match modifier {
176                Some(RelativeModifier::Next) => {
177                    let diff = (weekday as i64) - (ref_weekday as i64);
178                    if diff <= 0 { diff + 7 } else { diff }
179                }
180                Some(RelativeModifier::Last) => {
181                    let diff = (weekday as i64) - (ref_weekday as i64);
182                    if diff >= 0 { diff - 7 } else { diff }
183                }
184                Some(RelativeModifier::This) | None => {
185                    // Find closest occurrence (past or future)
186                    let diff = (weekday as i64) - (ref_weekday as i64);
187                    if diff == 0 {
188                        0 // Same day
189                    } else if diff > 0 {
190                        // Target is ahead in the week
191                        if diff <= 3 {
192                            diff // Go forward
193                        } else {
194                            diff - 7 // Go back to previous week
195                        }
196                    } else {
197                        // diff < 0, target is behind in the week
198                        if diff >= -3 {
199                            diff // Go back
200                        } else {
201                            diff + 7 // Go to next week
202                        }
203                    }
204                }
205            };
206
207            let target_date = ref_date + Duration::days(days_offset);
208
209            let mut components = context.create_components();
210            components.assign(Component::Year, target_date.year());
211            components.assign(Component::Month, target_date.month() as i32);
212            components.assign(Component::Day, target_date.day() as i32);
213            components.assign(Component::Weekday, weekday as i32);
214
215            // Find actual text bounds
216            let actual_start = matched_text
217                .find(|c: char| c.is_alphanumeric())
218                .unwrap_or(0);
219            let actual_end = matched_text
220                .rfind(|c: char| c.is_alphanumeric())
221                .map(|i| i + matched_text[i..].chars().next().map_or(1, char::len_utf8))
222                .unwrap_or(matched_text.len());
223
224            results.push(context.create_result(
225                index + actual_start,
226                index + actual_end,
227                components,
228                None,
229            ));
230        }
231
232        Ok(results)
233    }
234}