alibi_core/utils/datetime/
parse.rs1use crate::utils::javascript::trim as js_trim;
5use chrono::{DateTime, Datelike, Local, NaiveDate, NaiveDateTime, Offset, TimeZone};
6
7#[must_use]
8pub fn parse_date_millis(value: &str) -> Option<i64> {
9 let value = js_trim(value);
10 parse_iso(value).or_else(|| parse_legacy(value))
11}
12
13fn digits(input: &mut &str, width: usize) -> Option<i64> {
14 let prefix = input.get(..width)?;
15 if !prefix.bytes().all(|byte| byte.is_ascii_digit()) {
16 return None;
17 }
18 let value = prefix.parse().ok()?;
19 *input = input.get(width..)?;
20 Some(value)
21}
22
23fn separator(input: &mut &str, expected: char) -> Option<()> {
24 *input = input.strip_prefix(expected)?;
25 Some(())
26}
27
28fn parse_iso(mut input: &str) -> Option<i64> {
29 let year = match input.as_bytes().first() {
30 Some(b'+' | b'-') => {
31 let negative = input.starts_with('-');
32 input = input.get(1..)?;
33 let year = digits(&mut input, 6)?;
34 if negative && year == 0 {
35 return None;
36 }
37 if negative { -year } else { year }
38 }
39 _ => digits(&mut input, 4)?,
40 };
41 let mut month = 1;
42 let mut day = 1;
43 if input.starts_with('-') {
44 separator(&mut input, '-')?;
45 month = digits(&mut input, 2)?;
46 if input.starts_with('-') {
47 separator(&mut input, '-')?;
48 day = digits(&mut input, 2)?;
49 }
50 }
51 if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
52 return None;
53 }
54 let days = civil_days(year, month, day);
55 if input.is_empty() {
56 return time_clip(days.checked_mul(86_400_000)?);
57 }
58 if !input.starts_with(['T', 't', ' ']) {
59 return None;
60 }
61 input = input.get(1..)?;
62 let hour = digits(&mut input, 2)?;
63 separator(&mut input, ':')?;
64 let minute = digits(&mut input, 2)?;
65 let mut second = 0;
66 let mut milliseconds = 0;
67 let mut fractional_nonzero = false;
68 if input.starts_with(':') {
69 separator(&mut input, ':')?;
70 second = digits(&mut input, 2)?;
71 if input.starts_with('.') {
72 separator(&mut input, '.')?;
73 let length = input.bytes().take_while(u8::is_ascii_digit).count();
74 if length == 0 {
75 return None;
76 }
77 fractional_nonzero = input.get(..length)?.bytes().any(|digit| digit != b'0');
78 let first = input.get(..length.min(3))?;
79 milliseconds =
80 first.parse::<i64>().ok()? * 10i64.pow(u32::try_from(3 - first.len()).ok()?);
81 input = input.get(length..)?;
82 }
83 }
84 if hour > 24
85 || minute > 59
86 || second > 59
87 || (hour == 24 && (minute != 0 || second != 0 || fractional_nonzero))
88 {
89 return None;
90 }
91 let nominal = days.checked_mul(86_400_000)?
92 + hour * 3_600_000
93 + minute * 60_000
94 + second * 1_000
95 + milliseconds;
96 if input.is_empty() {
97 return time_clip(local_millis(nominal)?);
98 }
99 if input == "Z" || input == "z" {
100 return time_clip(nominal);
101 }
102 let negative = input.starts_with('-');
103 if !negative && !input.starts_with('+') {
104 return None;
105 }
106 input = input.get(1..)?;
107 let offset_hour = digits(&mut input, 2)?;
108 if input.starts_with(':') {
109 separator(&mut input, ':')?;
110 }
111 let offset_minute = digits(&mut input, 2)?;
112 if !input.is_empty() || offset_hour > 23 || offset_minute > 59 {
113 return None;
114 }
115 let offset = (offset_hour * 60 + offset_minute) * 60_000;
116 time_clip(nominal + if negative { offset } else { -offset })
117}
118
119fn time_clip(milliseconds: i64) -> Option<i64> {
120 (milliseconds.abs() <= 8_640_000_000_000_000).then_some(milliseconds)
121}
122
123fn civil_days(year: i64, month: i64, day: i64) -> i64 {
126 let adjusted_year = year - i64::from(month <= 2);
127 let era = adjusted_year.div_euclid(400);
128 let year_in_era = adjusted_year - era * 400;
129 let march_month = month + if month > 2 { -3 } else { 9 };
130 let day_in_year = (153 * march_month + 2) / 5 + day - 1;
131 let day_in_era = year_in_era * 365 + year_in_era / 4 - year_in_era / 100 + day_in_year;
132 era * 146_097 + day_in_era - 719_468
133}
134
135fn local_millis(nominal: i64) -> Option<i64> {
136 let naive = DateTime::from_timestamp_millis(nominal)?.naive_utc();
137 if let Some(date) = Local.from_local_datetime(&naive).earliest() {
138 return Some(date.timestamp_millis());
139 }
140 let prior = Local
143 .timestamp_millis_opt(nominal.checked_sub(86_400_000)?)
144 .single()?;
145 let offset = i64::from(prior.offset().fix().local_minus_utc()) * 1_000;
146 nominal.checked_sub(offset)
147}
148
149fn parse_legacy(value: &str) -> Option<i64> {
150 let value = remove_comments(value)?;
151 let value = value.trim();
152 let value = strip_weekday(value).trim();
153 if let Ok(date) = DateTime::parse_from_rfc2822(value) {
154 return time_clip(date.timestamp_millis());
155 }
156 let value = value.replace(',', " ");
157 let value = normalize_legacy(&value);
158 let without_gmt = value.replace("GMT+", "+").replace("GMT-", "-");
159 let without_gmt = without_gmt
162 .strip_suffix(" GMT")
163 .or_else(|| without_gmt.strip_suffix(" UTC"))
164 .map_or_else(|| without_gmt.clone(), |prefix| format!("{prefix} +0000"));
165 for format in [
166 "%b %d %Y %H:%M:%S%.f %z",
167 "%d %b %Y %H:%M:%S%.f %z",
168 "%b %d %Y %H:%M:%S%.f GMT",
169 "%d %b %Y %H:%M:%S%.f GMT",
170 "%b %d %Y %H:%M:%S%.f UTC",
171 "%d %b %Y %H:%M:%S%.f UTC",
172 ] {
173 if let Ok(date) = DateTime::parse_from_str(&without_gmt, format) {
174 return time_clip(date.timestamp_millis());
175 }
176 }
177 for format in [
178 "%b %d %Y %H:%M:%S%.f",
179 "%d %b %Y %H:%M:%S%.f",
180 "%m/%d/%Y %H:%M:%S%.f",
181 "%Y/%m/%d %H:%M:%S%.f",
182 "%Y-%m-%d %H:%M:%S%.f",
183 "%b %d %Y %I:%M:%S %p",
184 "%d %b %Y %I:%M:%S %p",
185 ] {
186 if let Ok(date) = NaiveDateTime::parse_from_str(&value, format) {
187 return time_clip(local_millis(date.and_utc().timestamp_millis())?);
188 }
189 }
190 for format in [
191 "%b %d %Y", "%d %b %Y", "%m/%d/%Y", "%Y/%m/%d", "%m-%d-%Y", "%Y-%m-%d",
192 ] {
193 if let Ok(mut date) = NaiveDate::parse_from_str(&value, format) {
194 if (0..=99).contains(&date.year()) {
195 date = date.with_year(if date.year() < 50 {
196 2000 + date.year()
197 } else {
198 1900 + date.year()
199 })?;
200 }
201 return time_clip(local_millis(
202 date.and_hms_opt(0, 0, 0)?.and_utc().timestamp_millis(),
203 )?);
204 }
205 }
206 if !value.is_empty() && value.bytes().all(|byte| byte.is_ascii_digit()) {
207 let number: i64 = value.parse().ok()?;
208 if number > 1_000_000 {
209 return None;
210 }
211 let (year, month) = match number {
212 0 => (2000, 1),
213 1..=12 => (2001, number),
214 13..=31 => return None,
215 32..=49 => (2000 + number, 1),
216 50..=99 => (1900 + number, 1),
217 _ => (number, 1),
218 };
219 return time_clip(civil_days(year, month, 1).checked_mul(86_400_000)?);
220 }
221 None
222}
223
224fn strip_weekday(value: &str) -> &str {
225 let Some(prefix) = value.get(..3) else {
226 return value;
227 };
228 if ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
229 .iter()
230 .any(|weekday| prefix.eq_ignore_ascii_case(weekday))
231 {
232 return value
233 .get(3..)
234 .unwrap_or(value)
235 .trim_start_matches([',', ' ']);
236 }
237 value
238}
239
240fn normalize_legacy(value: &str) -> String {
241 let months = [
242 ("January", "Jan"),
243 ("February", "Feb"),
244 ("March", "Mar"),
245 ("April", "Apr"),
246 ("May", "May"),
247 ("June", "Jun"),
248 ("July", "Jul"),
249 ("August", "Aug"),
250 ("September", "Sep"),
251 ("October", "Oct"),
252 ("November", "Nov"),
253 ("December", "Dec"),
254 ];
255 let mut parts = value
256 .split_whitespace()
257 .map(|part| {
258 months
259 .iter()
260 .find(|(name, _)| part.eq_ignore_ascii_case(name))
261 .map_or(part, |(_, abbreviation)| *abbreviation)
262 })
263 .collect::<Vec<_>>();
264 let zones = [
267 ("UT", "+0000"),
268 ("UTC", "+0000"),
269 ("GMT", "+0000"),
270 ("EST", "-0500"),
271 ("EDT", "-0400"),
272 ("CST", "-0600"),
273 ("CDT", "-0500"),
274 ("MST", "-0700"),
275 ("MDT", "-0600"),
276 ("PST", "-0800"),
277 ("PDT", "-0700"),
278 ];
279 if let Some(last) = parts.last_mut()
280 && let Some((_, offset)) = zones
281 .iter()
282 .find(|(name, _)| last.eq_ignore_ascii_case(name))
283 {
284 *last = offset;
285 }
286 parts.join(" ")
287}
288
289fn remove_comments(value: &str) -> Option<String> {
290 let mut result = String::new();
291 let mut depth = 0usize;
292 for character in value.chars() {
293 match character {
294 '(' => depth = depth.checked_add(1)?,
295 ')' if depth > 0 => depth -= 1,
296 ')' => return None,
297 _ if depth == 0 => result.push(character),
298 _ => {}
299 }
300 }
301 (depth == 0).then_some(result)
302}