whichtime_sys/parsers/common/
casual_time.rs1use crate::components::Component;
6use crate::context::ParsingContext;
7use crate::dictionaries::{CasualTimeType, Locale, RelativeModifier};
8use crate::error::Result;
9use crate::parsers::Parser;
10use crate::results::ParsedResult;
11use crate::scanner::TokenType;
12use crate::types::Meridiem;
13use chrono::{Datelike, Duration, Timelike};
14use regex::Regex;
15use std::sync::LazyLock;
16
17static EN_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
19 Regex::new(r"(?i)\b(?:(this|last|next|past|previous)\s+)?(noon|midday|midnight|morning|afternoon|evening|night)\b").unwrap()
20});
21
22static DE_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
23 Regex::new(r"(?i)\b(?:(dieser?|diese[nms]?|letzter?|letzte[nms]?|nächster?|nächste[nms]?|naechster?|naechste[nms]?)\s+)?(mittag|mitternacht|morgens?|vormittags?|nachmittags?|abends?|nachts?)\b").unwrap()
24});
25
26static ES_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
27 Regex::new(r"(?i)\b(?:(este|esta|pasado|pasada|próximo|próxima|proximo|proxima)\s+)?(mediodía|mediodia|medianoche|mañana|manana|tarde|noche)(?:\s+pasad[ao]|)?\b").unwrap()
28});
29
30static FR_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
31 Regex::new(r"(?i)\b(?:(ce|cette|dernier|dernière|derniere|prochain|prochaine)\s+)?(midi|minuit|matin|après-midi|apres-midi|soir|nuit)\b").unwrap()
32});
33
34static IT_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
35 Regex::new(r"(?i)\b(?:(questo|questa|scorso|scorsa|prossimo|prossima)\s+)?(mezzogiorno|mezzanotte|mattina|mattino|pomeriggio|sera|notte)\b").unwrap()
36});
37
38static JA_PATTERN: LazyLock<Regex> =
39 LazyLock::new(|| Regex::new(r"(正午|真夜中|朝|午前|午後|夕方|夜|深夜)").unwrap());
40
41static NL_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
42 Regex::new(r"(?i)\b(?:(deze|vorige|volgende|afgelopen|komende)\s+)?(middag|middernacht|ochtend|'s\s*ochtends|'s\s*middags|'s\s*avonds|avond|nacht|'s\s*nachts)\b").unwrap()
43});
44
45static PT_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
46 Regex::new(r"(?i)\b(?:(este|esta|passado|passada|próximo|próxima|proximo|proxima)\s+)?(meio-dia|meio\s*dia|meia-noite|meia\s*noite|manhã|manha|tarde|noite)\b").unwrap()
47});
48
49static RU_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
50 Regex::new(r"(?i)\b(?:(этот|эта|прошлый|прошлая|следующий|следующая)\s+)?(полдень|в\s*полдень|полночь|в\s*полночь|утром?|днём|днем|вечером?|ночью?)\b").unwrap()
51});
52
53static SV_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
54 Regex::new(r"(?i)\b(?:(denna|förra|forra|nästa|nasta)\s+)?(middag|midnatt|morgon(?:en)?|på\s*morgonen|förmiddag(?:en)?|formiddag(?:en)?|eftermiddag(?:en)?|kväll(?:en)?|kvall(?:en)?|natt(?:en)?)\b").unwrap()
55});
56
57static UK_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
58 Regex::new(r"(?i)\b(?:(цей|ця|минулий|минула|наступний|наступна)\s+)?(полудень|опівдні|опівночі|вранці|ранок|вдень|ввечері|вночі)\b").unwrap()
59});
60
61static ZH_PATTERN: LazyLock<Regex> =
62 LazyLock::new(|| Regex::new(r"(中午|正午|午夜|凌晨|早上|上午|下午|傍晚|晚上|深夜)").unwrap());
63
64pub struct MultiLocaleCasualTimeParser {
66 locale: Locale,
67}
68
69impl MultiLocaleCasualTimeParser {
70 pub fn new(locale: Locale) -> Self {
71 Self { locale }
72 }
73
74 fn get_pattern(&self) -> &'static Regex {
75 match self.locale {
76 Locale::En => &EN_PATTERN,
77 Locale::De => &DE_PATTERN,
78 Locale::Es => &ES_PATTERN,
79 Locale::Fr => &FR_PATTERN,
80 Locale::It => &IT_PATTERN,
81 Locale::Ja => &JA_PATTERN,
82 Locale::Nl => &NL_PATTERN,
83 Locale::Pt => &PT_PATTERN,
84 Locale::Ru => &RU_PATTERN,
85 Locale::Sv => &SV_PATTERN,
86 Locale::Uk => &UK_PATTERN,
87 Locale::Zh => &ZH_PATTERN,
88 }
89 }
90
91 fn lookup_casual_time(&self, text: &str) -> Option<CasualTimeType> {
92 let lower = text.to_lowercase();
93 let normalized: String = lower.split_whitespace().collect::<Vec<_>>().join(" ");
95
96 match self.locale {
97 Locale::En => crate::dictionaries::en::get_casual_time(&normalized),
98 Locale::De => crate::dictionaries::de::get_casual_time(&normalized),
99 Locale::Es => crate::dictionaries::es::get_casual_time(&normalized),
100 Locale::Fr => crate::dictionaries::fr::get_casual_time(&normalized),
101 Locale::It => crate::dictionaries::it::get_casual_time(&normalized),
102 Locale::Ja => crate::dictionaries::ja::get_casual_time(text)
103 .or_else(|| crate::dictionaries::ja::get_casual_time(&normalized)),
104 Locale::Nl => crate::dictionaries::nl::get_casual_time(&normalized),
105 Locale::Pt => crate::dictionaries::pt::get_casual_time(&normalized),
106 Locale::Ru => crate::dictionaries::ru::get_casual_time(&normalized),
107 Locale::Sv => crate::dictionaries::sv::get_casual_time(&normalized),
108 Locale::Uk => crate::dictionaries::uk::get_casual_time(&normalized),
109 Locale::Zh => crate::dictionaries::zh::get_casual_time(text)
110 .or_else(|| crate::dictionaries::zh::get_casual_time(&normalized)),
111 }
112 }
113
114 fn lookup_relative_modifier(&self, text: &str) -> Option<RelativeModifier> {
115 let lower = text.to_lowercase();
116 match self.locale {
117 Locale::En => crate::dictionaries::en::get_relative_modifier(&lower),
118 Locale::De => crate::dictionaries::de::get_relative_modifier(&lower),
119 Locale::Es => crate::dictionaries::es::get_relative_modifier(&lower),
120 Locale::Fr => crate::dictionaries::fr::get_relative_modifier(&lower),
121 Locale::It => crate::dictionaries::it::get_relative_modifier(&lower),
122 Locale::Ja => crate::dictionaries::ja::get_relative_modifier(text)
123 .or_else(|| crate::dictionaries::ja::get_relative_modifier(&lower)),
124 Locale::Nl => crate::dictionaries::nl::get_relative_modifier(&lower),
125 Locale::Pt => crate::dictionaries::pt::get_relative_modifier(&lower),
126 Locale::Ru => crate::dictionaries::ru::get_relative_modifier(&lower),
127 Locale::Sv => crate::dictionaries::sv::get_relative_modifier(&lower),
128 Locale::Uk => crate::dictionaries::uk::get_relative_modifier(&lower),
129 Locale::Zh => crate::dictionaries::zh::get_relative_modifier(text)
130 .or_else(|| crate::dictionaries::zh::get_relative_modifier(&lower)),
131 }
132 }
133
134 fn is_digit_like(ch: char) -> bool {
135 ch.is_ascii_digit()
136 || ('0'..='9').contains(&ch)
137 || matches!(
138 ch,
139 '〇' | '一' | '二' | '三' | '四' | '五' | '六' | '七' | '八' | '九' | '十'
140 )
141 }
142
143 fn has_trailing_number(text: &str, idx: usize) -> bool {
144 if idx >= text.len() {
145 return false;
146 }
147
148 let chars = text[idx..].chars();
149 for ch in chars {
150 if ch.is_whitespace() {
151 continue;
152 }
153 return Self::is_digit_like(ch);
154 }
155 false
156 }
157}
158
159impl Parser for MultiLocaleCasualTimeParser {
160 fn name(&self) -> &'static str {
161 "MultiLocaleCasualTimeParser"
162 }
163
164 fn should_apply(&self, context: &ParsingContext) -> bool {
165 context.has_token_type(TokenType::CasualTime)
166 }
167
168 fn parse(&self, context: &ParsingContext) -> Result<Vec<ParsedResult>> {
169 let mut results = Vec::new();
170 let pattern = self.get_pattern();
171 let ref_date = context.reference.instant;
172
173 for mat in pattern.find_iter(context.text) {
174 let matched_text = mat.as_str();
175 let index = mat.start();
176 if matches!(self.locale, Locale::Ja)
177 && Self::has_trailing_number(context.text, mat.end())
178 {
179 continue;
180 }
181
182 let Some(caps) = pattern.captures(matched_text) else {
183 continue;
184 };
185
186 let (modifier_str, time_word) = match self.locale {
188 Locale::Ja | Locale::Zh => {
189 (None, caps.get(1).map(|m| m.as_str()).unwrap_or_default())
190 }
191 _ => (
192 caps.get(1).map(|m| m.as_str()),
193 caps.get(2).map(|m| m.as_str()).unwrap_or_default(),
194 ),
195 };
196
197 let Some(time_type) = self.lookup_casual_time(time_word) else {
198 continue;
199 };
200
201 let modifier = modifier_str.and_then(|s| self.lookup_relative_modifier(s));
202
203 let mut components = context.create_components();
204
205 let target_date = match modifier {
207 Some(RelativeModifier::Last) => {
208 if ref_date.hour() <= 6 && matches!(time_type, CasualTimeType::Night) {
209 ref_date
210 } else {
211 ref_date - Duration::days(1)
212 }
213 }
214 Some(RelativeModifier::Next) => ref_date + Duration::days(1),
215 Some(RelativeModifier::This) | None => ref_date,
216 };
217
218 components.assign(Component::Year, target_date.year());
220 components.assign(Component::Month, target_date.month() as i32);
221 components.assign(Component::Day, target_date.day() as i32);
222
223 match time_type {
225 CasualTimeType::Noon => {
226 components.assign(Component::Hour, 12);
227 components.assign(Component::Minute, 0);
228 components.assign(Component::Second, 0);
229 components.assign(Component::Meridiem, Meridiem::PM as i32);
230 }
231 CasualTimeType::Midnight => {
232 if matches!(modifier, Some(RelativeModifier::Last)) {
233 components.assign(Component::Day, target_date.day() as i32);
234 } else if modifier.is_none() {
235 let next_day = ref_date + Duration::days(1);
236 components.assign(Component::Year, next_day.year());
237 components.assign(Component::Month, next_day.month() as i32);
238 components.assign(Component::Day, next_day.day() as i32);
239 }
240 components.assign(Component::Hour, 0);
241 components.assign(Component::Minute, 0);
242 components.assign(Component::Second, 0);
243 }
244 CasualTimeType::Morning => {
245 components.imply(Component::Hour, 6);
246 components.imply(Component::Minute, 0);
247 components.assign(Component::Meridiem, Meridiem::AM as i32);
248 }
249 CasualTimeType::Afternoon => {
250 components.imply(Component::Hour, 15);
251 components.imply(Component::Minute, 0);
252 components.assign(Component::Meridiem, Meridiem::PM as i32);
253 }
254 CasualTimeType::Evening => {
255 components.imply(Component::Hour, 20);
256 components.imply(Component::Minute, 0);
257 components.assign(Component::Meridiem, Meridiem::PM as i32);
258 }
259 CasualTimeType::Night => {
260 components.imply(Component::Hour, 22);
261 components.imply(Component::Minute, 0);
262 components.assign(Component::Meridiem, Meridiem::PM as i32);
263 }
264 }
265
266 results.push(context.create_result(
267 index,
268 index + matched_text.len(),
269 components,
270 None,
271 ));
272 }
273
274 Ok(results)
275 }
276}