Skip to main content

qsv_dateparser/
datetime.rs

1#![allow(deprecated)]
2use crate::timezone;
3use anyhow::{Result, anyhow};
4use chrono::prelude::*;
5use regex::Regex;
6
7macro_rules! regex {
8    ($re:literal $(,)?) => {{
9        static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
10        RE.get_or_init(|| {
11            regex::RegexBuilder::new($re)
12                .unicode(false)
13                .build()
14                .expect("invalid regex literal")
15        })
16    }};
17}
18/// Lookup table of bytes that may legally appear in an accepted date format:
19/// ASCII alphanumerics, ASCII whitespace (`\s` under `unicode(false)` =
20/// space, `\t`, `\n`, `\x0B`, `\x0C`, `\r`), and the separators `- + / : . ,`.
21const fn build_date_byte_table() -> [bool; 256] {
22    let mut table = [false; 256];
23    let mut i = 0usize;
24    while i < 256 {
25        let b = i as u8;
26        table[i] = b.is_ascii_alphanumeric()
27            || matches!(b, b' ' | 0x09..=0x0D)
28            || matches!(b, b'-' | b'+' | b'/' | b':' | b'.' | b',');
29        i += 1;
30    }
31    table
32}
33
34static DATE_BYTE: [bool; 256] = build_date_byte_table();
35
36/// Cheap structural pre-filter run before the regex dispatch chain.
37///
38/// Any byte outside [`DATE_BYTE`] (e.g. `_`, `#`, `(`, or any non-ASCII byte)
39/// means the input cannot be a date, so we can bail before running 5-6 failing
40/// regex probes. This is the common, hot case for non-date string columns. It is
41/// intentionally conservative: it rejects nothing that currently parses.
42/// The table collapses the per-byte test to a single load + branch.
43#[inline]
44fn cannot_be_date(input: &str) -> bool {
45    input.bytes().any(|b| !DATE_BYTE[b as usize])
46}
47
48/// Returns true when the year field of a slash-separated date (the digits
49/// after the second `/`) is exactly 2 digits wide. Callers' regexes guarantee
50/// the `d{1,2}/d{1,2}/d{2,4}` shape, but the scan is panic-free regardless.
51///
52/// Used to dispatch between the `%y` and `%Y` chrono format families: `%y`
53/// consumes at most 2 digits, so it always fails on 3-4 digit years, and `%Y`
54/// is never reached on a 2-digit year that `%y` accepts (both families apply
55/// identical date-validity rules), so picking one family by year width is
56/// result-preserving and halves the trial-parse chain.
57#[inline]
58fn slash_year_is_two_digits(bytes: &[u8]) -> bool {
59    let mut slashes = 0u8;
60    let mut year_len = 0usize;
61    for &b in bytes {
62        if b == b'/' {
63            slashes += 1;
64        } else if slashes == 2 {
65            if b.is_ascii_digit() {
66                year_len += 1;
67            } else {
68                break;
69            }
70        }
71    }
72    year_len == 2
73}
74
75/// Parse struct has methods implemented parsers for accepted formats.
76pub struct Parse<'z, Tz2> {
77    tz: &'z Tz2,
78    default_time: NaiveTime,
79    prefer_dmy: bool,
80}
81
82impl<'z, Tz2> Parse<'z, Tz2>
83where
84    Tz2: TimeZone,
85{
86    /// Create a new instance of [`Parse`] with a custom parsing timezone that handles the
87    /// datetime string without time offset.
88    pub const fn new(tz: &'z Tz2, default_time: NaiveTime) -> Self {
89        Self {
90            tz,
91            default_time,
92            prefer_dmy: false,
93        }
94    }
95
96    pub const fn prefer_dmy(&mut self, yes: bool) -> &Self {
97        self.prefer_dmy = yes;
98        self
99    }
100
101    /// Create a new instance of [`Parse`] with a custom parsing timezone that handles the
102    /// datetime string without time offset, and the date parsing preference.
103    pub const fn new_with_preference(
104        tz: &'z Tz2,
105        default_time: NaiveTime,
106        prefer_dmy: bool,
107    ) -> Self {
108        Self {
109            tz,
110            default_time,
111            prefer_dmy,
112        }
113    }
114
115    /// This method tries to parse the input datetime string with a list of accepted formats. See
116    /// more examples from [`Parse`], [`crate::parse()`] and [`crate::parse_with_timezone()`].
117    ///
118    /// Order rationale: the regex-gated families are tried first because their
119    /// `is_match` gate rejects non-matching inputs cheaply. The two parsers
120    /// without a family regex gate — `unix_timestamp` (runs `fast_float2`) and
121    /// `rfc2822` (runs `parse_from_rfc2822`) — are tried last to avoid paying
122    /// their cost on the common ISO/slash dates. Each still applies its own cheap
123    /// byte pre-filter before the heavy parse (`unix_timestamp` checks the first
124    /// byte against the leads `fast_float2` accepts; `rfc2822` requires a `:`).
125    ///
126    /// This reorder is result-preserving:
127    /// - A `fast_float2`-parseable input (a pure finite number; `inf`/`nan` are
128    ///   rejected as non-finite) matches no family gate (they all require `/`,
129    ///   an interior `-`, or a letters+space shape a bare number lacks), so it
130    ///   still reaches `unix_timestamp`.
131    /// - An `rfc2822` input always carries a timezone, which makes the
132    ///   `$`-anchored `month_dmy_*` regexes fail; conversely `month_dmy_*` only
133    ///   succeeds without a timezone, which makes `rfc2822` fail. The two are
134    ///   mutually exclusive, so deferring `rfc2822` cannot change any result.
135    #[inline]
136    pub fn parse(&self, input: &str) -> Result<DateTime<Utc>> {
137        if cannot_be_date(input) {
138            return Err(anyhow!("{} did not match any formats.", input));
139        }
140        self.slash_mdy_family(input)
141            .or_else(|| self.slash_ymd_family(input))
142            .or_else(|| self.ymd_family(input))
143            .or_else(|| self.month_ymd(input))
144            .or_else(|| self.month_mdy_family(input))
145            .or_else(|| self.month_dmy_family(input))
146            .or_else(|| self.unix_timestamp(input))
147            .or_else(|| self.rfc2822(input))
148            .unwrap_or_else(|| Err(anyhow!("{} did not match any formats.", input)))
149    }
150
151    #[inline]
152    fn ymd_family(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
153        let re: &Regex = regex! {
154            r"^\d{4}-\d{2}"
155
156        };
157
158        if !re.is_match(input) {
159            return None;
160        }
161        self.rfc3339(input)
162            .or_else(|| self.ymd_hms(input))
163            .or_else(|| self.ymd_hms_z(input))
164            .or_else(|| self.ymd(input))
165            .or_else(|| self.ymd_z(input))
166    }
167
168    #[inline]
169    fn month_mdy_family(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
170        let re: &Regex = regex! {
171            r"^[a-zA-Z]{3,9}\.?\s+\d{1,2}"
172        };
173
174        if !re.is_match(input) {
175            return None;
176        }
177        self.month_mdy_hms(input)
178            .or_else(|| self.month_mdy_hms_z(input))
179            .or_else(|| self.month_mdy(input))
180    }
181
182    #[inline]
183    fn month_dmy_family(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
184        let re: &Regex = regex! {r"^\d{1,2}\s+[a-zA-Z]{3,9}"
185        };
186
187        if !re.is_match(input) {
188            return None;
189        }
190        self.month_dmy_hms(input).or_else(|| self.month_dmy(input))
191    }
192
193    #[inline]
194    fn slash_mdy_family(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
195        let re: &Regex = regex! {r"^\d{1,2}/\d{1,2}"
196        };
197        if !re.is_match(input) {
198            return None;
199        }
200        if self.prefer_dmy {
201            self.slash_dmy_hms(input)
202                .or_else(|| self.slash_dmy(input))
203                .or_else(|| self.slash_mdy_hms(input))
204                .or_else(|| self.slash_mdy(input))
205        } else {
206            self.slash_mdy_hms(input)
207                .or_else(|| self.slash_mdy(input))
208                .or_else(|| self.slash_dmy_hms(input))
209                .or_else(|| self.slash_dmy(input))
210        }
211    }
212
213    #[inline]
214    fn slash_ymd_family(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
215        let re: &Regex = regex! {r"^[0-9]{4}/[0-9]{1,2}"};
216        if !re.is_match(input) {
217            return None;
218        }
219        self.slash_ymd_hms(input).or_else(|| self.slash_ymd(input))
220    }
221
222    // unix timestamp
223    // - 0
224    // - -770172300
225    // - 1671673426.123456789
226    #[inline]
227    fn unix_timestamp(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
228        // Cheap pre-filter before the heavier float parse: only finite numbers
229        // are accepted, so the first byte must be a digit, sign, or dot
230        // (`fast_float2` rejects leading whitespace and empty input; bare
231        // `inf`/`nan` are excluded here, matching the non-finite check below).
232        // This is the last-resort numeric parser, so most inputs reaching it
233        // are non-numeric.
234        let &b0 = input.as_bytes().first()?;
235        if !(b0.is_ascii_digit() || matches!(b0, b'+' | b'-' | b'.')) {
236            return None;
237        }
238
239        let ts_sec_val: f64 = if let Ok(val) = fast_float2::parse(input) {
240            val
241        } else {
242            return None;
243        };
244
245        // Reject non-finite values (`+inf`, `-nan`, … pass the lead-byte filter
246        // above): the `as i64` cast below would otherwise turn `nan` into 0
247        // (1970-01-01) and `inf` into i64::MAX nanos (2262-04-11) — never the
248        // intended reading of the input.
249        if !ts_sec_val.is_finite() {
250            return None;
251        }
252
253        // convert the timestamp seconds value to nanoseconds
254        let ts_ns_val = ts_sec_val * 1_000_000_000_f64;
255
256        let result = Utc.timestamp_nanos(ts_ns_val as i64).with_timezone(&Utc);
257        Some(Ok(result))
258    }
259
260    // rfc3339
261    // - 2021-05-01T01:17:02.604456Z
262    // - 2017-11-25T22:34:50Z
263    #[inline]
264    fn rfc3339(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
265        DateTime::parse_from_rfc3339(input)
266            .ok()
267            .map(|parsed| parsed.with_timezone(&Utc))
268            .map(Ok)
269    }
270
271    // rfc2822
272    // - Wed, 02 Jun 2021 06:31:39 GMT
273    #[inline]
274    fn rfc2822(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
275        // Fast pre-filter: every RFC2822 datetime carries a time-of-day
276        // (`hour ":" minute`), so it always contains ':'. Skip the
277        // `parse_from_rfc2822` attempt for colon-free inputs. This is the
278        // last-resort parser, so most inputs reaching it are non-rfc2822.
279        if !input.as_bytes().contains(&b':') {
280            return None;
281        }
282        DateTime::parse_from_rfc2822(input)
283            .ok()
284            .map(|parsed| parsed.with_timezone(&Utc))
285            .map(Ok)
286    }
287
288    // yyyy-mm-dd hh:mm:ss  (separator is space OR ISO 8601 'T')
289    // - 2014-04-26 05:24:37 PM
290    // - 2021-04-30 21:14
291    // - 2021-04-30 21:14:10
292    // - 2021-04-30 21:14:10.052282
293    // - 2014-04-26 17:24:37.123
294    // - 2014-04-26 17:24:37.3186369
295    // - 2012-08-03 18:31:59.257000000
296    // - 2020-01-15T08:00
297    // - 2020-01-15T08:00:00
298    // - 2020-01-15T08:00:00.123456
299    #[inline]
300    fn ymd_hms(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
301        let re: &Regex = regex! {
302                r"^\d{4}-\d{2}-\d{2}[T\s]+\d{2}:\d{2}(?::\d{2})?(?:\.\d{1,9})?\s*(?:am|pm|AM|PM)?$"
303
304        };
305        if !re.is_match(input) {
306            return None;
307        }
308
309        // Byte 10 is the date/time separator. The regex guarantees the input
310        // has at least 16 bytes and that byte 10 is either 'T' or ASCII
311        // whitespace, so picking the format-string family on this single byte
312        // avoids doubling the trial-parse chain for the common space case.
313        let (fmt_hms, fmt_hm, fmt_hms_f, fmt_ims_p, fmt_im_p) = if input.as_bytes()[10] == b'T' {
314            (
315                "%Y-%m-%dT%H:%M:%S",
316                "%Y-%m-%dT%H:%M",
317                "%Y-%m-%dT%H:%M:%S%.f",
318                "%Y-%m-%dT%I:%M:%S %P",
319                "%Y-%m-%dT%I:%M %P",
320            )
321        } else {
322            (
323                "%Y-%m-%d %H:%M:%S",
324                "%Y-%m-%d %H:%M",
325                "%Y-%m-%d %H:%M:%S%.f",
326                "%Y-%m-%d %I:%M:%S %P",
327                "%Y-%m-%d %I:%M %P",
328            )
329        };
330
331        self.tz
332            .datetime_from_str(input, fmt_hms)
333            .or_else(|_| self.tz.datetime_from_str(input, fmt_hm))
334            .or_else(|_| self.tz.datetime_from_str(input, fmt_hms_f))
335            .or_else(|_| self.tz.datetime_from_str(input, fmt_ims_p))
336            .or_else(|_| self.tz.datetime_from_str(input, fmt_im_p))
337            .ok()
338            .map(|parsed| parsed.with_timezone(&Utc))
339            .map(Ok)
340    }
341
342    // yyyy-mm-dd hh:mm:ss z
343    // - 2017-11-25 13:31:15 PST
344    // - 2017-11-25 13:31 PST
345    // - 2014-12-16 06:20:00 UTC
346    // - 2014-12-16 06:20:00 GMT
347    // - 2014-04-26 13:13:43 +0800
348    // - 2014-04-26 13:13:44 +09:00
349    // - 2012-08-03 18:31:59.257000000 +0000
350    // - 2015-09-30 18:48:56.35272715 UTC
351    #[inline]
352    fn ymd_hms_z(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
353        // Fast pre-filter: bare dates "YYYY-MM-DD" are 10 chars; valid inputs need space + time
354        if input.len() < 17 || !input.as_bytes()[10].is_ascii_whitespace() {
355            return None;
356        }
357        let re: &Regex = regex! {
358                r"^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}(?::\d{2})?(?:\.\d{1,9})?(?P<tz>\s*[+-:a-zA-Z0-9]{3,6})$"
359        };
360
361        if let Some(caps) = re.captures(input)
362            && let Some(matched_tz) = caps.name("tz")
363        {
364            let parse_from_str = NaiveDateTime::parse_from_str;
365            return match timezone::parse(matched_tz.as_str().trim()) {
366                Ok(offset) => parse_from_str(input, "%Y-%m-%d %H:%M:%S %Z")
367                    .or_else(|_| parse_from_str(input, "%Y-%m-%d %H:%M %Z"))
368                    .or_else(|_| parse_from_str(input, "%Y-%m-%d %H:%M:%S%.f %Z"))
369                    .ok()
370                    .and_then(|parsed| offset.from_local_datetime(&parsed).single())
371                    .map(|datetime| datetime.with_timezone(&Utc))
372                    .map(Ok),
373                Err(err) => Some(Err(err)),
374            };
375        }
376        None
377    }
378
379    // yyyy-mm-dd
380    // - 2021-02-21
381    #[inline]
382    fn ymd(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
383        let re: &Regex = regex! {r"^\d{4}-\d{2}-\d{2}$"
384        };
385
386        if !re.is_match(input) {
387            return None;
388        }
389        let now = Utc::now()
390            .date()
391            .and_time(self.default_time)?
392            .with_timezone(self.tz);
393        NaiveDate::parse_from_str(input, "%Y-%m-%d")
394            .ok()
395            .map(|parsed| parsed.and_time(now.time()))
396            .and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
397            .map(|at_tz| at_tz.with_timezone(&Utc))
398            .map(Ok)
399    }
400
401    // yyyy-mm-dd z
402    // - 2021-02-21 PST
403    // - 2021-02-21 UTC
404    // - 2020-07-20+08:00 (yyyy-mm-dd-07:00)
405    #[inline]
406    fn ymd_z(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
407        // Fast pre-filter: bare date "YYYY-MM-DD" is exactly 10 chars; timezone appended = longer
408        if input.len() <= 10 {
409            return None;
410        }
411        let re: &Regex = regex! {r"^\d{4}-\d{2}-\d{2}(?P<tz>\s*[+-:a-zA-Z0-9]{3,6})$"
412        };
413        if let Some(caps) = re.captures(input)
414            && let Some(matched_tz) = caps.name("tz")
415        {
416            return match timezone::parse(matched_tz.as_str().trim()) {
417                Ok(offset) => {
418                    let now = Utc::now()
419                        .date()
420                        .and_time(self.default_time)?
421                        .with_timezone(&offset);
422                    NaiveDate::parse_from_str(input, "%Y-%m-%d %Z")
423                        .ok()
424                        .map(|parsed| parsed.and_time(now.time()))
425                        .and_then(|datetime| offset.from_local_datetime(&datetime).single())
426                        .map(|at_tz| at_tz.with_timezone(&Utc))
427                        .map(Ok)
428                }
429                Err(err) => Some(Err(err)),
430            };
431        }
432        None
433    }
434
435    // yyyy-mon-dd
436    // - 2021-Feb-21
437    #[inline]
438    fn month_ymd(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
439        let re: &Regex = regex! {r"^\d{4}-\w{3,9}-\d{2}$"
440        };
441        if !re.is_match(input) {
442            return None;
443        }
444
445        let now = Utc::now()
446            .date()
447            .and_time(self.default_time)?
448            .with_timezone(self.tz);
449        NaiveDate::parse_from_str(input, "%Y-%m-%d")
450            .or_else(|_| NaiveDate::parse_from_str(input, "%Y-%b-%d"))
451            .ok()
452            .map(|parsed| parsed.and_time(now.time()))
453            .and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
454            .map(|at_tz| at_tz.with_timezone(&Utc))
455            .map(Ok)
456    }
457
458    // Mon dd, yyyy, hh:mm:ss
459    // - May 8, 2009 5:57:51 PM
460    // - September 17, 2012 10:09am
461    // - September 17, 2012, 10:10:09
462    #[inline]
463    fn month_mdy_hms(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
464        let re: &Regex = regex! {
465                r"^[a-zA-Z]{3,9}\.?\s+\d{1,2},\s+\d{2,4},?\s+\d{1,2}:\d{2}(?::\d{2})?\s*(?:am|pm|AM|PM)?$"
466        };
467        if !re.is_match(input) {
468            return None;
469        }
470
471        // The regex above enforces \s+ after any comma or period, so removing bare ',' or '.'
472        // is equivalent to the previous `replace(", ", " ").replace(". ", " ")` for all
473        // inputs that reach this point — marginally-malformed inputs (e.g. "May 27,2012 …")
474        // still fail to parse after stripping because the digits run together.
475        let dt = input.replace([',', '.'], "");
476        self.tz
477            .datetime_from_str(&dt, "%B %d %Y %H:%M:%S")
478            .or_else(|_| self.tz.datetime_from_str(&dt, "%B %d %Y %H:%M"))
479            .or_else(|_| self.tz.datetime_from_str(&dt, "%B %d %Y %I:%M:%S %P"))
480            .or_else(|_| self.tz.datetime_from_str(&dt, "%B %d %Y %I:%M %P"))
481            .ok()
482            .map(|at_tz| at_tz.with_timezone(&Utc))
483            .map(Ok)
484    }
485
486    // Mon dd, yyyy hh:mm:ss z
487    // - May 02, 2021 15:51:31 UTC
488    // - May 02, 2021 15:51 UTC
489    // - May 26, 2021, 12:49 AM PDT
490    // - September 17, 2012 at 10:09am PST
491    #[inline]
492    fn month_mdy_hms_z(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
493        // Fast pre-filter: must contain an isolated 4-digit year — eliminates "May 27 02:45:27".
494        // Skip the O(n) scan entirely for inputs too short to hold a valid month+day+year+time+tz.
495        if input.len() < 20 {
496            return None;
497        }
498        let bytes = input.as_bytes();
499        let has_year = (0..bytes.len().saturating_sub(3)).any(|i| {
500            bytes[i..i + 4].iter().all(|b| b.is_ascii_digit())
501                && (i == 0 || !bytes[i - 1].is_ascii_digit())
502                && bytes.get(i + 4).is_none_or(|b| !b.is_ascii_digit())
503        });
504        if !has_year {
505            return None;
506        }
507        let re: &Regex = regex! {
508                r"^[a-zA-Z]{3,9}\s+\d{1,2},?\s+\d{4}\s*,?(?:at)?\s+\d{2}:\d{2}(?::\d{2})?\s*(?:am|pm|AM|PM)?(?P<tz>\s+[+-:a-zA-Z0-9]{3,6})$",
509        };
510        if let Some(caps) = re.captures(input)
511            && let Some(matched_tz) = caps.name("tz")
512        {
513            let parse_from_str = NaiveDateTime::parse_from_str;
514            return match timezone::parse(matched_tz.as_str().trim()) {
515                Ok(offset) => {
516                    let mut dt = input.replace(',', "");
517                    if let Some(pos) = dt.find("at") {
518                        dt.replace_range(pos..pos + 2, "");
519                    }
520                    parse_from_str(&dt, "%B %d %Y %H:%M:%S %Z")
521                        .or_else(|_| parse_from_str(&dt, "%B %d %Y %H:%M %Z"))
522                        .or_else(|_| parse_from_str(&dt, "%B %d %Y %I:%M:%S %P %Z"))
523                        .or_else(|_| parse_from_str(&dt, "%B %d %Y %I:%M %P %Z"))
524                        .ok()
525                        .and_then(|parsed| offset.from_local_datetime(&parsed).single())
526                        .map(|datetime| datetime.with_timezone(&Utc))
527                        .map(Ok)
528                }
529                Err(err) => Some(Err(err)),
530            };
531        }
532        None
533    }
534
535    // Mon dd, yyyy
536    // - May 25, 2021
537    // - oct 7, 1970
538    // - oct 7, 70
539    // - oct. 7, 1970
540    // - oct. 7, 70
541    // - October 7, 1970
542    #[inline]
543    fn month_mdy(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
544        let re: &Regex = regex! {r"^[a-zA-Z]{3,9}\.?\s+\d{1,2},\s+\d{2,4}$"
545        };
546        if !re.is_match(input) {
547            return None;
548        }
549
550        let now = Utc::now()
551            .date()
552            .and_time(self.default_time)?
553            .with_timezone(self.tz);
554        // The regex above enforces \s+ after any comma or period, so removing bare ',' or '.'
555        // is equivalent to the previous `replace(", ", " ").replace(". ", " ")` for all
556        // inputs that reach this point.
557        let dt = input.replace([',', '.'], "");
558        NaiveDate::parse_from_str(&dt, "%B %d %y")
559            .or_else(|_| NaiveDate::parse_from_str(&dt, "%B %d %Y"))
560            .ok()
561            .map(|parsed| parsed.and_time(now.time()))
562            .and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
563            .map(|at_tz| at_tz.with_timezone(&Utc))
564            .map(Ok)
565    }
566
567    // dd Mon yyyy hh:mm:ss
568    // - 12 Feb 2006, 19:17
569    // - 12 Feb 2006 19:17
570    // - 14 May 2019 19:11:40.164
571    #[inline]
572    fn month_dmy_hms(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
573        // Fast pre-filter: time component always contains ':', skip regex for date-only inputs.
574        if !input.as_bytes().contains(&b':') {
575            return None;
576        }
577        let re: &Regex = regex! {
578                r"^\d{1,2}\s+[a-zA-Z]{3,9}\s+\d{2,4},?\s+\d{1,2}:[0-9]{2}(?::[0-9]{2})?(?:\.[0-9]{1,9})?$"
579        };
580        if !re.is_match(input) {
581            return None;
582        }
583
584        let dt = input.replace(',', "");
585        self.tz
586            .datetime_from_str(&dt, "%d %B %Y %H:%M:%S")
587            .or_else(|_| self.tz.datetime_from_str(&dt, "%d %B %Y %H:%M"))
588            .or_else(|_| self.tz.datetime_from_str(&dt, "%d %B %Y %H:%M:%S%.f"))
589            .or_else(|_| self.tz.datetime_from_str(&dt, "%d %B %Y %I:%M:%S %P"))
590            .or_else(|_| self.tz.datetime_from_str(&dt, "%d %B %Y %I:%M %P"))
591            .ok()
592            .map(|at_tz| at_tz.with_timezone(&Utc))
593            .map(Ok)
594    }
595
596    // dd Mon yyyy
597    // - 7 oct 70
598    // - 7 oct 1970
599    // - 03 February 2013
600    // - 1 July 2013
601    #[inline]
602    fn month_dmy(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
603        let re: &Regex = regex! {r"^\d{1,2}\s+[a-zA-Z]{3,9}\s+\d{2,4}$"
604        };
605        if !re.is_match(input) {
606            return None;
607        }
608
609        let now = Utc::now()
610            .date()
611            .and_time(self.default_time)?
612            .with_timezone(self.tz);
613        // Fast path: if the last 4 bytes are all digits and preceded by a space, it's a
614        // 4-digit year — skip the always-failing %d %B %y (2-digit year) attempt.
615        let bytes = input.as_bytes();
616        let len = bytes.len();
617        let four_digit_year = len >= 5
618            && bytes[len - 4..].iter().all(|b| b.is_ascii_digit())
619            && bytes[len - 5].is_ascii_whitespace();
620        let parsed = if four_digit_year {
621            NaiveDate::parse_from_str(input, "%d %B %Y")
622        } else {
623            NaiveDate::parse_from_str(input, "%d %B %y")
624                .or_else(|_| NaiveDate::parse_from_str(input, "%d %B %Y"))
625        };
626        parsed
627            .ok()
628            .map(|parsed| parsed.and_time(now.time()))
629            .and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
630            .map(|at_tz| at_tz.with_timezone(&Utc))
631            .map(Ok)
632    }
633
634    // mm/dd/yyyy hh:mm:ss
635    // - 4/8/2014 22:05
636    // - 04/08/2014 22:05
637    // - 4/8/14 22:05
638    // - 04/2/2014 03:00:51
639    // - 8/8/1965 12:00:00 AM
640    // - 8/8/1965 01:00:01 PM
641    // - 8/8/1965 01:00 PM
642    // - 8/8/1965 1:00 PM
643    // - 8/8/1965 12:00 AM
644    // - 4/02/2014 03:00:51
645    // - 03/19/2012 10:11:59
646    // - 03/19/2012 10:11:59.3186369
647    #[inline]
648    fn slash_mdy_hms(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
649        let re: &Regex = regex! {
650                r"^\d{1,2}/\d{1,2}/\d{2,4}\s+\d{1,2}:\d{2}(?::\d{2})?(?:\.\d{1,9})?\s*(?:am|pm|AM|PM)?$"
651        };
652        if !re.is_match(input) {
653            return None;
654        }
655
656        // Dispatch on year width (see slash_year_is_two_digits) instead of
657        // trying all 10 formats: 4-digit years previously burned 5 guaranteed-
658        // failing %y attempts before the first %Y one could succeed.
659        let (fmt_hms, fmt_hm, fmt_hms_f, fmt_ims_p, fmt_im_p) =
660            if slash_year_is_two_digits(input.as_bytes()) {
661                (
662                    "%m/%d/%y %H:%M:%S",
663                    "%m/%d/%y %H:%M",
664                    "%m/%d/%y %H:%M:%S%.f",
665                    "%m/%d/%y %I:%M:%S %P",
666                    "%m/%d/%y %I:%M %P",
667                )
668            } else {
669                (
670                    "%m/%d/%Y %H:%M:%S",
671                    "%m/%d/%Y %H:%M",
672                    "%m/%d/%Y %H:%M:%S%.f",
673                    "%m/%d/%Y %I:%M:%S %P",
674                    "%m/%d/%Y %I:%M %P",
675                )
676            };
677        self.tz
678            .datetime_from_str(input, fmt_hms)
679            .or_else(|_| self.tz.datetime_from_str(input, fmt_hm))
680            .or_else(|_| self.tz.datetime_from_str(input, fmt_hms_f))
681            .or_else(|_| self.tz.datetime_from_str(input, fmt_ims_p))
682            .or_else(|_| self.tz.datetime_from_str(input, fmt_im_p))
683            .ok()
684            .map(|at_tz| at_tz.with_timezone(&Utc))
685            .map(Ok)
686    }
687
688    // dd/mm/yyyy hh:mm:ss
689    // - 8/4/2014 22:05
690    // - 08/04/2014 22:05
691    // - 8/4/14 22:05
692    // - 2/04/2014 03:00:51
693    // - 8/8/1965 12:00:00 AM
694    // - 8/8/1965 01:00:01 PM
695    // - 8/8/1965 01:00 PM
696    // - 8/8/1965 1:00 PM
697    // - 8/8/1965 12:00 AM
698    // - 02/4/2014 03:00:51
699    // - 19/03/2012 10:11:59
700    // - 19/03/2012 10:11:59.3186369
701    #[inline]
702    fn slash_dmy_hms(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
703        let re: &Regex = regex! {
704                r"^\d{1,2}/\d{1,2}/\d{2,4}\s+\d{1,2}:\d{2}(?::\d{2})?(?:\.\d{1,9})?\s*(?:am|pm|AM|PM)?$"
705        };
706        if !re.is_match(input) {
707            return None;
708        }
709
710        // Dispatch on year width — see the twin comment in slash_mdy_hms.
711        let (fmt_hms, fmt_hm, fmt_hms_f, fmt_ims_p, fmt_im_p) =
712            if slash_year_is_two_digits(input.as_bytes()) {
713                (
714                    "%d/%m/%y %H:%M:%S",
715                    "%d/%m/%y %H:%M",
716                    "%d/%m/%y %H:%M:%S%.f",
717                    "%d/%m/%y %I:%M:%S %P",
718                    "%d/%m/%y %I:%M %P",
719                )
720            } else {
721                (
722                    "%d/%m/%Y %H:%M:%S",
723                    "%d/%m/%Y %H:%M",
724                    "%d/%m/%Y %H:%M:%S%.f",
725                    "%d/%m/%Y %I:%M:%S %P",
726                    "%d/%m/%Y %I:%M %P",
727                )
728            };
729        self.tz
730            .datetime_from_str(input, fmt_hms)
731            .or_else(|_| self.tz.datetime_from_str(input, fmt_hm))
732            .or_else(|_| self.tz.datetime_from_str(input, fmt_hms_f))
733            .or_else(|_| self.tz.datetime_from_str(input, fmt_ims_p))
734            .or_else(|_| self.tz.datetime_from_str(input, fmt_im_p))
735            .ok()
736            .map(|at_tz| at_tz.with_timezone(&Utc))
737            .map(Ok)
738    }
739
740    // mm/dd/yyyy
741    // - 3/31/2014
742    // - 03/31/2014
743    // - 08/21/71
744    // - 8/1/71
745    #[inline]
746    fn slash_mdy(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
747        let re: &Regex = regex! {r"^\d{1,2}/\d{1,2}/\d{2,4}$"
748        };
749        if !re.is_match(input) {
750            return None;
751        }
752
753        let now = Utc::now()
754            .date()
755            .and_time(self.default_time)?
756            .with_timezone(self.tz);
757        let fmt = if slash_year_is_two_digits(input.as_bytes()) {
758            "%m/%d/%y"
759        } else {
760            "%m/%d/%Y"
761        };
762        NaiveDate::parse_from_str(input, fmt)
763            .ok()
764            .map(|parsed| parsed.and_time(now.time()))
765            .and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
766            .map(|at_tz| at_tz.with_timezone(&Utc))
767            .map(Ok)
768    }
769
770    // dd/mm/yyyy
771    // - 31/3/2014
772    // - 31/03/2014
773    // - 21/08/71
774    // - 1/8/71
775    #[inline]
776    fn slash_dmy(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
777        let re: &Regex = regex! {r"^[0-9]{1,2}/[0-9]{1,2}/[0-9]{2,4}$"
778        };
779        if !re.is_match(input) {
780            return None;
781        }
782
783        let now = Utc::now()
784            .date()
785            .and_time(self.default_time)?
786            .with_timezone(self.tz);
787        let fmt = if slash_year_is_two_digits(input.as_bytes()) {
788            "%d/%m/%y"
789        } else {
790            "%d/%m/%Y"
791        };
792        NaiveDate::parse_from_str(input, fmt)
793            .ok()
794            .map(|parsed| parsed.and_time(now.time()))
795            .and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
796            .map(|at_tz| at_tz.with_timezone(&Utc))
797            .map(Ok)
798    }
799
800    // yyyy/mm/dd hh:mm:ss
801    // - 2014/4/8 22:05
802    // - 2014/04/08 22:05
803    // - 2014/04/2 03:00:51
804    // - 2014/4/02 03:00:51
805    // - 2012/03/19 10:11:59
806    // - 2012/03/19 10:11:59.3186369
807    #[inline]
808    fn slash_ymd_hms(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
809        let re: &Regex = regex! {
810                r"^[0-9]{4}/[0-9]{1,2}/[0-9]{1,2}\s+[0-9]{1,2}:[0-9]{2}(?::[0-9]{2})?(?:\.[0-9]{1,9})?\s*(?:am|pm|AM|PM)?$"
811        };
812        if !re.is_match(input) {
813            return None;
814        }
815
816        self.tz
817            .datetime_from_str(input, "%Y/%m/%d %H:%M:%S")
818            .or_else(|_| self.tz.datetime_from_str(input, "%Y/%m/%d %H:%M"))
819            .or_else(|_| self.tz.datetime_from_str(input, "%Y/%m/%d %H:%M:%S%.f"))
820            .or_else(|_| self.tz.datetime_from_str(input, "%Y/%m/%d %I:%M:%S %P"))
821            .or_else(|_| self.tz.datetime_from_str(input, "%Y/%m/%d %I:%M %P"))
822            .ok()
823            .map(|at_tz| at_tz.with_timezone(&Utc))
824            .map(Ok)
825    }
826
827    // yyyy/mm/dd
828    // - 2014/3/31
829    // - 2014/03/31
830    #[inline]
831    fn slash_ymd(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
832        let re: &Regex = regex! {r"^[0-9]{4}/[0-9]{1,2}/[0-9]{1,2}$"
833        };
834        if !re.is_match(input) {
835            return None;
836        }
837
838        let now = Utc::now()
839            .date()
840            .and_time(self.default_time)?
841            .with_timezone(self.tz);
842        NaiveDate::parse_from_str(input, "%Y/%m/%d")
843            .ok()
844            .map(|parsed| parsed.and_time(now.time()))
845            .and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
846            .map(|at_tz| at_tz.with_timezone(&Utc))
847            .map(Ok)
848    }
849}
850
851#[cfg(test)]
852mod tests {
853    use super::*;
854
855    #[test]
856    fn unix_timestamp() {
857        let parse = Parse::new(&Utc, Utc::now().time());
858
859        let test_cases = vec![
860            ("0", Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)),
861            ("0000000000", Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)),
862            ("0000000000000", Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)),
863            ("0000000000000000000", Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)),
864            ("-770172300", Utc.ymd(1945, 8, 5).and_hms(23, 15, 0)),
865            (
866                "1671673426.123456789",
867                Utc.ymd(2022, 12, 22).and_hms_nano(1, 43, 46, 123456768),
868            ),
869            ("1511648546", Utc.ymd(2017, 11, 25).and_hms(22, 22, 26)),
870            (
871                "1620036248.420",
872                Utc.ymd(2021, 5, 3).and_hms_milli(10, 4, 8, 420),
873            ),
874            (
875                "1620036248.717915136",
876                Utc.ymd(2021, 5, 3).and_hms_nano(10, 4, 8, 717915136),
877            ),
878        ];
879
880        for &(input, want) in test_cases.iter() {
881            assert_eq!(
882                parse.unix_timestamp(input).unwrap().unwrap(),
883                want,
884                "unix_timestamp/{}",
885                input
886            )
887        }
888        assert!(parse.unix_timestamp("15116").is_some());
889        assert!(
890            parse
891                .unix_timestamp("16200248727179150001620024872717915000") //DevSkim: ignore DS173237
892                .is_some()
893        );
894        assert!(parse.unix_timestamp("not-a-ts").is_none());
895        // Non-finite floats must be rejected, whether caught by the lead-byte
896        // pre-filter (bare `inf`/`nan`) or the is_finite check (signed forms).
897        for input in [
898            "inf", "nan", "INF", "NaN", "infinity", "+inf", "-inf", "-nan",
899        ] {
900            assert!(
901                parse.unix_timestamp(input).is_none(),
902                "unix_timestamp must reject non-finite {input}"
903            );
904        }
905    }
906
907    #[test]
908    fn rfc3339() {
909        let parse = Parse::new(&Utc, Utc::now().time());
910
911        let test_cases = [
912            (
913                "2021-05-01T01:17:02.604456Z",
914                Utc.ymd(2021, 5, 1).and_hms_nano(1, 17, 2, 604456000),
915            ),
916            (
917                "2017-11-25T22:34:50Z",
918                Utc.ymd(2017, 11, 25).and_hms(22, 34, 50),
919            ),
920        ];
921
922        for &(input, want) in test_cases.iter() {
923            assert_eq!(
924                parse.rfc3339(input).unwrap().unwrap(),
925                want,
926                "rfc3339/{}",
927                input
928            )
929        }
930        assert!(parse.rfc3339("2017-11-25 22:34:50").is_none());
931        assert!(parse.rfc3339("not-date-time").is_none());
932    }
933
934    #[test]
935    fn rfc2822() {
936        let parse = Parse::new(&Utc, Utc::now().time());
937
938        let test_cases = [
939            (
940                "Wed, 02 Jun 2021 06:31:39 GMT",
941                Utc.ymd(2021, 6, 2).and_hms(6, 31, 39),
942            ),
943            (
944                "Wed, 02 Jun 2021 06:31:39 PDT",
945                Utc.ymd(2021, 6, 2).and_hms(13, 31, 39),
946            ),
947        ];
948
949        for &(input, want) in test_cases.iter() {
950            assert_eq!(
951                parse.rfc2822(input).unwrap().unwrap(),
952                want,
953                "rfc2822/{}",
954                input
955            )
956        }
957        assert!(parse.rfc2822("02 Jun 2021 06:31:39").is_none());
958        assert!(parse.rfc2822("not-date-time").is_none());
959    }
960
961    #[test]
962    fn ymd_hms() {
963        let parse = Parse::new(&Utc, Utc::now().time());
964
965        let test_cases = [
966            ("2021-04-30 21:14", Utc.ymd(2021, 4, 30).and_hms(21, 14, 0)),
967            (
968                "2021-04-30 21:14:10",
969                Utc.ymd(2021, 4, 30).and_hms(21, 14, 10),
970            ),
971            (
972                "2021-04-30 21:14:10.052282",
973                Utc.ymd(2021, 4, 30).and_hms_micro(21, 14, 10, 52282),
974            ),
975            (
976                "2014-04-26 05:24:37 PM",
977                Utc.ymd(2014, 4, 26).and_hms(17, 24, 37),
978            ),
979            (
980                "2014-04-26 17:24:37.123",
981                Utc.ymd(2014, 4, 26).and_hms_milli(17, 24, 37, 123),
982            ),
983            (
984                "2014-04-26 17:24:37.3186369",
985                Utc.ymd(2014, 4, 26).and_hms_nano(17, 24, 37, 318636900),
986            ),
987            (
988                "2012-08-03 18:31:59.257000000",
989                Utc.ymd(2012, 8, 3).and_hms_nano(18, 31, 59, 257000000),
990            ),
991            // ISO 8601 with 'T' separator and no timezone (naive wall-clock).
992            // Must agree with the space-separated form on the same wall-clock instant.
993            ("2020-01-15T08:00", Utc.ymd(2020, 1, 15).and_hms(8, 0, 0)),
994            ("2020-01-15T08:00:00", Utc.ymd(2020, 1, 15).and_hms(8, 0, 0)),
995            (
996                "2020-01-15T08:00:00.123",
997                Utc.ymd(2020, 1, 15).and_hms_milli(8, 0, 0, 123),
998            ),
999            (
1000                "2020-01-15T08:00:00.123456",
1001                Utc.ymd(2020, 1, 15).and_hms_micro(8, 0, 0, 123456),
1002            ),
1003            (
1004                "2020-01-15T08:00:00.123456789",
1005                Utc.ymd(2020, 1, 15).and_hms_nano(8, 0, 0, 123456789),
1006            ),
1007        ];
1008
1009        for &(input, want) in test_cases.iter() {
1010            assert_eq!(
1011                parse.ymd_hms(input).unwrap().unwrap(),
1012                want,
1013                "ymd_hms/{}",
1014                input
1015            )
1016        }
1017        assert!(parse.ymd_hms("not-date-time").is_none());
1018
1019        // T and space separators must produce the same instant.
1020        let t_form = parse.ymd_hms("2020-01-15T08:00:00").unwrap().unwrap();
1021        let space_form = parse.ymd_hms("2020-01-15 08:00:00").unwrap().unwrap();
1022        assert_eq!(t_form, space_form, "T-separator vs space disagree");
1023    }
1024
1025    #[test]
1026    fn ymd_hms_z() {
1027        let parse = Parse::new(&Utc, Utc::now().time());
1028
1029        let test_cases = [
1030            (
1031                "2017-11-25 13:31:15 PST",
1032                Utc.ymd(2017, 11, 25).and_hms(21, 31, 15),
1033            ),
1034            (
1035                "2017-11-25 13:31 PST",
1036                Utc.ymd(2017, 11, 25).and_hms(21, 31, 0),
1037            ),
1038            (
1039                "2014-12-16 06:20:00 UTC",
1040                Utc.ymd(2014, 12, 16).and_hms(6, 20, 0),
1041            ),
1042            (
1043                "2014-12-16 06:20:00 GMT",
1044                Utc.ymd(2014, 12, 16).and_hms(6, 20, 0),
1045            ),
1046            (
1047                "2014-04-26 13:13:43 +0800",
1048                Utc.ymd(2014, 4, 26).and_hms(5, 13, 43),
1049            ),
1050            (
1051                "2014-04-26 13:13:44 +09:00",
1052                Utc.ymd(2014, 4, 26).and_hms(4, 13, 44),
1053            ),
1054            (
1055                "2012-08-03 18:31:59.257000000 +0000",
1056                Utc.ymd(2012, 8, 3).and_hms_nano(18, 31, 59, 257000000),
1057            ),
1058            (
1059                "2015-09-30 18:48:56.35272715 UTC",
1060                Utc.ymd(2015, 9, 30).and_hms_nano(18, 48, 56, 352727150),
1061            ),
1062        ];
1063
1064        for &(input, want) in test_cases.iter() {
1065            assert_eq!(
1066                parse.ymd_hms_z(input).unwrap().unwrap(),
1067                want,
1068                "ymd_hms_z/{}",
1069                input
1070            )
1071        }
1072        assert!(parse.ymd_hms_z("not-date-time").is_none());
1073        // Pre-filter boundary: exactly 16 chars is rejected by length guard (< 17)
1074        assert!(parse.ymd_hms_z("2021-04-30 21:14").is_none()); // 16 chars, rejected by length guard
1075        // 17 chars but byte[10] is not whitespace — rejected by whitespace check
1076        assert!(parse.ymd_hms_z("2021-04-30X21:14Z").is_none()); // 17 chars, byte[10]='X' not space
1077        // 17 chars with whitespace at byte[10] proceeds to regex but regex rejects malformed input
1078        assert!(parse.ymd_hms_z("2021-04-30 21:1XZ").is_none()); // 17 chars, byte[10]=' ', regex rejects
1079    }
1080
1081    #[test]
1082    fn ymd() {
1083        let parse = Parse::new(&Utc, Utc::now().time());
1084
1085        let test_cases = [(
1086            "2021-02-21",
1087            Utc.ymd(2021, 2, 21).and_time(Utc::now().time()),
1088        )];
1089
1090        for &(input, want) in test_cases.iter() {
1091            assert_eq!(
1092                parse
1093                    .ymd(input)
1094                    .unwrap()
1095                    .unwrap()
1096                    .trunc_subsecs(0)
1097                    .with_second(0)
1098                    .unwrap(),
1099                want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
1100                "ymd/{}",
1101                input
1102            )
1103        }
1104        assert!(parse.ymd("not-date-time").is_none());
1105    }
1106
1107    #[test]
1108    fn ymd_z() {
1109        let parse = Parse::new(&Utc, Utc::now().time());
1110        let now_at_pst = Utc::now().with_timezone(&FixedOffset::west(8 * 3600));
1111        let now_at_cst = Utc::now().with_timezone(&FixedOffset::east(8 * 3600));
1112
1113        let test_cases = [
1114            (
1115                "2021-02-21 PST",
1116                FixedOffset::west(8 * 3600)
1117                    .ymd(2021, 2, 21)
1118                    .and_time(now_at_pst.time())
1119                    .map(|dt| dt.with_timezone(&Utc)),
1120            ),
1121            (
1122                "2021-02-21 UTC",
1123                FixedOffset::west(0)
1124                    .ymd(2021, 2, 21)
1125                    .and_time(Utc::now().time())
1126                    .map(|dt| dt.with_timezone(&Utc)),
1127            ),
1128            (
1129                "2020-07-20+08:00",
1130                FixedOffset::east(8 * 3600)
1131                    .ymd(2020, 7, 20)
1132                    .and_time(now_at_cst.time())
1133                    .map(|dt| dt.with_timezone(&Utc)),
1134            ),
1135        ];
1136
1137        for &(input, want) in test_cases.iter() {
1138            assert_eq!(
1139                parse
1140                    .ymd_z(input)
1141                    .unwrap()
1142                    .unwrap()
1143                    .trunc_subsecs(0)
1144                    .with_second(0)
1145                    .unwrap(),
1146                want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
1147                "ymd_z/{}",
1148                input
1149            )
1150        }
1151        assert!(parse.ymd_z("not-date-time").is_none());
1152        // Pre-filter boundary: exactly 10 chars (bare date) is rejected (<= 10 guard), 11+ proceeds
1153        assert!(parse.ymd_z("2021-02-21").is_none()); // exactly 10 chars, rejected
1154        assert!(parse.ymd_z("2021-02-21X").is_none()); // 11 chars, proceeds to regex but regex rejects
1155    }
1156
1157    #[test]
1158    fn month_ymd() {
1159        let parse = Parse::new(&Utc, Utc::now().time());
1160
1161        let test_cases = [(
1162            "2021-Feb-21",
1163            Utc.ymd(2021, 2, 21).and_time(Utc::now().time()),
1164        )];
1165
1166        for &(input, want) in test_cases.iter() {
1167            assert_eq!(
1168                parse
1169                    .month_ymd(input)
1170                    .unwrap()
1171                    .unwrap()
1172                    .trunc_subsecs(0)
1173                    .with_second(0)
1174                    .unwrap(),
1175                want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
1176                "month_ymd/{}",
1177                input
1178            )
1179        }
1180        assert!(parse.month_ymd("not-date-time").is_none());
1181    }
1182
1183    #[test]
1184    fn month_mdy_hms() {
1185        let parse = Parse::new(&Utc, Utc::now().time());
1186
1187        let test_cases = [
1188            (
1189                "May 8, 2009 5:57:51 PM",
1190                Utc.ymd(2009, 5, 8).and_hms(17, 57, 51),
1191            ),
1192            (
1193                "September 17, 2012 10:09am",
1194                Utc.ymd(2012, 9, 17).and_hms(10, 9, 0),
1195            ),
1196            (
1197                "September 17, 2012, 10:10:09",
1198                Utc.ymd(2012, 9, 17).and_hms(10, 10, 9),
1199            ),
1200        ];
1201
1202        for &(input, want) in test_cases.iter() {
1203            assert_eq!(
1204                parse.month_mdy_hms(input).unwrap().unwrap(),
1205                want,
1206                "month_mdy_hms/{}",
1207                input
1208            )
1209        }
1210        assert!(parse.month_mdy_hms("not-date-time").is_none());
1211    }
1212
1213    #[test]
1214    fn month_mdy_hms_z() {
1215        let parse = Parse::new(&Utc, Utc::now().time());
1216
1217        let test_cases = [
1218            (
1219                "May 02, 2021 15:51:31 UTC",
1220                Utc.ymd(2021, 5, 2).and_hms(15, 51, 31),
1221            ),
1222            (
1223                "May 02, 2021 15:51 UTC",
1224                Utc.ymd(2021, 5, 2).and_hms(15, 51, 0),
1225            ),
1226            (
1227                "May 26, 2021, 12:49 AM PDT",
1228                Utc.ymd(2021, 5, 26).and_hms(7, 49, 0),
1229            ),
1230            (
1231                "September 17, 2012 at 10:09am PST",
1232                Utc.ymd(2012, 9, 17).and_hms(18, 9, 0),
1233            ),
1234        ];
1235
1236        for &(input, want) in test_cases.iter() {
1237            assert_eq!(
1238                parse.month_mdy_hms_z(input).unwrap().unwrap(),
1239                want,
1240                "month_mdy_hms_z/{}",
1241                input
1242            )
1243        }
1244        assert!(parse.month_mdy_hms_z("not-date-time").is_none());
1245        // Pre-filter: 20+ chars required; no isolated 4-digit year → has_year=false, rejected
1246        assert!(parse.month_mdy_hms_z("May 27, 02:45:27 XX PST").is_none()); // 23 chars, no 4-digit year
1247        // Pre-filter: 20+ chars with isolated 4-digit sequence → has_year=true, regex rejects format
1248        assert!(parse.month_mdy_hms_z("May 27 1234 something PST").is_none()); // 25 chars, has_year=true but regex rejects
1249    }
1250
1251    #[test]
1252    fn month_mdy() {
1253        let parse = Parse::new(&Utc, Utc::now().time());
1254
1255        let test_cases = [
1256            (
1257                "May 25, 2021",
1258                Utc.ymd(2021, 5, 25).and_time(Utc::now().time()),
1259            ),
1260            (
1261                "oct 7, 1970",
1262                Utc.ymd(1970, 10, 7).and_time(Utc::now().time()),
1263            ),
1264            (
1265                "oct 7, 70",
1266                Utc.ymd(1970, 10, 7).and_time(Utc::now().time()),
1267            ),
1268            (
1269                "oct. 7, 1970",
1270                Utc.ymd(1970, 10, 7).and_time(Utc::now().time()),
1271            ),
1272            (
1273                "oct. 7, 70",
1274                Utc.ymd(1970, 10, 7).and_time(Utc::now().time()),
1275            ),
1276            (
1277                "October 7, 1970",
1278                Utc.ymd(1970, 10, 7).and_time(Utc::now().time()),
1279            ),
1280        ];
1281
1282        for &(input, want) in test_cases.iter() {
1283            assert_eq!(
1284                parse
1285                    .month_mdy(input)
1286                    .unwrap()
1287                    .unwrap()
1288                    .trunc_subsecs(0)
1289                    .with_second(0)
1290                    .unwrap(),
1291                want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
1292                "month_mdy/{}",
1293                input
1294            )
1295        }
1296        assert!(parse.month_mdy("not-date-time").is_none());
1297    }
1298
1299    #[test]
1300    fn month_dmy_hms() {
1301        let parse = Parse::new(&Utc, Utc::now().time());
1302
1303        let test_cases = [
1304            (
1305                "12 Feb 2006, 19:17",
1306                Utc.ymd(2006, 2, 12).and_hms(19, 17, 0),
1307            ),
1308            ("12 Feb 2006 19:17", Utc.ymd(2006, 2, 12).and_hms(19, 17, 0)),
1309            (
1310                "14 May 2019 19:11:40.164",
1311                Utc.ymd(2019, 5, 14).and_hms_milli(19, 11, 40, 164),
1312            ),
1313        ];
1314
1315        for &(input, want) in test_cases.iter() {
1316            assert_eq!(
1317                parse.month_dmy_hms(input).unwrap().unwrap(),
1318                want,
1319                "month_dmy_hms/{}",
1320                input
1321            )
1322        }
1323        assert!(parse.month_dmy_hms("not-date-time").is_none());
1324    }
1325
1326    #[test]
1327    fn month_dmy() {
1328        let parse = Parse::new(&Utc, Utc::now().time());
1329
1330        let test_cases = [
1331            ("7 oct 70", Utc.ymd(1970, 10, 7).and_time(Utc::now().time())),
1332            (
1333                "7 oct 1970",
1334                Utc.ymd(1970, 10, 7).and_time(Utc::now().time()),
1335            ),
1336            (
1337                "03 February 2013",
1338                Utc.ymd(2013, 2, 3).and_time(Utc::now().time()),
1339            ),
1340            (
1341                "1 July 2013",
1342                Utc.ymd(2013, 7, 1).and_time(Utc::now().time()),
1343            ),
1344        ];
1345
1346        for &(input, want) in test_cases.iter() {
1347            assert_eq!(
1348                parse
1349                    .month_dmy(input)
1350                    .unwrap()
1351                    .unwrap()
1352                    .trunc_subsecs(0)
1353                    .with_second(0)
1354                    .unwrap(),
1355                want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
1356                "month_dmy/{}",
1357                input
1358            )
1359        }
1360        assert!(parse.month_dmy("not-date-time").is_none());
1361    }
1362
1363    // Explicitly tests the `four_digit_year` fast path in `month_dmy` (skips `%d %B %y`) and
1364    // the else-branch fallback that tries `%d %B %y` first then `%d %B %Y`.
1365    #[test]
1366    fn month_dmy_year_fast_path() {
1367        let parse = Parse::new(&Utc, Utc::now().time());
1368
1369        // Fast path: 4-digit year — `four_digit_year` is true, goes directly to `%d %B %Y`
1370        let four_digit = parse.month_dmy("14 May 2019").unwrap().unwrap();
1371        assert_eq!(four_digit.year(), 2019);
1372        assert_eq!(four_digit.month(), 5);
1373        assert_eq!(four_digit.day(), 14);
1374
1375        // Else-branch: 2-digit year — `four_digit_year` is false, tries `%d %B %y` first
1376        // chrono %y: 00–68 → 2000–2068, so "19" → 2019 (not 1919)
1377        let two_digit = parse.month_dmy("14 May 19").unwrap().unwrap();
1378        assert_eq!(two_digit.year(), 2019);
1379        assert_eq!(two_digit.month(), 5);
1380        assert_eq!(two_digit.day(), 14);
1381    }
1382
1383    #[test]
1384    fn slash_mdy_hms() {
1385        let parse = Parse::new(&Utc, Utc::now().time());
1386
1387        let test_cases = vec![
1388            ("4/8/2014 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)),
1389            ("04/08/2014 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)),
1390            ("4/8/14 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)),
1391            ("04/2/2014 03:00:51", Utc.ymd(2014, 4, 2).and_hms(3, 0, 51)),
1392            ("8/8/1965 12:00:00 AM", Utc.ymd(1965, 8, 8).and_hms(0, 0, 0)),
1393            (
1394                "8/8/1965 01:00:01 PM",
1395                Utc.ymd(1965, 8, 8).and_hms(13, 0, 1),
1396            ),
1397            ("8/8/1965 01:00 PM", Utc.ymd(1965, 8, 8).and_hms(13, 0, 0)),
1398            ("8/8/1965 1:00 PM", Utc.ymd(1965, 8, 8).and_hms(13, 0, 0)),
1399            ("8/8/1965 12:00 AM", Utc.ymd(1965, 8, 8).and_hms(0, 0, 0)),
1400            ("4/02/2014 03:00:51", Utc.ymd(2014, 4, 2).and_hms(3, 0, 51)),
1401            (
1402                "03/19/2012 10:11:59",
1403                Utc.ymd(2012, 3, 19).and_hms(10, 11, 59),
1404            ),
1405            (
1406                "03/19/2012 10:11:59.3186369",
1407                Utc.ymd(2012, 3, 19).and_hms_nano(10, 11, 59, 318636900),
1408            ),
1409        ];
1410
1411        for &(input, want) in test_cases.iter() {
1412            assert_eq!(
1413                parse.slash_mdy_hms(input).unwrap().unwrap(),
1414                want,
1415                "slash_mdy_hms/{}",
1416                input
1417            )
1418        }
1419        assert!(parse.slash_mdy_hms("not-date-time").is_none());
1420    }
1421
1422    #[test]
1423    fn slash_mdy() {
1424        let parse = Parse::new(&Utc, Utc::now().time());
1425
1426        let test_cases = [
1427            (
1428                "3/31/2014",
1429                Utc.ymd(2014, 3, 31).and_time(Utc::now().time()),
1430            ),
1431            (
1432                "03/31/2014",
1433                Utc.ymd(2014, 3, 31).and_time(Utc::now().time()),
1434            ),
1435            ("08/21/71", Utc.ymd(1971, 8, 21).and_time(Utc::now().time())),
1436            ("8/1/71", Utc.ymd(1971, 8, 1).and_time(Utc::now().time())),
1437        ];
1438
1439        for &(input, want) in test_cases.iter() {
1440            assert_eq!(
1441                parse
1442                    .slash_mdy(input)
1443                    .unwrap()
1444                    .unwrap()
1445                    .trunc_subsecs(0)
1446                    .with_second(0)
1447                    .unwrap(),
1448                want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
1449                "slash_mdy/{}",
1450                input
1451            )
1452        }
1453        assert!(parse.slash_mdy("not-date-time").is_none());
1454    }
1455
1456    #[test]
1457    fn slash_dmy() {
1458        let mut parse = Parse::new(&Utc, Utc::now().time());
1459
1460        let test_cases = [
1461            (
1462                "31/3/2014",
1463                Utc.ymd(2014, 3, 31).and_time(Utc::now().time()),
1464            ),
1465            (
1466                "13/11/2014",
1467                Utc.ymd(2014, 11, 13).and_time(Utc::now().time()),
1468            ),
1469            ("21/08/71", Utc.ymd(1971, 8, 21).and_time(Utc::now().time())),
1470            ("1/8/71", Utc.ymd(1971, 8, 1).and_time(Utc::now().time())),
1471        ];
1472
1473        for &(input, want) in test_cases.iter() {
1474            assert_eq!(
1475                parse
1476                    .prefer_dmy(true)
1477                    .slash_dmy(input)
1478                    .unwrap()
1479                    .unwrap()
1480                    .trunc_subsecs(0)
1481                    .with_second(0)
1482                    .unwrap(),
1483                want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
1484                "slash_dmy/{}",
1485                input
1486            )
1487        }
1488        assert!(parse.slash_dmy("not-date-time").is_none());
1489    }
1490
1491    #[test]
1492    fn slash_ymd_hms() {
1493        let parse = Parse::new(&Utc, Utc::now().time());
1494
1495        let test_cases = [
1496            ("2014/4/8 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)),
1497            ("2014/04/08 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)),
1498            ("2014/04/2 03:00:51", Utc.ymd(2014, 4, 2).and_hms(3, 0, 51)),
1499            ("2014/4/02 03:00:51", Utc.ymd(2014, 4, 2).and_hms(3, 0, 51)),
1500            (
1501                "2012/03/19 10:11:59",
1502                Utc.ymd(2012, 3, 19).and_hms(10, 11, 59),
1503            ),
1504            (
1505                "2012/03/19 10:11:59.3186369",
1506                Utc.ymd(2012, 3, 19).and_hms_nano(10, 11, 59, 318636900),
1507            ),
1508        ];
1509
1510        for &(input, want) in test_cases.iter() {
1511            assert_eq!(
1512                parse.slash_ymd_hms(input).unwrap().unwrap(),
1513                want,
1514                "slash_ymd_hms/{}",
1515                input
1516            )
1517        }
1518        assert!(parse.slash_ymd_hms("not-date-time").is_none());
1519    }
1520
1521    #[test]
1522    fn slash_ymd() {
1523        let parse = Parse::new(&Utc, Utc::now().time());
1524
1525        let test_cases = [
1526            (
1527                "2014/3/31",
1528                Utc.ymd(2014, 3, 31).and_time(Utc::now().time()),
1529            ),
1530            (
1531                "2014/03/31",
1532                Utc.ymd(2014, 3, 31).and_time(Utc::now().time()),
1533            ),
1534        ];
1535
1536        for &(input, want) in test_cases.iter() {
1537            assert_eq!(
1538                parse
1539                    .slash_ymd(input)
1540                    .unwrap()
1541                    .unwrap()
1542                    .trunc_subsecs(0)
1543                    .with_second(0)
1544                    .unwrap(),
1545                want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
1546                "slash_ymd/{}",
1547                input
1548            )
1549        }
1550        assert!(parse.slash_ymd("not-date-time").is_none());
1551    }
1552}