whichtime_sys/parsers/fr/
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+)?(?:à\s+|a\s+)?)?(\d{1,2})(?:h(\d{2})?m?|[:\.](\d{2}))(?::(\d{2}))?(?:\s*(a\.?m\.?|p\.?m\.?))?(?:\s*(?:à|a|[\-–~])\s*(\d{1,2})(?:h(\d{2})?m?|[:\.](\d{2}))?(?::(\d{2}))?(?:\s*(a\.?m\.?|p\.?m\.?))?)?(?![\d:a-zA-Z])"
23 ).unwrap()
24});
25
26pub struct FRTimeExpressionParser;
28
29impl FRTimeExpressionParser {
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 FRTimeExpressionParser {
67 fn name(&self) -> &'static str {
68 "FRTimeExpressionParser"
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('h')
76 || text.contains(':')
77 || text.contains('.')
78 || text.to_lowercase().contains("am")
79 || text.to_lowercase().contains("pm"))
80 }
81
82 fn parse(&self, context: &ParsingContext) -> Result<Vec<ParsedResult>> {
83 let mut results = Vec::new();
84 let ref_date = context.reference.instant;
85
86 let mut start = 0;
87 while start < context.text.len() {
88 let search_text = &context.text[start..];
89 let mat = match PRIMARY_PATTERN.find(search_text) {
90 Ok(Some(m)) => m,
91 Ok(None) => break,
92 Err(_) => break,
93 };
94
95 let matched_text = mat.as_str();
96 let index = start + mat.start();
97
98 if index > 0 {
100 let prev_char = context.text.as_bytes().get(index - 1);
101 if let Some(&c) = prev_char
102 && c.is_ascii_digit()
103 {
104 start += mat.end();
105 continue;
106 }
107 }
108
109 let caps = match PRIMARY_PATTERN.captures(matched_text) {
110 Ok(Some(c)) => c,
111 Ok(None) => {
112 start = index + 1;
113 continue;
114 }
115 Err(_) => {
116 start = index + 1;
117 continue;
118 }
119 };
120
121 let hour1: i32 = caps
123 .get(1)
124 .and_then(|m| m.as_str().parse().ok())
125 .unwrap_or(-1);
126
127 if !(0..=23).contains(&hour1) {
128 start += mat.end();
129 continue;
130 }
131
132 let minute1: i32 = caps
134 .get(2)
135 .or(caps.get(3))
136 .and_then(|m| m.as_str().parse().ok())
137 .unwrap_or(0);
138
139 if !(0..=59).contains(&minute1) {
141 start = index + 1;
142 continue;
143 }
144
145 let second1: i32 = caps
146 .get(4)
147 .and_then(|m| m.as_str().parse().ok())
148 .unwrap_or(0);
149
150 if !(0..=59).contains(&second1) {
152 start += mat.end();
153 continue;
154 }
155
156 let meridiem1 = caps
157 .get(5)
158 .map(|m| m.as_str())
159 .and_then(Self::parse_meridiem);
160
161 if meridiem1.is_some() && hour1 > 12 {
164 start += mat.end();
165 continue;
166 }
167 if meridiem1.is_some() && hour1 == 0 {
169 start += mat.end();
170 continue;
171 }
172
173 let has_end_time = caps.get(6).is_some();
175 let hour2: i32 = caps
176 .get(6)
177 .and_then(|m| m.as_str().parse().ok())
178 .unwrap_or(-1);
179 let minute2: i32 = caps
180 .get(7)
181 .or(caps.get(8))
182 .and_then(|m| m.as_str().parse().ok())
183 .unwrap_or(0);
184 let second2: i32 = caps
185 .get(9)
186 .and_then(|m| m.as_str().parse().ok())
187 .unwrap_or(0);
188 let meridiem2 = caps
189 .get(10)
190 .map(|m| m.as_str())
191 .and_then(Self::parse_meridiem);
192
193 let adj_hour1 = Self::adjust_hour(hour1, meridiem1);
195
196 let effective_meridiem2 = meridiem2.or(meridiem1);
198 let adj_hour2 = if has_end_time && hour2 >= 0 {
199 Self::adjust_hour(hour2, effective_meridiem2)
200 } else {
201 0
202 };
203
204 let mut components = context.create_components();
206 components.assign(Component::Hour, adj_hour1);
207 components.assign(Component::Minute, minute1);
208 if caps.get(4).is_some() {
209 components.assign(Component::Second, second1);
210 }
211 if let Some(m) = meridiem1 {
212 components.assign(Component::Meridiem, m as i32);
213 } else if adj_hour1 >= 12 {
214 components.assign(Component::Meridiem, Meridiem::PM as i32);
215 }
216
217 let end_components = if has_end_time && hour2 >= 0 {
219 let mut end_comp = context.create_components();
220 end_comp.assign(Component::Hour, adj_hour2);
221 end_comp.assign(Component::Minute, minute2);
222 if caps.get(9).is_some() {
223 end_comp.assign(Component::Second, second2);
224 }
225 if let Some(m) = effective_meridiem2 {
226 end_comp.assign(Component::Meridiem, m as i32);
227 } else if adj_hour2 >= 12 {
228 end_comp.assign(Component::Meridiem, Meridiem::PM as i32);
229 }
230
231 use chrono::Datelike;
233 end_comp.imply(Component::Year, ref_date.year());
234 end_comp.imply(Component::Month, ref_date.month() as i32);
235 end_comp.imply(Component::Day, ref_date.day() as i32);
236
237 Some(end_comp)
238 } else {
239 None
240 };
241
242 let actual_start = matched_text
244 .find(|c: char| c.is_ascii_digit() || c == 'd' || c == 'D' || c == 'à' || c == 'a')
245 .unwrap_or(0);
246 let actual_text = &matched_text[actual_start..];
247 let actual_end = actual_text
248 .rfind(|c: char| c.is_ascii_alphanumeric())
249 .map(|i| i + actual_text[i..].chars().next().map_or(1, char::len_utf8))
250 .unwrap_or(actual_text.len());
251
252 results.push(context.create_result(
253 index + actual_start,
254 index + actual_start + actual_end,
255 components,
256 end_components,
257 ));
258
259 start += mat.end();
260 }
261
262 Ok(results)
263 }
264}
265
266impl Default for FRTimeExpressionParser {
267 fn default() -> Self {
268 Self::new()
269 }
270}