whichtime_sys/parsers/es/
time_expression.rs1use crate::components::Component;
10use crate::context::ParsingContext;
11use crate::error::Result;
12use crate::parsers::Parser;
13use crate::results::ParsedResult;
14use crate::types::Meridiem;
15use fancy_regex::Regex;
16use std::sync::LazyLock;
17
18static PRIMARY_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
21 Regex::new(
22 r"(?i)(?:^|[^\d])(?:de\s+)?(?:las?\s+)?(\d{1,2})(?:[:\.](\d{2}))?(?::(\d{2}))?(?:\s*(a\.?m\.?|p\.?m\.?))?(?:\s*(?:a(?:\s+las?)?|[\-–~])\s*(\d{1,2})(?:[:\.](\d{2}))?(?::(\d{2}))?(?:\s*(a\.?m\.?|p\.?m\.?))?)?(?=[^\d]|$)"
23 ).unwrap()
24});
25
26pub struct ESTimeExpressionParser;
28
29impl ESTimeExpressionParser {
30 pub fn new() -> Self {
31 Self
32 }
33
34 fn parse_meridiem(s: &str) -> Option<Meridiem> {
35 let lower = s.to_lowercase();
36 if lower.starts_with('p') {
37 Some(Meridiem::PM)
38 } else if lower.starts_with('a') {
39 Some(Meridiem::AM)
40 } else {
41 None
42 }
43 }
44
45 fn adjust_hour(hour: i32, meridiem: Option<Meridiem>) -> i32 {
46 match meridiem {
47 Some(Meridiem::PM) => {
48 if hour < 12 {
49 hour + 12
50 } else {
51 hour
52 }
53 }
54 Some(Meridiem::AM) => {
55 if hour == 12 {
56 0
57 } else {
58 hour
59 }
60 }
61 None => hour,
62 }
63 }
64}
65
66impl Parser for ESTimeExpressionParser {
67 fn name(&self) -> &'static str {
68 "ESTimeExpressionParser"
69 }
70
71 fn should_apply(&self, context: &ParsingContext) -> bool {
72 let text = context.text;
73 text.bytes().any(|b| b.is_ascii_digit())
75 && (text.contains(':')
76 || text.contains('.')
77 || text.to_lowercase().contains("am")
78 || text.to_lowercase().contains("pm")
79 || text.to_lowercase().contains("a.m")
80 || text.to_lowercase().contains("p.m"))
81 }
82
83 fn parse(&self, context: &ParsingContext) -> Result<Vec<ParsedResult>> {
84 let mut results = Vec::new();
85 let ref_date = context.reference.instant;
86
87 let mut start = 0;
88 while start < context.text.len() {
89 let search_text = &context.text[start..];
90 let mat = match PRIMARY_PATTERN.find(search_text) {
91 Ok(Some(m)) => m,
92 Ok(None) => break,
93 Err(_) => break,
94 };
95
96 let matched_text = mat.as_str();
97 let index = start + mat.start();
98
99 let caps = match PRIMARY_PATTERN.captures(matched_text) {
100 Ok(Some(c)) => c,
101 Ok(None) => {
102 start = index + 1;
103 continue;
104 }
105 Err(_) => {
106 start = index + 1;
107 continue;
108 }
109 };
110
111 let hour1: i32 = caps
113 .get(1)
114 .and_then(|m| m.as_str().parse().ok())
115 .unwrap_or(-1);
116
117 if !(0..=23).contains(&hour1) {
118 start = index + 1;
119 continue;
120 }
121
122 let minute1: i32 = caps
123 .get(2)
124 .and_then(|m| m.as_str().parse().ok())
125 .unwrap_or(0);
126 let second1: i32 = caps
127 .get(3)
128 .and_then(|m| m.as_str().parse().ok())
129 .unwrap_or(0);
130 let meridiem1 = caps
131 .get(4)
132 .map(|m| m.as_str())
133 .and_then(Self::parse_meridiem);
134
135 let has_end_time = caps.get(5).is_some();
137 let hour2: i32 = caps
138 .get(5)
139 .and_then(|m| m.as_str().parse().ok())
140 .unwrap_or(-1);
141 let minute2: i32 = caps
142 .get(6)
143 .and_then(|m| m.as_str().parse().ok())
144 .unwrap_or(0);
145 let second2: i32 = caps
146 .get(7)
147 .and_then(|m| m.as_str().parse().ok())
148 .unwrap_or(0);
149 let meridiem2 = caps
150 .get(8)
151 .map(|m| m.as_str())
152 .and_then(Self::parse_meridiem);
153
154 if meridiem1.is_none() && caps.get(2).is_none() && !has_end_time {
157 let prefix =
159 &matched_text[..matched_text.find(|c: char| c.is_ascii_digit()).unwrap_or(0)];
160 let lower_prefix = prefix.to_lowercase();
161 if !lower_prefix.contains("las") && !lower_prefix.contains("de") {
162 start = index + 1;
163 continue;
164 }
165 }
166
167 let adj_hour1 = Self::adjust_hour(hour1, meridiem1);
169
170 let effective_meridiem2 = meridiem2.or(meridiem1);
172 let adj_hour2 = if has_end_time {
173 Self::adjust_hour(hour2, effective_meridiem2)
174 } else {
175 0
176 };
177
178 let mut components = context.create_components();
180 components.assign(Component::Hour, adj_hour1);
181 components.assign(Component::Minute, minute1);
182 if caps.get(3).is_some() {
183 components.assign(Component::Second, second1);
184 }
185 if let Some(m) = meridiem1 {
186 components.assign(Component::Meridiem, m as i32);
187 } else if adj_hour1 >= 12 {
188 components.assign(Component::Meridiem, Meridiem::PM as i32);
189 }
190
191 let end_components = if has_end_time && hour2 >= 0 {
193 let mut end_comp = context.create_components();
194 end_comp.assign(Component::Hour, adj_hour2);
195 end_comp.assign(Component::Minute, minute2);
196 if caps.get(7).is_some() {
197 end_comp.assign(Component::Second, second2);
198 }
199 if let Some(m) = effective_meridiem2 {
200 end_comp.assign(Component::Meridiem, m as i32);
201 } else if adj_hour2 >= 12 {
202 end_comp.assign(Component::Meridiem, Meridiem::PM as i32);
203 }
204
205 use chrono::Datelike;
207 end_comp.imply(Component::Year, ref_date.year());
208 end_comp.imply(Component::Month, ref_date.month() as i32);
209 end_comp.imply(Component::Day, ref_date.day() as i32);
210
211 Some(end_comp)
212 } else {
213 None
214 };
215
216 let actual_start = matched_text
218 .find(|c: char| c.is_ascii_digit() || c == 'd' || c == 'D' || c == 'l' || c == 'L')
219 .unwrap_or(0);
220 let actual_text = &matched_text[actual_start..];
221 let actual_end = actual_text
222 .rfind(|c: char| c.is_ascii_alphanumeric())
223 .map(|i| i + actual_text[i..].chars().next().map_or(1, char::len_utf8))
224 .unwrap_or(actual_text.len());
225
226 results.push(context.create_result(
227 index + actual_start,
228 index + actual_start + actual_end,
229 components,
230 end_components,
231 ));
232
233 start += mat.end();
234 }
235
236 Ok(results)
237 }
238}
239
240impl Default for ESTimeExpressionParser {
241 fn default() -> Self {
242 Self::new()
243 }
244}