whichtime_sys/parsers/common/
weekday.rs1use 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
16static 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 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
69pub 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 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 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 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 let diff = (weekday as i64) - (ref_weekday as i64);
187 if diff == 0 {
188 0 } else if diff > 0 {
190 if diff <= 3 {
192 diff } else {
194 diff - 7 }
196 } else {
197 if diff >= -3 {
199 diff } else {
201 diff + 7 }
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 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}