1use std::sync::OnceLock;
8
9use crate::calculation::duration::{Duration, DurationUnit};
10use crate::core::chrono::{Extracted, Parser};
11use crate::core::components::{meridiem, Component, ParsingComponents};
12use crate::core::context::ParsingContext;
13use crate::core::regex::{JsRegex, RegexMatch};
14use crate::core::result::ParsingResult;
15use crate::core::utils::js::js_parse_int;
16use crate::js_regex;
17
18const DEFAULT_SUFFIX: &str = r"(?!/)(?=\W|$)";
19
20fn primary_time_pattern(left_boundary: &str, primary_prefix: &str, primary_suffix: &str, flags: &str) -> JsRegex {
21 let source = format!(
22 concat!(
23 "{}{}",
24 r"(\d{{1,4}})",
25 r"(?:",
26 r"(?:\.|:|:)",
27 r"(\d{{1,2}})",
28 r"(?:",
29 r"(?::|:)",
30 r"(\d{{2}})",
31 r"(?:\.(\d{{1,6}}))?",
32 r")?",
33 r")?",
34 r"(?:\s*(a\.m\.|p\.m\.|am?|pm?))?",
35 "{}",
36 ),
37 left_boundary, primary_prefix, primary_suffix
38 );
39 JsRegex::new(&source, flags)
40}
41
42fn following_time_pattern(following_phase: &str, following_suffix: &str) -> JsRegex {
43 let source = format!(
44 concat!(
45 r"^({})",
46 r"(\d{{1,4}})",
47 r"(?:",
48 r"(?:\.|\:|\:)",
49 r"(\d{{1,2}})",
50 r"(?:",
51 r"(?:\.|\:|\:)",
52 r"(\d{{1,2}})(?:\.(\d{{1,6}}))?",
53 r")?",
54 r")?",
55 r"(?:\s*(a\.m\.|p\.m\.|am?|pm?))?",
56 "{}",
57 ),
58 following_phase, following_suffix
59 );
60 JsRegex::new(&source, "i")
61}
62
63pub const HOUR_GROUP: usize = 2;
64pub const MINUTE_GROUP: usize = 3;
65pub const SECOND_GROUP: usize = 4;
66pub const MILLI_SECOND_GROUP: usize = 5;
67pub const AM_PM_HOUR_GROUP: usize = 6;
68
69pub trait TimeExpressionParser: Send + Sync {
71 fn strict_mode(&self) -> bool;
72 fn primary_prefix(&self) -> &str;
73 fn following_phase(&self) -> &str;
74
75 fn pattern_flags(&self) -> &str {
76 "i"
77 }
78 fn primary_pattern_left_boundary(&self) -> &str {
79 r"(^|\s|T|\b)"
80 }
81 fn primary_suffix(&self) -> &str {
82 DEFAULT_SUFFIX
83 }
84 fn following_suffix(&self) -> &str {
85 DEFAULT_SUFFIX
86 }
87
88 fn extract_primary_time_components(&self, context: &ParsingContext, m: &RegexMatch) -> Option<ParsingComponents> {
89 extract_primary_time_components(self.strict_mode(), context, m)
90 }
91
92 fn extract_following_time_components(
93 &self,
94 context: &ParsingContext,
95 m: &RegexMatch,
96 result: &mut ParsingResult,
97 ) -> Option<ParsingComponents> {
98 extract_following_time_components(context, m, result)
99 }
100}
101
102fn truthy(group: Option<&str>) -> bool {
104 group.is_some_and(|g| !g.is_empty())
105}
106
107fn first_char_lower(s: &str) -> String {
108 s.chars().next().map(|c| c.to_lowercase().collect()).unwrap_or_default()
109}
110
111fn int_group(m: &RegexMatch, group: usize) -> Option<i64> {
113 m.get(group).and_then(js_parse_int)
114}
115
116fn millisecond_group(m: &RegexMatch) -> Option<i64> {
118 m.get(MILLI_SECOND_GROUP)
119 .and_then(|g| js_parse_int(&g[..g.len().min(3)]))
120}
121
122pub fn extract_primary_time_components(
124 strict_mode: bool,
125 context: &ParsingContext,
126 m: &RegexMatch,
127) -> Option<ParsingComponents> {
128 let mut components = context.create_parsing_components(&[]);
129 let mut minute = 0;
130 let mut meridiem_value: Option<i64> = None;
131
132 let mut hour = int_group(m, HOUR_GROUP)?;
134 if hour > 100 {
135 if m.get(HOUR_GROUP).map_or(0, str::len) == 4
137 && m.get(MINUTE_GROUP).is_none()
138 && !truthy(m.get(AM_PM_HOUR_GROUP))
139 {
140 return None;
141 }
142 if strict_mode || m.get(MINUTE_GROUP).is_some() {
143 return None;
144 }
145 minute = hour % 100;
146 hour /= 100;
147 }
148
149 if hour > 24 {
150 return None;
151 }
152
153 if let Some(minute_text) = m.get(MINUTE_GROUP) {
155 if minute_text.len() == 1 && !truthy(m.get(AM_PM_HOUR_GROUP)) {
156 return None;
158 }
159 minute = int_group(m, MINUTE_GROUP)?;
160 }
161
162 if minute >= 60 {
163 return None;
164 }
165
166 if hour > 12 {
167 meridiem_value = Some(meridiem::PM);
168 }
169
170 if let Some(ampm_text) = m.get(AM_PM_HOUR_GROUP) {
172 if hour > 12 {
173 return None;
174 }
175 let ampm = first_char_lower(ampm_text);
176 if ampm == "a" {
177 meridiem_value = Some(meridiem::AM);
178 if hour == 12 {
179 hour = 0;
180 }
181 }
182 if ampm == "p" {
183 meridiem_value = Some(meridiem::PM);
184 if hour != 12 {
185 hour += 12;
186 }
187 }
188 }
189
190 components.assign(Component::Hour, hour);
191 components.assign(Component::Minute, minute);
192
193 match meridiem_value {
194 Some(v) => {
195 components.assign(Component::Meridiem, v);
196 }
197 None => {
198 components.imply(Component::Meridiem, if hour < 12 { meridiem::AM } else { meridiem::PM });
199 }
200 }
201
202 if m.get(MILLI_SECOND_GROUP).is_some() {
204 let millisecond = millisecond_group(m)?;
205 if millisecond >= 1000 {
206 return None;
207 }
208 components.assign(Component::Millisecond, millisecond);
209 }
210
211 if m.get(SECOND_GROUP).is_some() {
213 let second = int_group(m, SECOND_GROUP)?;
214 if second >= 60 {
215 return None;
216 }
217 components.assign(Component::Second, second);
218 }
219
220 Some(components)
221}
222
223pub fn extract_following_time_components(
225 context: &ParsingContext,
226 m: &RegexMatch,
227 result: &mut ParsingResult,
228) -> Option<ParsingComponents> {
229 let mut components = context.create_parsing_components(&[]);
230
231 if m.get(MILLI_SECOND_GROUP).is_some() {
233 let millisecond = millisecond_group(m)?;
234 if millisecond >= 1000 {
235 return None;
236 }
237 components.assign(Component::Millisecond, millisecond);
238 }
239
240 if m.get(SECOND_GROUP).is_some() {
242 let second = int_group(m, SECOND_GROUP)?;
243 if second >= 60 {
244 return None;
245 }
246 components.assign(Component::Second, second);
247 }
248
249 let mut hour = int_group(m, HOUR_GROUP)?;
250 let mut minute = 0;
251 let mut meridiem_value: i64 = -1;
252
253 if m.get(MINUTE_GROUP).is_some() {
255 minute = int_group(m, MINUTE_GROUP)?;
256 } else if hour > 100 {
257 minute = hour % 100;
258 hour /= 100;
259 }
260
261 if minute >= 60 || hour > 24 {
262 return None;
263 }
264
265 if hour >= 12 {
266 meridiem_value = meridiem::PM;
267 }
268
269 if let Some(ampm_text) = m.get(AM_PM_HOUR_GROUP) {
271 if hour > 12 {
272 return None;
273 }
274 let ampm = first_char_lower(ampm_text);
275 if ampm == "a" {
276 meridiem_value = meridiem::AM;
277 if hour == 12 {
278 hour = 0;
279 if !components.is_certain(Component::Day) {
280 components.add_duration_as_implied(&mut Duration::from_pairs(&[(DurationUnit::Day, 1.0)]));
281 }
282 }
283 }
284 if ampm == "p" {
285 meridiem_value = meridiem::PM;
286 if hour != 12 {
287 hour += 12;
288 }
289 }
290
291 if !result.start.is_certain(Component::Meridiem) {
292 if meridiem_value == meridiem::AM {
293 result.start.imply(Component::Meridiem, meridiem::AM);
294 if result.start.get(Component::Hour) == Some(12) {
295 result.start.assign(Component::Hour, 0);
296 }
297 } else {
298 result.start.imply(Component::Meridiem, meridiem::PM);
299 if result.start.get(Component::Hour) != Some(12) {
300 let start_hour = result.start.get_or_zero(Component::Hour);
301 result.start.assign(Component::Hour, start_hour + 12);
302 }
303 }
304 }
305 }
306
307 components.assign(Component::Hour, hour);
308 components.assign(Component::Minute, minute);
309
310 if meridiem_value >= 0 {
311 components.assign(Component::Meridiem, meridiem_value);
312 } else {
313 let start_hour = result.start.get_or_zero(Component::Hour);
314 let start_at_pm = result.start.is_certain(Component::Meridiem) && start_hour > 12;
315 if start_at_pm {
316 if start_hour - 12 > hour {
317 components.imply(Component::Meridiem, meridiem::AM);
319 } else if hour <= 12 {
320 components.assign(Component::Hour, hour + 12);
321 components.assign(Component::Meridiem, meridiem::PM);
322 }
323 } else if hour > 12 {
324 components.imply(Component::Meridiem, meridiem::PM);
325 } else if hour <= 12 {
326 components.imply(Component::Meridiem, meridiem::AM);
327 }
328 }
329
330 if let (Some(end), Some(start)) = (components.date().get_time(), result.start.date().get_time()) {
331 if end < start {
332 components.add_duration_as_implied(&mut Duration::from_pairs(&[(DurationUnit::Day, 1.0)]));
333 }
334 }
335
336 Some(components)
337}
338
339pub struct TimeExpression<P> {
341 inner: P,
342 primary: OnceLock<JsRegex>,
343 following: OnceLock<JsRegex>,
344}
345
346impl<P: TimeExpressionParser> TimeExpression<P> {
347 pub fn new(inner: P) -> TimeExpression<P> {
348 TimeExpression {
349 inner,
350 primary: OnceLock::new(),
351 following: OnceLock::new(),
352 }
353 }
354
355 fn primary_pattern(&self) -> &JsRegex {
356 self.primary.get_or_init(|| {
357 primary_time_pattern(
358 self.inner.primary_pattern_left_boundary(),
359 self.inner.primary_prefix(),
360 self.inner.primary_suffix(),
361 self.inner.pattern_flags(),
362 )
363 })
364 }
365
366 fn following_pattern(&self) -> &JsRegex {
367 self.following
368 .get_or_init(|| following_time_pattern(self.inner.following_phase(), self.inner.following_suffix()))
369 }
370
371 fn check_and_return_without_following_pattern(&self, result: ParsingResult) -> Option<ParsingResult> {
372 let text = result.text.as_str();
373 if js_regex!(r"^\d$").is_match(text) {
375 return None;
376 }
377 if js_regex!(r"^\d\d\d+$").is_match(text) {
379 return None;
380 }
381 if js_regex!(r"\d[apAP]$").is_match(text) {
383 return None;
384 }
385 if let Some(ending) = js_regex!(r"[^\d:.](\d[\d.]+)$").exec(text) {
387 let ending_numbers = ending.get(1).unwrap_or("");
388 if self.inner.strict_mode() {
390 return None;
391 }
392 if ending_numbers.contains('.') && !js_regex!(r"\d(\.\d{2})+$").is_match(ending_numbers) {
394 return None;
395 }
396 if js_parse_int(ending_numbers).is_some_and(|v| v > 24) {
398 return None;
399 }
400 }
401 Some(result)
402 }
403
404 fn check_and_return_with_following_pattern(&self, result: ParsingResult) -> Option<ParsingResult> {
405 let text = result.text.as_str();
406 if js_regex!(r"^\d+-\d+$").is_match(text) {
407 return None;
408 }
409 if let Some(ending) = js_regex!(r"[^\d:.](\d[\d.]+)\s*-\s*(\d[\d.]+)$").exec(text) {
411 if self.inner.strict_mode() {
413 return None;
414 }
415 let starting_numbers = ending.get(1).unwrap_or("");
416 let ending_numbers = ending.get(2).unwrap_or("");
417 if ending_numbers.contains('.') && !js_regex!(r"\d(\.\d{2})+$").is_match(ending_numbers) {
419 return None;
420 }
421 let gt24 = |s: &str| js_parse_int(s).is_some_and(|v| v > 24);
423 if gt24(ending_numbers) || gt24(starting_numbers) {
424 return None;
425 }
426 }
427 Some(result)
428 }
429}
430
431impl<P: TimeExpressionParser> Parser for TimeExpression<P> {
432 fn pattern(&self, _context: &ParsingContext) -> JsRegex {
433 self.primary_pattern().clone()
434 }
435
436 fn extract(&self, context: &ParsingContext, m: &mut RegexMatch) -> Extracted {
437 let Some(start_components) = self.inner.extract_primary_time_components(context, m) else {
438 if js_regex!(r"^\d{4}").is_match(m.text()) {
440 m.index += 4;
441 return Extracted::None;
442 }
443 m.index += m.text().len();
444 return Extracted::None;
445 };
446
447 let header_len = m.get(1).map_or(0, str::len);
448 let index = m.index + header_len;
449 let text = m.text()[header_len..].to_string();
450 let mut result = context.create_parsing_result(index, text.clone(), Some(start_components), None);
451 m.index += m.text().len();
452
453 let remaining = context.text.get(m.index..).unwrap_or("");
454 let following = self.following_pattern().exec(remaining);
455
456 if let Some(f) = &following {
458 if js_regex!(r"^\d{3,4}").is_match(&text) {
459 if js_regex!(r"^\s*([+-])\s*\d{2,4}$").is_match(f.text()) {
461 return Extracted::None;
462 }
463 if js_regex!(r"^\s*([+-])\s*\d{2}\W\d{2}").is_match(f.text()) {
465 return Extracted::None;
466 }
467 }
468 }
469
470 let following = match following {
471 Some(f) if !js_regex!(r"^\s*([+-])\s*\d{3,4}$").is_match(f.text()) => f,
473 _ => {
474 return self
475 .check_and_return_without_following_pattern(result)
476 .map_or(Extracted::None, Extracted::Result);
477 }
478 };
479
480 result.end = self
481 .inner
482 .extract_following_time_components(context, &following, &mut result);
483 if result.end.is_some() {
484 result.text.push_str(following.text());
485 }
486
487 self.check_and_return_with_following_pattern(result)
488 .map_or(Extracted::None, Extracted::Result)
489 }
490}