Skip to main content

qsv_dateparser/
datetime.rs

1#![allow(deprecated)]
2use crate::timezone;
3use anyhow::{Result, anyhow};
4use chrono::format::{Item, ParseResult, Parsed, parse as parse_items};
5use chrono::prelude::*;
6use regex::Regex;
7
8macro_rules! regex {
9    ($re:literal $(,)?) => {{
10        static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
11        RE.get_or_init(|| {
12            regex::RegexBuilder::new($re)
13                .unicode(false)
14                .build()
15                .expect("invalid regex literal")
16        })
17    }};
18}
19
20/// Compiles a strftime format literal into `chrono` format items exactly once,
21/// mirroring [`regex!`].
22///
23/// `chrono`'s `parse_from_str` / `datetime_from_str` convenience methods walk
24/// the format string through `StrftimeItems` on *every* call. Since every
25/// format in this file is a literal, that walk is pure repeated work, and it is
26/// paid several times per input because the parsers try formats in an
27/// `or_else` chain. Hoisting it into a `OnceLock` leaves the parse itself
28/// untouched: each method below reduces to exactly the same
29/// `parse(&mut Parsed, input, items)` plus `Parsed::to_*` that the chrono
30/// convenience method performs internally.
31///
32/// The format is a literal, so the resulting items borrow `'static` and the
33/// `expect` is checked once at first use rather than per call.
34macro_rules! fmt_items {
35    ($fmt:literal $(,)?) => {{
36        static ITEMS: std::sync::OnceLock<Vec<chrono::format::Item<'static>>> =
37            std::sync::OnceLock::new();
38        ITEMS
39            .get_or_init(|| {
40                chrono::format::StrftimeItems::new($fmt)
41                    .parse()
42                    .expect("invalid strftime literal")
43            })
44            .as_slice()
45    }};
46}
47/// Lookup table of bytes that may legally appear in an accepted date format:
48/// ASCII alphanumerics, ASCII whitespace (`\s` under `unicode(false)` =
49/// space, `\t`, `\n`, `\x0B`, `\x0C`, `\r`), and the separators `- + / : . ,`.
50const fn build_date_byte_table() -> [bool; 256] {
51    let mut table = [false; 256];
52    let mut i = 0usize;
53    while i < 256 {
54        let b = i as u8;
55        table[i] = b.is_ascii_alphanumeric()
56            || matches!(b, b' ' | 0x09..=0x0D)
57            || matches!(b, b'-' | b'+' | b'/' | b':' | b'.' | b',');
58        i += 1;
59    }
60    table
61}
62
63static DATE_BYTE: [bool; 256] = build_date_byte_table();
64
65/// Bytes matched by `\s` in these patterns, which are all built with
66/// `unicode(false)`: space plus `\t \n \v \f \r`.
67///
68/// `u8::is_ascii_whitespace` is not a substitute — it omits `\v` (0x0B), so
69/// using it here would silently disagree with the regexes.
70const fn build_ws_table() -> [bool; 256] {
71    let mut table = [false; 256];
72    let mut i = 0usize;
73    while i < 256 {
74        table[i] = matches!(i as u8, b' ' | 0x09..=0x0D);
75        i += 1;
76    }
77    table
78}
79
80static DATE_WS: [bool; 256] = build_ws_table();
81
82/// Cheap structural pre-filter run before the regex dispatch chain.
83///
84/// Any byte outside [`DATE_BYTE`] (e.g. `_`, `#`, `(`, or any non-ASCII byte)
85/// means the input cannot be a date, so we can bail before running 5-6 failing
86/// regex probes. This is the common, hot case for non-date string columns. It is
87/// intentionally conservative: it rejects nothing that currently parses.
88/// The table collapses the per-byte test to a single load + branch.
89#[inline]
90fn cannot_be_date(input: &str) -> bool {
91    input.bytes().any(|b| !DATE_BYTE[b as usize])
92}
93
94/// Extracts the trailing timezone token without asking the regex engine for
95/// capture positions.
96///
97/// The `_z` parsers used a `(?P<tz>…)` group purely to locate this token, but
98/// capture tracking forces the regex crate onto a much slower engine: on
99/// `month_mdy_hms_z`'s pattern, `captures()` costs 188 ns against 18 ns for
100/// `is_match()` or `find()`, and a reused `capture_locations` buffer only gets
101/// it to 179 ns — so it is the engine, not the allocation.
102///
103/// Instead the caller runs a prefix regex (the same pattern minus the tz
104/// group) with `find()`, and passes the match end here. `prefix_end` is where
105/// the greedy prefix stopped, so the rest of the input is the timezone.
106///
107/// Returns `None` whenever the remainder is not unambiguously a timezone
108/// token, which is the caller's signal to fall back to the original
109/// capture-based path. Because the fallback still runs the full pattern, this
110/// cannot change any result: it either produces exactly what the capture group
111/// would have, or declines and lets the slow path decide.
112///
113/// `require_ws` mirrors the difference between the two tz groups: `\s*` in
114/// `ymd_hms_z` versus `\s+` in `month_mdy_hms_z`. It matters because the
115/// prefix's own trailing `\s*` may already have eaten the separator, and the
116/// caller trims the token anyway, so the whitespace has to be accounted for
117/// here rather than inferred from the remainder alone.
118#[inline]
119fn tz_suffix(input: &str, prefix_end: usize, require_ws: bool) -> Option<&str> {
120    let bytes = input.as_bytes();
121    let rest = input.get(prefix_end..)?;
122
123    if require_ws {
124        let leading_ws = rest
125            .as_bytes()
126            .first()
127            .is_some_and(|&b| DATE_WS[b as usize]);
128        let trailing_ws = prefix_end
129            .checked_sub(1)
130            .is_some_and(|i| DATE_WS[bytes[i] as usize]);
131        if !leading_ws && !trailing_ws {
132            return None;
133        }
134    }
135
136    // `\s*` then 3-6 of `[+-:a-zA-Z0-9]`, and nothing else.
137    let token = rest.trim_start_matches(|c: char| (c as u32) < 128 && DATE_WS[c as usize]);
138    if !(3..=6).contains(&token.len()) {
139        return None;
140    }
141    token
142        .bytes()
143        .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'+' | b'-' | b':'))
144        .then_some(token)
145}
146
147/// Scratch space for the month-name parsers, which must strip separators from
148/// the input before handing it to chrono.
149///
150/// Comfortably covers every shape those regexes accept in practice — the
151/// longest realistic input, `September 17, 2012 at 10:09am PST`, is 33 bytes.
152/// The patterns allow unbounded runs of whitespace, so a pathological input can
153/// still exceed this; that falls back to allocating, which makes the size a
154/// performance choice rather than a correctness one.
155const NORMALIZE_SCRATCH: usize = 64;
156
157/// Copies `input` into `buf`, dropping every byte in `strip`, and then removes
158/// the first `"at"` when `drop_at` is set.
159///
160/// This replaces `String::replace`, whose allocation cost 56 ns of
161/// `month_mdy_hms_z` — comparable to the entire format parse. Returns `None`
162/// when the input does not fit, leaving the caller to allocate as before.
163///
164/// The `"at"` removal deliberately mirrors `String::find`, which matches the
165/// first occurrence anywhere rather than only a standalone word. No month name
166/// or abbreviation contains a lowercase `at`, so the two agree on every input
167/// the callers' regexes admit, but keeping the behaviour identical avoids
168/// having to prove that separately.
169#[inline]
170fn normalize_into<'b>(
171    input: &str,
172    buf: &'b mut [u8; NORMALIZE_SCRATCH],
173    strip: &[u8],
174    drop_at: bool,
175) -> Option<&'b str> {
176    if input.len() > buf.len() {
177        return None;
178    }
179
180    let mut n = 0;
181    for &b in input.as_bytes() {
182        if !strip.contains(&b) {
183            buf[n] = b;
184            n += 1;
185        }
186    }
187
188    if drop_at && let Some(pos) = buf[..n].windows(2).position(|w| w == b"at") {
189        buf.copy_within(pos + 2..n, pos);
190        n -= 2;
191    }
192
193    // Only whole ASCII bytes are ever dropped, so this cannot split a
194    // multi-byte character; the check is a cheap way to say so without unsafe.
195    std::str::from_utf8(&buf[..n]).ok()
196}
197
198/// Which format a date-time input's time-of-day portion dispatches to.
199///
200/// These are parse-dispatch buckets, not an exhaustive account of what the
201/// family regexes admit. `\d{1,2}:\d{2}(?::\d{2})?(?:\.\d{1,9})?\s*(?:am|pm|AM|PM)?`
202/// also admits a fraction with no seconds field — `hh:mm.fff`, with or without
203/// an AM/PM marker — which buckets as [`Self::Hm`] or [`Self::ImP`] and then
204/// fails against that format. It is malformed rather than a real shape, and
205/// `unsupported_shapes_still_fail` pins it as unsupported.
206///
207/// Each bucket is matched by at most one format string per family, so
208/// classifying up front replaces a chain of up to five trial parses — all but
209/// the last guaranteed to fail — with a single attempt.
210#[derive(Clone, Copy, PartialEq, Eq)]
211enum TimeShape {
212    /// `hh:mm`
213    Hm,
214    /// `hh:mm:ss`
215    Hms,
216    /// `hh:mm:ss.fff`
217    HmsF,
218    /// `hh:mm AM/PM`
219    ImP,
220    /// `hh:mm:ss AM/PM`
221    ImsP,
222    /// `hh:mm:ss.fff AM/PM`.
223    ///
224    /// Families whose regex admits both a fraction and an AM/PM marker parse
225    /// this with the same `%I:%M:%S%.f %P` they use for [`Self::ImsP`], since
226    /// `%.f` consumes nothing when there is no period. The two month-name
227    /// families reject it at their regex gate and map it to `None`.
228    HmsFP,
229}
230
231/// Classifies the time-of-day portion of an input that has already passed a
232/// family regex, from a single byte scan.
233///
234/// Callers pass the exact string they are about to hand to the parser, not the
235/// raw input. `month_mdy_hms` strips both `,` and `.` beforehand, so
236/// classifying the raw input would mistake the period in `Sept. 17, 2012` for
237/// fractional seconds. `month_dmy_hms` strips only `,` — it must keep a
238/// fractional-seconds period for [`TimeShape::HmsF`] to be reachable.
239///
240/// A colon count of 2 distinguishes `%H:%M:%S` from `%H:%M`; the callers'
241/// regexes cap the time at two colons and admit none anywhere else. The AM/PM
242/// marker can only ever be the final two bytes, and chrono's `%P` is
243/// case-insensitive, so a single `| 32` comparison covers `am`/`AM`/`Am`.
244#[inline]
245fn time_shape(input: &str) -> TimeShape {
246    let bytes = input.as_bytes();
247
248    let ampm = bytes.len() >= 2 && {
249        let [.., ap, m] = bytes else { unreachable!() };
250        m | 32 == b'm' && matches!(ap | 32, b'a' | b'p')
251    };
252
253    let mut colons = 0_u8;
254    let mut fraction = false;
255    for &b in bytes {
256        match b {
257            b':' => colons += 1,
258            b'.' => fraction = true,
259            _ => {}
260        }
261    }
262
263    match (colons >= 2, fraction, ampm) {
264        (true, true, true) => TimeShape::HmsFP,
265        (true, true, false) => TimeShape::HmsF,
266        (true, false, true) => TimeShape::ImsP,
267        (true, false, false) => TimeShape::Hms,
268        (false, _, true) => TimeShape::ImP,
269        (false, _, false) => TimeShape::Hm,
270    }
271}
272
273/// Returns true when the year field of a slash-separated date (the digits
274/// after the second `/`) is exactly 2 digits wide. Callers' regexes guarantee
275/// the `d{1,2}/d{1,2}/d{2,4}` shape, but the scan is panic-free regardless.
276///
277/// Used to dispatch between the `%y` and `%Y` chrono format families: `%y`
278/// consumes at most 2 digits, so it always fails on 3-4 digit years, and `%Y`
279/// is never reached on a 2-digit year that `%y` accepts (both families apply
280/// identical date-validity rules), so picking one family by year width is
281/// result-preserving and halves the trial-parse chain.
282#[inline]
283fn slash_year_is_two_digits(bytes: &[u8]) -> bool {
284    let mut slashes = 0u8;
285    let mut year_len = 0usize;
286    for &b in bytes {
287        if b == b'/' {
288            slashes += 1;
289        } else if slashes == 2 {
290            if b.is_ascii_digit() {
291                year_len += 1;
292            } else {
293                break;
294            }
295        }
296    }
297    year_len == 2
298}
299
300/// Parse struct has methods implemented parsers for accepted formats.
301pub struct Parse<'z, Tz2> {
302    tz: &'z Tz2,
303    default_time: NaiveTime,
304    prefer_dmy: bool,
305}
306
307impl<'z, Tz2> Parse<'z, Tz2>
308where
309    Tz2: TimeZone,
310{
311    /// Create a new instance of [`Parse`] with a custom parsing timezone that handles the
312    /// datetime string without time offset.
313    pub const fn new(tz: &'z Tz2, default_time: NaiveTime) -> Self {
314        Self {
315            tz,
316            default_time,
317            prefer_dmy: false,
318        }
319    }
320
321    pub const fn prefer_dmy(&mut self, yes: bool) -> &Self {
322        self.prefer_dmy = yes;
323        self
324    }
325
326    /// Create a new instance of [`Parse`] with a custom parsing timezone that handles the
327    /// datetime string without time offset, and the date parsing preference.
328    pub const fn new_with_preference(
329        tz: &'z Tz2,
330        default_time: NaiveTime,
331        prefer_dmy: bool,
332    ) -> Self {
333        Self {
334            tz,
335            default_time,
336            prefer_dmy,
337        }
338    }
339
340    /// Drop-in replacement for `Tz::datetime_from_str` taking pre-compiled items.
341    ///
342    /// Note that `Parsed` **must** be constructed fresh for every attempt.
343    /// `Parsed::set_*` returns `Err` when a field is set twice to conflicting
344    /// values, so reusing one `Parsed` across the `or_else` chains below would
345    /// produce silently wrong results rather than a compile error.
346    #[inline]
347    fn dt_from_items(&self, input: &str, items: &[Item<'static>]) -> ParseResult<DateTime<Tz2>> {
348        let mut parsed = Parsed::new();
349        parse_items(&mut parsed, input, items.iter())?;
350        parsed.to_datetime_with_timezone(self.tz)
351    }
352
353    /// Drop-in replacement for `NaiveDateTime::parse_from_str` taking
354    /// pre-compiled items. See [`Self::dt_from_items`] on `Parsed` reuse.
355    #[inline]
356    fn naive_dt_from_items(input: &str, items: &[Item<'static>]) -> ParseResult<NaiveDateTime> {
357        let mut parsed = Parsed::new();
358        parse_items(&mut parsed, input, items.iter())?;
359        parsed.to_naive_datetime_with_offset(0)
360    }
361
362    /// Drop-in replacement for `NaiveDate::parse_from_str` taking pre-compiled
363    /// items. See [`Self::dt_from_items`] on `Parsed` reuse.
364    #[inline]
365    fn naive_date_from_items(input: &str, items: &[Item<'static>]) -> ParseResult<NaiveDate> {
366        let mut parsed = Parsed::new();
367        parse_items(&mut parsed, input, items.iter())?;
368        parsed.to_naive_date()
369    }
370
371    /// This method tries to parse the input datetime string with a list of accepted formats. See
372    /// more examples from [`Parse`], [`crate::parse()`] and [`crate::parse_with_timezone()`].
373    ///
374    /// Order rationale: the regex-gated families are tried first because their
375    /// `is_match` gate rejects non-matching inputs cheaply. The two parsers
376    /// without a family regex gate — `unix_timestamp` (runs `fast_float2`) and
377    /// `rfc2822` (runs `parse_from_rfc2822`) — are tried last to avoid paying
378    /// their cost on the common ISO/slash dates. Each still applies its own cheap
379    /// byte pre-filter before the heavy parse (`unix_timestamp` checks the first
380    /// byte against the leads `fast_float2` accepts; `rfc2822` requires a `:`).
381    ///
382    /// This reorder is result-preserving:
383    /// - A `fast_float2`-parseable input (a pure finite number; `inf`/`nan` are
384    ///   rejected as non-finite) matches no family gate (they all require `/`,
385    ///   an interior `-`, or a letters+space shape a bare number lacks), so it
386    ///   still reaches `unix_timestamp`.
387    /// - An `rfc2822` input always carries a timezone, which makes the
388    ///   `$`-anchored `month_dmy_*` regexes fail; conversely `month_dmy_*` only
389    ///   succeeds without a timezone, which makes `rfc2822` fail. The two are
390    ///   mutually exclusive, so deferring `rfc2822` cannot change any result.
391    ///
392    /// Within that order, the first byte selects which families can match at
393    /// all. Every family gate is `^`-anchored on either a digit or a letter, so
394    /// a letter-leading input cannot match any of the numeric families and vice
395    /// versa. Running them anyway costs roughly 5-10 ns each in regex startup
396    /// even when the first byte rejects immediately, which is most of the cost
397    /// of a non-date word — the dominant input in a `qsv stats --infer-dates`
398    /// run over a text column.
399    ///
400    /// `rfc2822` stays in the digit branch as well as the letter branch: the
401    /// day-of-week is optional in RFC 2822, so `02 Jun 2021 06:31:39 GMT`
402    /// parses and leads with a digit.
403    #[inline]
404    pub fn parse(&self, input: &str) -> Result<DateTime<Utc>> {
405        if cannot_be_date(input) {
406            return Err(anyhow!("{} did not match any formats.", input));
407        }
408        let Some(&first) = input.as_bytes().first() else {
409            return Err(anyhow!("{} did not match any formats.", input));
410        };
411
412        let parsed = if first.is_ascii_digit() {
413            self.slash_mdy_family(input)
414                .or_else(|| self.slash_ymd_family(input))
415                .or_else(|| self.ymd_family(input))
416                .or_else(|| self.month_ymd(input))
417                .or_else(|| self.month_dmy_family(input))
418                .or_else(|| self.unix_timestamp(input))
419                .or_else(|| self.rfc2822(input))
420        } else if first.is_ascii_alphabetic() {
421            // `month_mdy_family` is the only letter-anchored gate.
422            // `unix_timestamp` is excluded by its own lead-byte pre-filter.
423            self.month_mdy_family(input).or_else(|| self.rfc2822(input))
424        } else {
425            // `+`, `-`, `.` and the separators that survive `cannot_be_date`.
426            // No family gate can match, but a signed or bare-decimal timestamp
427            // can; `rfc2822` is kept for its own leading-whitespace handling.
428            self.unix_timestamp(input).or_else(|| self.rfc2822(input))
429        };
430
431        parsed.unwrap_or_else(|| Err(anyhow!("{} did not match any formats.", input)))
432    }
433
434    #[inline]
435    fn ymd_family(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
436        let re: &Regex = regex! {
437            r"^\d{4}-\d{2}"
438
439        };
440
441        if !re.is_match(input) {
442            return None;
443        }
444        self.rfc3339(input)
445            .or_else(|| self.ymd_hms(input))
446            .or_else(|| self.ymd_hms_z(input))
447            .or_else(|| self.ymd(input))
448            .or_else(|| self.ymd_z(input))
449    }
450
451    #[inline]
452    fn month_mdy_family(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
453        let re: &Regex = regex! {
454            r"^[a-zA-Z]{3,9}\.?\s+\d{1,2}"
455        };
456
457        if !re.is_match(input) {
458            return None;
459        }
460        self.month_mdy_hms(input)
461            .or_else(|| self.month_mdy_hms_z(input))
462            .or_else(|| self.month_mdy(input))
463    }
464
465    #[inline]
466    fn month_dmy_family(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
467        let re: &Regex = regex! {r"^\d{1,2}\s+[a-zA-Z]{3,9}"
468        };
469
470        if !re.is_match(input) {
471            return None;
472        }
473        self.month_dmy_hms(input).or_else(|| self.month_dmy(input))
474    }
475
476    #[inline]
477    fn slash_mdy_family(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
478        let re: &Regex = regex! {r"^\d{1,2}/\d{1,2}"
479        };
480        if !re.is_match(input) {
481            return None;
482        }
483        if self.prefer_dmy {
484            self.slash_dmy_hms(input)
485                .or_else(|| self.slash_dmy(input))
486                .or_else(|| self.slash_mdy_hms(input))
487                .or_else(|| self.slash_mdy(input))
488        } else {
489            self.slash_mdy_hms(input)
490                .or_else(|| self.slash_mdy(input))
491                .or_else(|| self.slash_dmy_hms(input))
492                .or_else(|| self.slash_dmy(input))
493        }
494    }
495
496    #[inline]
497    fn slash_ymd_family(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
498        let re: &Regex = regex! {r"^[0-9]{4}/[0-9]{1,2}"};
499        if !re.is_match(input) {
500            return None;
501        }
502        self.slash_ymd_hms(input).or_else(|| self.slash_ymd(input))
503    }
504
505    // unix timestamp
506    // - 0
507    // - -770172300
508    // - 1671673426.123456789
509    #[inline]
510    fn unix_timestamp(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
511        // Cheap pre-filter before the heavier float parse: only finite numbers
512        // are accepted, so the first byte must be a digit, sign, or dot
513        // (`fast_float2` rejects leading whitespace and empty input; bare
514        // `inf`/`nan` are excluded here, matching the non-finite check below).
515        // This is the last-resort numeric parser, so most inputs reaching it
516        // are non-numeric.
517        let &b0 = input.as_bytes().first()?;
518        if !(b0.is_ascii_digit() || matches!(b0, b'+' | b'-' | b'.')) {
519            return None;
520        }
521
522        let ts_sec_val: f64 = if let Ok(val) = fast_float2::parse(input) {
523            val
524        } else {
525            return None;
526        };
527
528        // Reject non-finite values (`+inf`, `-nan`, … pass the lead-byte filter
529        // above): the `as i64` cast below would otherwise turn `nan` into 0
530        // (1970-01-01) and `inf` into i64::MAX nanos (2262-04-11) — never the
531        // intended reading of the input.
532        if !ts_sec_val.is_finite() {
533            return None;
534        }
535
536        // convert the timestamp seconds value to nanoseconds
537        let ts_ns_val = ts_sec_val * 1_000_000_000_f64;
538
539        let result = Utc.timestamp_nanos(ts_ns_val as i64).with_timezone(&Utc);
540        Some(Ok(result))
541    }
542
543    // rfc3339
544    // - 2021-05-01T01:17:02.604456Z
545    // - 2017-11-25T22:34:50Z
546    #[inline]
547    fn rfc3339(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
548        DateTime::parse_from_rfc3339(input)
549            .ok()
550            .map(|parsed| parsed.with_timezone(&Utc))
551            .map(Ok)
552    }
553
554    // rfc2822
555    // - Wed, 02 Jun 2021 06:31:39 GMT
556    #[inline]
557    fn rfc2822(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
558        // Fast pre-filter: every RFC2822 datetime carries a time-of-day
559        // (`hour ":" minute`), so it always contains ':'. Skip the
560        // `parse_from_rfc2822` attempt for colon-free inputs. This is the
561        // last-resort parser, so most inputs reaching it are non-rfc2822.
562        if !input.as_bytes().contains(&b':') {
563            return None;
564        }
565        DateTime::parse_from_rfc2822(input)
566            .ok()
567            .map(|parsed| parsed.with_timezone(&Utc))
568            .map(Ok)
569    }
570
571    // yyyy-mm-dd hh:mm:ss  (separator is space OR ISO 8601 'T')
572    // - 2014-04-26 05:24:37 PM
573    // - 2021-04-30 21:14
574    // - 2021-04-30 21:14:10
575    // - 2021-04-30 21:14:10.052282
576    // - 2014-04-26 17:24:37.123
577    // - 2014-04-26 17:24:37.3186369
578    // - 2012-08-03 18:31:59.257000000
579    // - 2020-01-15T08:00
580    // - 2020-01-15T08:00:00
581    // - 2020-01-15T08:00:00.123456
582    // - 2012-03-19 10:11:59.318 PM
583    #[inline]
584    fn ymd_hms(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
585        let re: &Regex = regex! {
586                r"^\d{4}-\d{2}-\d{2}[T\s]+\d{2}:\d{2}(?::\d{2})?(?:\.\d{1,9})?\s*(?:am|pm|AM|PM)?$"
587
588        };
589        if !re.is_match(input) {
590            return None;
591        }
592
593        // Byte 10 is the date/time separator. The regex guarantees the input
594        // has at least 16 bytes and that byte 10 is either 'T' or ASCII
595        // whitespace, so the single byte picks the format family.
596        let items = match (input.as_bytes()[10] == b'T', time_shape(input)) {
597            (true, TimeShape::Hms) => fmt_items!("%Y-%m-%dT%H:%M:%S"),
598            (true, TimeShape::Hm) => fmt_items!("%Y-%m-%dT%H:%M"),
599            (true, TimeShape::HmsF) => fmt_items!("%Y-%m-%dT%H:%M:%S%.f"),
600            (true, TimeShape::ImsP | TimeShape::HmsFP) => {
601                fmt_items!("%Y-%m-%dT%I:%M:%S%.f %P")
602            }
603            (true, TimeShape::ImP) => fmt_items!("%Y-%m-%dT%I:%M %P"),
604            (false, TimeShape::Hms) => fmt_items!("%Y-%m-%d %H:%M:%S"),
605            (false, TimeShape::Hm) => fmt_items!("%Y-%m-%d %H:%M"),
606            (false, TimeShape::HmsF) => fmt_items!("%Y-%m-%d %H:%M:%S%.f"),
607            (false, TimeShape::ImsP | TimeShape::HmsFP) => {
608                fmt_items!("%Y-%m-%d %I:%M:%S%.f %P")
609            }
610            (false, TimeShape::ImP) => fmt_items!("%Y-%m-%d %I:%M %P"),
611        };
612
613        self.dt_from_items(input, items)
614            .ok()
615            .map(|parsed| parsed.with_timezone(&Utc))
616            .map(Ok)
617    }
618
619    // yyyy-mm-dd hh:mm:ss z
620    // - 2017-11-25 13:31:15 PST
621    // - 2017-11-25 13:31 PST
622    // - 2014-12-16 06:20:00 UTC
623    // - 2014-12-16 06:20:00 GMT
624    // - 2014-04-26 13:13:43 +0800
625    // - 2014-04-26 13:13:44 +09:00
626    // - 2012-08-03 18:31:59.257000000 +0000
627    // - 2015-09-30 18:48:56.35272715 UTC
628    #[inline]
629    fn ymd_hms_z(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
630        // Fast pre-filter: bare dates "YYYY-MM-DD" are 10 chars; valid inputs need space + time
631        if input.len() < 17 || !input.as_bytes()[10].is_ascii_whitespace() {
632            return None;
633        }
634        // Fast path: locate the timezone with `find()` on the pattern minus the
635        // tz group, which keeps the regex engine off the capture-tracking path.
636        // See `tz_suffix` — on failure this falls through to the original
637        // capture-based match, so no input can change meaning.
638        let prefix: &Regex = regex! {r"^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}(?::\d{2})?(?:\.\d{1,9})?"};
639        let fast_tz = prefix
640            .find(input)
641            .and_then(|m| tz_suffix(input, m.end(), false));
642
643        let re: &Regex = regex! {
644                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})$"
645        };
646
647        if let Some(tz) = fast_tz.or_else(|| {
648            re.captures(input)
649                .and_then(|caps| caps.name("tz").map(|m| m.as_str().trim()))
650        }) {
651            let parse_from_str = Self::naive_dt_from_items;
652            return match timezone::parse(tz) {
653                Ok(offset) => parse_from_str(input, fmt_items!("%Y-%m-%d %H:%M:%S %Z"))
654                    .or_else(|_| parse_from_str(input, fmt_items!("%Y-%m-%d %H:%M %Z")))
655                    .or_else(|_| parse_from_str(input, fmt_items!("%Y-%m-%d %H:%M:%S%.f %Z")))
656                    .ok()
657                    .and_then(|parsed| offset.from_local_datetime(&parsed).single())
658                    .map(|datetime| datetime.with_timezone(&Utc))
659                    .map(Ok),
660                Err(err) => Some(Err(err)),
661            };
662        }
663        None
664    }
665
666    // yyyy-mm-dd
667    // - 2021-02-21
668    #[inline]
669    fn ymd(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
670        let re: &Regex = regex! {r"^\d{4}-\d{2}-\d{2}$"
671        };
672
673        if !re.is_match(input) {
674            return None;
675        }
676        let now = Utc::now()
677            .date()
678            .and_time(self.default_time)?
679            .with_timezone(self.tz);
680        Self::naive_date_from_items(input, fmt_items!("%Y-%m-%d"))
681            .ok()
682            .map(|parsed| parsed.and_time(now.time()))
683            .and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
684            .map(|at_tz| at_tz.with_timezone(&Utc))
685            .map(Ok)
686    }
687
688    // yyyy-mm-dd z
689    // - 2021-02-21 PST
690    // - 2021-02-21 UTC
691    // - 2020-07-20+08:00 (yyyy-mm-dd-07:00)
692    #[inline]
693    fn ymd_z(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
694        // Fast pre-filter: bare date "YYYY-MM-DD" is exactly 10 chars; timezone appended = longer
695        if input.len() <= 10 {
696            return None;
697        }
698        let re: &Regex = regex! {r"^\d{4}-\d{2}-\d{2}(?P<tz>\s*[+-:a-zA-Z0-9]{3,6})$"
699        };
700        if let Some(caps) = re.captures(input)
701            && let Some(matched_tz) = caps.name("tz")
702        {
703            return match timezone::parse(matched_tz.as_str().trim()) {
704                Ok(offset) => {
705                    let now = Utc::now()
706                        .date()
707                        .and_time(self.default_time)?
708                        .with_timezone(&offset);
709                    Self::naive_date_from_items(input, fmt_items!("%Y-%m-%d %Z"))
710                        .ok()
711                        .map(|parsed| parsed.and_time(now.time()))
712                        .and_then(|datetime| offset.from_local_datetime(&datetime).single())
713                        .map(|at_tz| at_tz.with_timezone(&Utc))
714                        .map(Ok)
715                }
716                Err(err) => Some(Err(err)),
717            };
718        }
719        None
720    }
721
722    // yyyy-mon-dd
723    // - 2021-Feb-21
724    #[inline]
725    fn month_ymd(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
726        let re: &Regex = regex! {r"^\d{4}-\w{3,9}-\d{2}$"
727        };
728        if !re.is_match(input) {
729            return None;
730        }
731
732        let now = Utc::now()
733            .date()
734            .and_time(self.default_time)?
735            .with_timezone(self.tz);
736        Self::naive_date_from_items(input, fmt_items!("%Y-%m-%d"))
737            .or_else(|_| Self::naive_date_from_items(input, fmt_items!("%Y-%b-%d")))
738            .ok()
739            .map(|parsed| parsed.and_time(now.time()))
740            .and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
741            .map(|at_tz| at_tz.with_timezone(&Utc))
742            .map(Ok)
743    }
744
745    // Mon dd, yyyy, hh:mm:ss
746    // - May 8, 2009 5:57:51 PM
747    // - September 17, 2012 10:09am
748    // - September 17, 2012, 10:10:09
749    #[inline]
750    fn month_mdy_hms(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
751        let re: &Regex = regex! {
752                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)?$"
753        };
754        if !re.is_match(input) {
755            return None;
756        }
757
758        // The regex above enforces \s+ after any comma or period, so removing bare ',' or '.'
759        // is equivalent to the previous `replace(", ", " ").replace(". ", " ")` for all
760        // inputs that reach this point — marginally-malformed inputs (e.g. "May 27,2012 …")
761        // still fail to parse after stripping because the digits run together.
762        let mut buf = [0_u8; NORMALIZE_SCRATCH];
763        let fallback;
764        let dt = match normalize_into(input, &mut buf, b",.", false) {
765            Some(s) => s,
766            None => {
767                fallback = input.replace([',', '.'], "");
768                fallback.as_str()
769            }
770        };
771        // Classify `dt`, not `input`: the regex admits a period after an
772        // abbreviated month ("Sept. 17, 2012"), which would otherwise read as
773        // fractional seconds. This family's regex has no fractional-seconds
774        // group at all, and the strip removes any period regardless, so the
775        // two fraction-bearing shapes cannot occur — and never had a format.
776        let items = match time_shape(dt) {
777            TimeShape::Hms => fmt_items!("%B %d %Y %H:%M:%S"),
778            TimeShape::Hm => fmt_items!("%B %d %Y %H:%M"),
779            TimeShape::ImsP => fmt_items!("%B %d %Y %I:%M:%S %P"),
780            TimeShape::ImP => fmt_items!("%B %d %Y %I:%M %P"),
781            TimeShape::HmsF | TimeShape::HmsFP => return None,
782        };
783        self.dt_from_items(dt, items)
784            .ok()
785            .map(|at_tz| at_tz.with_timezone(&Utc))
786            .map(Ok)
787    }
788
789    // Mon dd, yyyy hh:mm:ss z
790    // - May 02, 2021 15:51:31 UTC
791    // - May 02, 2021 15:51 UTC
792    // - May 26, 2021, 12:49 AM PDT
793    // - September 17, 2012 at 10:09am PST
794    #[inline]
795    fn month_mdy_hms_z(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
796        // Fast pre-filter: must contain an isolated 4-digit year — eliminates "May 27 02:45:27".
797        // Skip the O(n) scan entirely for inputs too short to hold a valid month+day+year+time+tz.
798        if input.len() < 20 {
799            return None;
800        }
801        let bytes = input.as_bytes();
802        let has_year = (0..bytes.len().saturating_sub(3)).any(|i| {
803            bytes[i..i + 4].iter().all(|b| b.is_ascii_digit())
804                && (i == 0 || !bytes[i - 1].is_ascii_digit())
805                && bytes.get(i + 4).is_none_or(|b| !b.is_ascii_digit())
806        });
807        if !has_year {
808            return None;
809        }
810        // Fast path — see the twin comment in ymd_hms_z. This pattern's tz
811        // group requires whitespace (`\s+`), but the prefix's own trailing
812        // `\s*` may already have consumed it, so `tz_suffix` is told to accept
813        // the separator on either side of the boundary.
814        let prefix: &Regex = regex! {
815                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)?"
816        };
817        let fast_tz = prefix
818            .find(input)
819            .and_then(|m| tz_suffix(input, m.end(), true));
820
821        let re: &Regex = regex! {
822                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})$",
823        };
824        if let Some(tz) = fast_tz.or_else(|| {
825            re.captures(input)
826                .and_then(|caps| caps.name("tz").map(|m| m.as_str().trim()))
827        }) {
828            let parse_from_str = Self::naive_dt_from_items;
829            return match timezone::parse(tz) {
830                Ok(offset) => {
831                    let mut buf = [0_u8; NORMALIZE_SCRATCH];
832                    let fallback;
833                    let dt: &str = match normalize_into(input, &mut buf, b",", true) {
834                        Some(s) => s,
835                        None => {
836                            let mut owned = input.replace(',', "");
837                            if let Some(pos) = owned.find("at") {
838                                owned.replace_range(pos..pos + 2, "");
839                            }
840                            fallback = owned;
841                            fallback.as_str()
842                        }
843                    };
844                    parse_from_str(dt, fmt_items!("%B %d %Y %H:%M:%S %Z"))
845                        .or_else(|_| parse_from_str(dt, fmt_items!("%B %d %Y %H:%M %Z")))
846                        .or_else(|_| parse_from_str(dt, fmt_items!("%B %d %Y %I:%M:%S %P %Z")))
847                        .or_else(|_| parse_from_str(dt, fmt_items!("%B %d %Y %I:%M %P %Z")))
848                        .ok()
849                        .and_then(|parsed| offset.from_local_datetime(&parsed).single())
850                        .map(|datetime| datetime.with_timezone(&Utc))
851                        .map(Ok)
852                }
853                Err(err) => Some(Err(err)),
854            };
855        }
856        None
857    }
858
859    // Mon dd, yyyy
860    // - May 25, 2021
861    // - oct 7, 1970
862    // - oct 7, 70
863    // - oct. 7, 1970
864    // - oct. 7, 70
865    // - October 7, 1970
866    #[inline]
867    fn month_mdy(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
868        let re: &Regex = regex! {r"^[a-zA-Z]{3,9}\.?\s+\d{1,2},\s+\d{2,4}$"
869        };
870        if !re.is_match(input) {
871            return None;
872        }
873
874        let now = Utc::now()
875            .date()
876            .and_time(self.default_time)?
877            .with_timezone(self.tz);
878        // The regex above enforces \s+ after any comma or period, so removing bare ',' or '.'
879        // is equivalent to the previous `replace(", ", " ").replace(". ", " ")` for all
880        // inputs that reach this point.
881        let mut buf = [0_u8; NORMALIZE_SCRATCH];
882        let fallback;
883        let dt = match normalize_into(input, &mut buf, b",.", false) {
884            Some(s) => s,
885            None => {
886                fallback = input.replace([',', '.'], "");
887                fallback.as_str()
888            }
889        };
890        Self::naive_date_from_items(dt, fmt_items!("%B %d %y"))
891            .or_else(|_| Self::naive_date_from_items(dt, fmt_items!("%B %d %Y")))
892            .ok()
893            .map(|parsed| parsed.and_time(now.time()))
894            .and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
895            .map(|at_tz| at_tz.with_timezone(&Utc))
896            .map(Ok)
897    }
898
899    // dd Mon yyyy hh:mm:ss
900    // - 12 Feb 2006, 19:17
901    // - 12 Feb 2006 19:17
902    // - 14 May 2019 19:11:40.164
903    #[inline]
904    fn month_dmy_hms(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
905        // Fast pre-filter: time component always contains ':', skip regex for date-only inputs.
906        if !input.as_bytes().contains(&b':') {
907            return None;
908        }
909        let re: &Regex = regex! {
910                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})?$"
911        };
912        if !re.is_match(input) {
913            return None;
914        }
915
916        let mut buf = [0_u8; NORMALIZE_SCRATCH];
917        let fallback;
918        let dt = match normalize_into(input, &mut buf, b",", false) {
919            Some(s) => s,
920            None => {
921                fallback = input.replace(',', "");
922                fallback.as_str()
923            }
924        };
925        // This family's regex has no am/pm alternative, so the AM/PM shapes
926        // cannot occur here. The chain previously ended in `%I:%M:%S %P` and
927        // `%I:%M %P`, which were therefore unreachable; dropping them changes
928        // no result.
929        let items = match time_shape(dt) {
930            TimeShape::Hms => fmt_items!("%d %B %Y %H:%M:%S"),
931            TimeShape::Hm => fmt_items!("%d %B %Y %H:%M"),
932            TimeShape::HmsF => fmt_items!("%d %B %Y %H:%M:%S%.f"),
933            TimeShape::ImP | TimeShape::ImsP | TimeShape::HmsFP => return None,
934        };
935        self.dt_from_items(dt, items)
936            .ok()
937            .map(|at_tz| at_tz.with_timezone(&Utc))
938            .map(Ok)
939    }
940
941    // dd Mon yyyy
942    // - 7 oct 70
943    // - 7 oct 1970
944    // - 03 February 2013
945    // - 1 July 2013
946    #[inline]
947    fn month_dmy(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
948        let re: &Regex = regex! {r"^\d{1,2}\s+[a-zA-Z]{3,9}\s+\d{2,4}$"
949        };
950        if !re.is_match(input) {
951            return None;
952        }
953
954        let now = Utc::now()
955            .date()
956            .and_time(self.default_time)?
957            .with_timezone(self.tz);
958        // Fast path: if the last 4 bytes are all digits and preceded by a space, it's a
959        // 4-digit year — skip the always-failing %d %B %y (2-digit year) attempt.
960        let bytes = input.as_bytes();
961        let len = bytes.len();
962        let four_digit_year = len >= 5
963            && bytes[len - 4..].iter().all(|b| b.is_ascii_digit())
964            && bytes[len - 5].is_ascii_whitespace();
965        let parsed = if four_digit_year {
966            Self::naive_date_from_items(input, fmt_items!("%d %B %Y"))
967        } else {
968            Self::naive_date_from_items(input, fmt_items!("%d %B %y"))
969                .or_else(|_| Self::naive_date_from_items(input, fmt_items!("%d %B %Y")))
970        };
971        parsed
972            .ok()
973            .map(|parsed| parsed.and_time(now.time()))
974            .and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
975            .map(|at_tz| at_tz.with_timezone(&Utc))
976            .map(Ok)
977    }
978
979    // mm/dd/yyyy hh:mm:ss
980    // - 4/8/2014 22:05
981    // - 04/08/2014 22:05
982    // - 4/8/14 22:05
983    // - 04/2/2014 03:00:51
984    // - 8/8/1965 12:00:00 AM
985    // - 8/8/1965 01:00:01 PM
986    // - 8/8/1965 01:00 PM
987    // - 8/8/1965 1:00 PM
988    // - 8/8/1965 12:00 AM
989    // - 4/02/2014 03:00:51
990    // - 03/19/2012 10:11:59
991    // - 03/19/2012 10:11:59.3186369
992    // - 03/19/2012 10:11:59.318 PM
993    #[inline]
994    fn slash_mdy_hms(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
995        let re: &Regex = regex! {
996                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)?$"
997        };
998        if !re.is_match(input) {
999            return None;
1000        }
1001
1002        // Dispatch on year width (see slash_year_is_two_digits) and on the
1003        // time shape (see time_shape) instead of trying all 10 formats. This
1004        // picks the one format that can match, so `MM/DD/YYYY hh:mm:ss AM/PM`
1005        // no longer burns three guaranteed-failing attempts before the fourth.
1006        let items = match (
1007            slash_year_is_two_digits(input.as_bytes()),
1008            time_shape(input),
1009        ) {
1010            (true, TimeShape::Hms) => fmt_items!("%m/%d/%y %H:%M:%S"),
1011            (true, TimeShape::Hm) => fmt_items!("%m/%d/%y %H:%M"),
1012            (true, TimeShape::HmsF) => fmt_items!("%m/%d/%y %H:%M:%S%.f"),
1013            (true, TimeShape::ImsP | TimeShape::HmsFP) => {
1014                fmt_items!("%m/%d/%y %I:%M:%S%.f %P")
1015            }
1016            (true, TimeShape::ImP) => fmt_items!("%m/%d/%y %I:%M %P"),
1017            (false, TimeShape::Hms) => fmt_items!("%m/%d/%Y %H:%M:%S"),
1018            (false, TimeShape::Hm) => fmt_items!("%m/%d/%Y %H:%M"),
1019            (false, TimeShape::HmsF) => fmt_items!("%m/%d/%Y %H:%M:%S%.f"),
1020            (false, TimeShape::ImsP | TimeShape::HmsFP) => {
1021                fmt_items!("%m/%d/%Y %I:%M:%S%.f %P")
1022            }
1023            (false, TimeShape::ImP) => fmt_items!("%m/%d/%Y %I:%M %P"),
1024        };
1025        self.dt_from_items(input, items)
1026            .ok()
1027            .map(|at_tz| at_tz.with_timezone(&Utc))
1028            .map(Ok)
1029    }
1030
1031    // dd/mm/yyyy hh:mm:ss
1032    // - 8/4/2014 22:05
1033    // - 08/04/2014 22:05
1034    // - 8/4/14 22:05
1035    // - 2/04/2014 03:00:51
1036    // - 8/8/1965 12:00:00 AM
1037    // - 8/8/1965 01:00:01 PM
1038    // - 8/8/1965 01:00 PM
1039    // - 8/8/1965 1:00 PM
1040    // - 8/8/1965 12:00 AM
1041    // - 02/4/2014 03:00:51
1042    // - 19/03/2012 10:11:59
1043    // - 19/03/2012 10:11:59.3186369
1044    // - 19/03/2012 10:11:59.318 PM
1045    #[inline]
1046    fn slash_dmy_hms(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
1047        let re: &Regex = regex! {
1048                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)?$"
1049        };
1050        if !re.is_match(input) {
1051            return None;
1052        }
1053
1054        // Dispatch on year width and time shape — see the twin comment in
1055        // slash_mdy_hms.
1056        let items = match (
1057            slash_year_is_two_digits(input.as_bytes()),
1058            time_shape(input),
1059        ) {
1060            (true, TimeShape::Hms) => fmt_items!("%d/%m/%y %H:%M:%S"),
1061            (true, TimeShape::Hm) => fmt_items!("%d/%m/%y %H:%M"),
1062            (true, TimeShape::HmsF) => fmt_items!("%d/%m/%y %H:%M:%S%.f"),
1063            (true, TimeShape::ImsP | TimeShape::HmsFP) => {
1064                fmt_items!("%d/%m/%y %I:%M:%S%.f %P")
1065            }
1066            (true, TimeShape::ImP) => fmt_items!("%d/%m/%y %I:%M %P"),
1067            (false, TimeShape::Hms) => fmt_items!("%d/%m/%Y %H:%M:%S"),
1068            (false, TimeShape::Hm) => fmt_items!("%d/%m/%Y %H:%M"),
1069            (false, TimeShape::HmsF) => fmt_items!("%d/%m/%Y %H:%M:%S%.f"),
1070            (false, TimeShape::ImsP | TimeShape::HmsFP) => {
1071                fmt_items!("%d/%m/%Y %I:%M:%S%.f %P")
1072            }
1073            (false, TimeShape::ImP) => fmt_items!("%d/%m/%Y %I:%M %P"),
1074        };
1075        self.dt_from_items(input, items)
1076            .ok()
1077            .map(|at_tz| at_tz.with_timezone(&Utc))
1078            .map(Ok)
1079    }
1080
1081    // mm/dd/yyyy
1082    // - 3/31/2014
1083    // - 03/31/2014
1084    // - 08/21/71
1085    // - 8/1/71
1086    #[inline]
1087    fn slash_mdy(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
1088        let re: &Regex = regex! {r"^\d{1,2}/\d{1,2}/\d{2,4}$"
1089        };
1090        if !re.is_match(input) {
1091            return None;
1092        }
1093
1094        let now = Utc::now()
1095            .date()
1096            .and_time(self.default_time)?
1097            .with_timezone(self.tz);
1098        let fmt = if slash_year_is_two_digits(input.as_bytes()) {
1099            fmt_items!("%m/%d/%y")
1100        } else {
1101            fmt_items!("%m/%d/%Y")
1102        };
1103        Self::naive_date_from_items(input, fmt)
1104            .ok()
1105            .map(|parsed| parsed.and_time(now.time()))
1106            .and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
1107            .map(|at_tz| at_tz.with_timezone(&Utc))
1108            .map(Ok)
1109    }
1110
1111    // dd/mm/yyyy
1112    // - 31/3/2014
1113    // - 31/03/2014
1114    // - 21/08/71
1115    // - 1/8/71
1116    #[inline]
1117    fn slash_dmy(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
1118        let re: &Regex = regex! {r"^[0-9]{1,2}/[0-9]{1,2}/[0-9]{2,4}$"
1119        };
1120        if !re.is_match(input) {
1121            return None;
1122        }
1123
1124        let now = Utc::now()
1125            .date()
1126            .and_time(self.default_time)?
1127            .with_timezone(self.tz);
1128        let fmt = if slash_year_is_two_digits(input.as_bytes()) {
1129            fmt_items!("%d/%m/%y")
1130        } else {
1131            fmt_items!("%d/%m/%Y")
1132        };
1133        Self::naive_date_from_items(input, fmt)
1134            .ok()
1135            .map(|parsed| parsed.and_time(now.time()))
1136            .and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
1137            .map(|at_tz| at_tz.with_timezone(&Utc))
1138            .map(Ok)
1139    }
1140
1141    // yyyy/mm/dd hh:mm:ss
1142    // - 2014/4/8 22:05
1143    // - 2014/04/08 22:05
1144    // - 2014/04/2 03:00:51
1145    // - 2014/4/02 03:00:51
1146    // - 2012/03/19 10:11:59
1147    // - 2012/03/19 10:11:59.3186369
1148    // - 2012/03/19 10:11:59.318 PM
1149    #[inline]
1150    fn slash_ymd_hms(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
1151        let re: &Regex = regex! {
1152                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)?$"
1153        };
1154        if !re.is_match(input) {
1155            return None;
1156        }
1157
1158        let items = match time_shape(input) {
1159            TimeShape::Hms => fmt_items!("%Y/%m/%d %H:%M:%S"),
1160            TimeShape::Hm => fmt_items!("%Y/%m/%d %H:%M"),
1161            TimeShape::HmsF => fmt_items!("%Y/%m/%d %H:%M:%S%.f"),
1162            TimeShape::ImsP | TimeShape::HmsFP => fmt_items!("%Y/%m/%d %I:%M:%S%.f %P"),
1163            TimeShape::ImP => fmt_items!("%Y/%m/%d %I:%M %P"),
1164        };
1165        self.dt_from_items(input, items)
1166            .ok()
1167            .map(|at_tz| at_tz.with_timezone(&Utc))
1168            .map(Ok)
1169    }
1170
1171    // yyyy/mm/dd
1172    // - 2014/3/31
1173    // - 2014/03/31
1174    #[inline]
1175    fn slash_ymd(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
1176        let re: &Regex = regex! {r"^[0-9]{4}/[0-9]{1,2}/[0-9]{1,2}$"
1177        };
1178        if !re.is_match(input) {
1179            return None;
1180        }
1181
1182        let now = Utc::now()
1183            .date()
1184            .and_time(self.default_time)?
1185            .with_timezone(self.tz);
1186        Self::naive_date_from_items(input, fmt_items!("%Y/%m/%d"))
1187            .ok()
1188            .map(|parsed| parsed.and_time(now.time()))
1189            .and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
1190            .map(|at_tz| at_tz.with_timezone(&Utc))
1191            .map(Ok)
1192    }
1193}
1194
1195#[cfg(test)]
1196mod tests {
1197    use super::*;
1198
1199    #[test]
1200    fn unix_timestamp() {
1201        let parse = Parse::new(&Utc, Utc::now().time());
1202
1203        let test_cases = vec![
1204            ("0", Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)),
1205            ("0000000000", Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)),
1206            ("0000000000000", Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)),
1207            ("0000000000000000000", Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)),
1208            ("-770172300", Utc.ymd(1945, 8, 5).and_hms(23, 15, 0)),
1209            (
1210                "1671673426.123456789",
1211                Utc.ymd(2022, 12, 22).and_hms_nano(1, 43, 46, 123456768),
1212            ),
1213            ("1511648546", Utc.ymd(2017, 11, 25).and_hms(22, 22, 26)),
1214            (
1215                "1620036248.420",
1216                Utc.ymd(2021, 5, 3).and_hms_milli(10, 4, 8, 420),
1217            ),
1218            (
1219                "1620036248.717915136",
1220                Utc.ymd(2021, 5, 3).and_hms_nano(10, 4, 8, 717915136),
1221            ),
1222        ];
1223
1224        for &(input, want) in test_cases.iter() {
1225            assert_eq!(
1226                parse.unix_timestamp(input).unwrap().unwrap(),
1227                want,
1228                "unix_timestamp/{}",
1229                input
1230            )
1231        }
1232        assert!(parse.unix_timestamp("15116").is_some());
1233        assert!(
1234            parse
1235                .unix_timestamp("16200248727179150001620024872717915000") //DevSkim: ignore DS173237
1236                .is_some()
1237        );
1238        assert!(parse.unix_timestamp("not-a-ts").is_none());
1239        // Non-finite floats must be rejected, whether caught by the lead-byte
1240        // pre-filter (bare `inf`/`nan`) or the is_finite check (signed forms).
1241        for input in [
1242            "inf", "nan", "INF", "NaN", "infinity", "+inf", "-inf", "-nan",
1243        ] {
1244            assert!(
1245                parse.unix_timestamp(input).is_none(),
1246                "unix_timestamp must reject non-finite {input}"
1247            );
1248        }
1249    }
1250
1251    #[test]
1252    fn rfc3339() {
1253        let parse = Parse::new(&Utc, Utc::now().time());
1254
1255        let test_cases = [
1256            (
1257                "2021-05-01T01:17:02.604456Z",
1258                Utc.ymd(2021, 5, 1).and_hms_nano(1, 17, 2, 604456000),
1259            ),
1260            (
1261                "2017-11-25T22:34:50Z",
1262                Utc.ymd(2017, 11, 25).and_hms(22, 34, 50),
1263            ),
1264        ];
1265
1266        for &(input, want) in test_cases.iter() {
1267            assert_eq!(
1268                parse.rfc3339(input).unwrap().unwrap(),
1269                want,
1270                "rfc3339/{}",
1271                input
1272            )
1273        }
1274        assert!(parse.rfc3339("2017-11-25 22:34:50").is_none());
1275        assert!(parse.rfc3339("not-date-time").is_none());
1276    }
1277
1278    #[test]
1279    fn rfc2822() {
1280        let parse = Parse::new(&Utc, Utc::now().time());
1281
1282        let test_cases = [
1283            (
1284                "Wed, 02 Jun 2021 06:31:39 GMT",
1285                Utc.ymd(2021, 6, 2).and_hms(6, 31, 39),
1286            ),
1287            (
1288                "Wed, 02 Jun 2021 06:31:39 PDT",
1289                Utc.ymd(2021, 6, 2).and_hms(13, 31, 39),
1290            ),
1291        ];
1292
1293        for &(input, want) in test_cases.iter() {
1294            assert_eq!(
1295                parse.rfc2822(input).unwrap().unwrap(),
1296                want,
1297                "rfc2822/{}",
1298                input
1299            )
1300        }
1301        assert!(parse.rfc2822("02 Jun 2021 06:31:39").is_none());
1302        assert!(parse.rfc2822("not-date-time").is_none());
1303    }
1304
1305    #[test]
1306    fn ymd_hms() {
1307        let parse = Parse::new(&Utc, Utc::now().time());
1308
1309        let test_cases = [
1310            ("2021-04-30 21:14", Utc.ymd(2021, 4, 30).and_hms(21, 14, 0)),
1311            (
1312                "2021-04-30 21:14:10",
1313                Utc.ymd(2021, 4, 30).and_hms(21, 14, 10),
1314            ),
1315            (
1316                "2021-04-30 21:14:10.052282",
1317                Utc.ymd(2021, 4, 30).and_hms_micro(21, 14, 10, 52282),
1318            ),
1319            (
1320                "2014-04-26 05:24:37 PM",
1321                Utc.ymd(2014, 4, 26).and_hms(17, 24, 37),
1322            ),
1323            (
1324                "2014-04-26 17:24:37.123",
1325                Utc.ymd(2014, 4, 26).and_hms_milli(17, 24, 37, 123),
1326            ),
1327            (
1328                "2014-04-26 17:24:37.3186369",
1329                Utc.ymd(2014, 4, 26).and_hms_nano(17, 24, 37, 318636900),
1330            ),
1331            (
1332                "2012-08-03 18:31:59.257000000",
1333                Utc.ymd(2012, 8, 3).and_hms_nano(18, 31, 59, 257000000),
1334            ),
1335            // ISO 8601 with 'T' separator and no timezone (naive wall-clock).
1336            // Must agree with the space-separated form on the same wall-clock instant.
1337            ("2020-01-15T08:00", Utc.ymd(2020, 1, 15).and_hms(8, 0, 0)),
1338            ("2020-01-15T08:00:00", Utc.ymd(2020, 1, 15).and_hms(8, 0, 0)),
1339            (
1340                "2020-01-15T08:00:00.123",
1341                Utc.ymd(2020, 1, 15).and_hms_milli(8, 0, 0, 123),
1342            ),
1343            (
1344                "2020-01-15T08:00:00.123456",
1345                Utc.ymd(2020, 1, 15).and_hms_micro(8, 0, 0, 123456),
1346            ),
1347            (
1348                "2020-01-15T08:00:00.123456789",
1349                Utc.ymd(2020, 1, 15).and_hms_nano(8, 0, 0, 123456789),
1350            ),
1351        ];
1352
1353        for &(input, want) in test_cases.iter() {
1354            assert_eq!(
1355                parse.ymd_hms(input).unwrap().unwrap(),
1356                want,
1357                "ymd_hms/{}",
1358                input
1359            )
1360        }
1361        assert!(parse.ymd_hms("not-date-time").is_none());
1362
1363        // T and space separators must produce the same instant.
1364        let t_form = parse.ymd_hms("2020-01-15T08:00:00").unwrap().unwrap();
1365        let space_form = parse.ymd_hms("2020-01-15 08:00:00").unwrap().unwrap();
1366        assert_eq!(t_form, space_form, "T-separator vs space disagree");
1367    }
1368
1369    #[test]
1370    fn ymd_hms_z() {
1371        let parse = Parse::new(&Utc, Utc::now().time());
1372
1373        let test_cases = [
1374            (
1375                "2017-11-25 13:31:15 PST",
1376                Utc.ymd(2017, 11, 25).and_hms(21, 31, 15),
1377            ),
1378            (
1379                "2017-11-25 13:31 PST",
1380                Utc.ymd(2017, 11, 25).and_hms(21, 31, 0),
1381            ),
1382            (
1383                "2014-12-16 06:20:00 UTC",
1384                Utc.ymd(2014, 12, 16).and_hms(6, 20, 0),
1385            ),
1386            (
1387                "2014-12-16 06:20:00 GMT",
1388                Utc.ymd(2014, 12, 16).and_hms(6, 20, 0),
1389            ),
1390            (
1391                "2014-04-26 13:13:43 +0800",
1392                Utc.ymd(2014, 4, 26).and_hms(5, 13, 43),
1393            ),
1394            (
1395                "2014-04-26 13:13:44 +09:00",
1396                Utc.ymd(2014, 4, 26).and_hms(4, 13, 44),
1397            ),
1398            (
1399                "2012-08-03 18:31:59.257000000 +0000",
1400                Utc.ymd(2012, 8, 3).and_hms_nano(18, 31, 59, 257000000),
1401            ),
1402            (
1403                "2015-09-30 18:48:56.35272715 UTC",
1404                Utc.ymd(2015, 9, 30).and_hms_nano(18, 48, 56, 352727150),
1405            ),
1406        ];
1407
1408        for &(input, want) in test_cases.iter() {
1409            assert_eq!(
1410                parse.ymd_hms_z(input).unwrap().unwrap(),
1411                want,
1412                "ymd_hms_z/{}",
1413                input
1414            )
1415        }
1416        assert!(parse.ymd_hms_z("not-date-time").is_none());
1417        // Pre-filter boundary: exactly 16 chars is rejected by length guard (< 17)
1418        assert!(parse.ymd_hms_z("2021-04-30 21:14").is_none()); // 16 chars, rejected by length guard
1419        // 17 chars but byte[10] is not whitespace — rejected by whitespace check
1420        assert!(parse.ymd_hms_z("2021-04-30X21:14Z").is_none()); // 17 chars, byte[10]='X' not space
1421        // 17 chars with whitespace at byte[10] proceeds to regex but regex rejects malformed input
1422        assert!(parse.ymd_hms_z("2021-04-30 21:1XZ").is_none()); // 17 chars, byte[10]=' ', regex rejects
1423    }
1424
1425    #[test]
1426    fn ymd() {
1427        let parse = Parse::new(&Utc, Utc::now().time());
1428
1429        let test_cases = [(
1430            "2021-02-21",
1431            Utc.ymd(2021, 2, 21).and_time(Utc::now().time()),
1432        )];
1433
1434        for &(input, want) in test_cases.iter() {
1435            assert_eq!(
1436                parse
1437                    .ymd(input)
1438                    .unwrap()
1439                    .unwrap()
1440                    .trunc_subsecs(0)
1441                    .with_second(0)
1442                    .unwrap(),
1443                want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
1444                "ymd/{}",
1445                input
1446            )
1447        }
1448        assert!(parse.ymd("not-date-time").is_none());
1449    }
1450
1451    #[test]
1452    fn ymd_z() {
1453        let parse = Parse::new(&Utc, Utc::now().time());
1454        let now_at_pst = Utc::now().with_timezone(&FixedOffset::west(8 * 3600));
1455        let now_at_cst = Utc::now().with_timezone(&FixedOffset::east(8 * 3600));
1456
1457        let test_cases = [
1458            (
1459                "2021-02-21 PST",
1460                FixedOffset::west(8 * 3600)
1461                    .ymd(2021, 2, 21)
1462                    .and_time(now_at_pst.time())
1463                    .map(|dt| dt.with_timezone(&Utc)),
1464            ),
1465            (
1466                "2021-02-21 UTC",
1467                FixedOffset::west(0)
1468                    .ymd(2021, 2, 21)
1469                    .and_time(Utc::now().time())
1470                    .map(|dt| dt.with_timezone(&Utc)),
1471            ),
1472            (
1473                "2020-07-20+08:00",
1474                FixedOffset::east(8 * 3600)
1475                    .ymd(2020, 7, 20)
1476                    .and_time(now_at_cst.time())
1477                    .map(|dt| dt.with_timezone(&Utc)),
1478            ),
1479        ];
1480
1481        for &(input, want) in test_cases.iter() {
1482            assert_eq!(
1483                parse
1484                    .ymd_z(input)
1485                    .unwrap()
1486                    .unwrap()
1487                    .trunc_subsecs(0)
1488                    .with_second(0)
1489                    .unwrap(),
1490                want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
1491                "ymd_z/{}",
1492                input
1493            )
1494        }
1495        assert!(parse.ymd_z("not-date-time").is_none());
1496        // Pre-filter boundary: exactly 10 chars (bare date) is rejected (<= 10 guard), 11+ proceeds
1497        assert!(parse.ymd_z("2021-02-21").is_none()); // exactly 10 chars, rejected
1498        assert!(parse.ymd_z("2021-02-21X").is_none()); // 11 chars, proceeds to regex but regex rejects
1499    }
1500
1501    #[test]
1502    fn month_ymd() {
1503        let parse = Parse::new(&Utc, Utc::now().time());
1504
1505        let test_cases = [(
1506            "2021-Feb-21",
1507            Utc.ymd(2021, 2, 21).and_time(Utc::now().time()),
1508        )];
1509
1510        for &(input, want) in test_cases.iter() {
1511            assert_eq!(
1512                parse
1513                    .month_ymd(input)
1514                    .unwrap()
1515                    .unwrap()
1516                    .trunc_subsecs(0)
1517                    .with_second(0)
1518                    .unwrap(),
1519                want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
1520                "month_ymd/{}",
1521                input
1522            )
1523        }
1524        assert!(parse.month_ymd("not-date-time").is_none());
1525    }
1526
1527    #[test]
1528    fn month_mdy_hms() {
1529        let parse = Parse::new(&Utc, Utc::now().time());
1530
1531        let test_cases = [
1532            (
1533                "May 8, 2009 5:57:51 PM",
1534                Utc.ymd(2009, 5, 8).and_hms(17, 57, 51),
1535            ),
1536            (
1537                "September 17, 2012 10:09am",
1538                Utc.ymd(2012, 9, 17).and_hms(10, 9, 0),
1539            ),
1540            (
1541                "September 17, 2012, 10:10:09",
1542                Utc.ymd(2012, 9, 17).and_hms(10, 10, 9),
1543            ),
1544        ];
1545
1546        for &(input, want) in test_cases.iter() {
1547            assert_eq!(
1548                parse.month_mdy_hms(input).unwrap().unwrap(),
1549                want,
1550                "month_mdy_hms/{}",
1551                input
1552            )
1553        }
1554        assert!(parse.month_mdy_hms("not-date-time").is_none());
1555    }
1556
1557    #[test]
1558    fn month_mdy_hms_z() {
1559        let parse = Parse::new(&Utc, Utc::now().time());
1560
1561        let test_cases = [
1562            (
1563                "May 02, 2021 15:51:31 UTC",
1564                Utc.ymd(2021, 5, 2).and_hms(15, 51, 31),
1565            ),
1566            (
1567                "May 02, 2021 15:51 UTC",
1568                Utc.ymd(2021, 5, 2).and_hms(15, 51, 0),
1569            ),
1570            (
1571                "May 26, 2021, 12:49 AM PDT",
1572                Utc.ymd(2021, 5, 26).and_hms(7, 49, 0),
1573            ),
1574            (
1575                "September 17, 2012 at 10:09am PST",
1576                Utc.ymd(2012, 9, 17).and_hms(18, 9, 0),
1577            ),
1578        ];
1579
1580        for &(input, want) in test_cases.iter() {
1581            assert_eq!(
1582                parse.month_mdy_hms_z(input).unwrap().unwrap(),
1583                want,
1584                "month_mdy_hms_z/{}",
1585                input
1586            )
1587        }
1588        assert!(parse.month_mdy_hms_z("not-date-time").is_none());
1589        // Pre-filter: 20+ chars required; no isolated 4-digit year → has_year=false, rejected
1590        assert!(parse.month_mdy_hms_z("May 27, 02:45:27 XX PST").is_none()); // 23 chars, no 4-digit year
1591        // Pre-filter: 20+ chars with isolated 4-digit sequence → has_year=true, regex rejects format
1592        assert!(parse.month_mdy_hms_z("May 27 1234 something PST").is_none()); // 25 chars, has_year=true but regex rejects
1593    }
1594
1595    #[test]
1596    fn month_mdy() {
1597        let parse = Parse::new(&Utc, Utc::now().time());
1598
1599        let test_cases = [
1600            (
1601                "May 25, 2021",
1602                Utc.ymd(2021, 5, 25).and_time(Utc::now().time()),
1603            ),
1604            (
1605                "oct 7, 1970",
1606                Utc.ymd(1970, 10, 7).and_time(Utc::now().time()),
1607            ),
1608            (
1609                "oct 7, 70",
1610                Utc.ymd(1970, 10, 7).and_time(Utc::now().time()),
1611            ),
1612            (
1613                "oct. 7, 1970",
1614                Utc.ymd(1970, 10, 7).and_time(Utc::now().time()),
1615            ),
1616            (
1617                "oct. 7, 70",
1618                Utc.ymd(1970, 10, 7).and_time(Utc::now().time()),
1619            ),
1620            (
1621                "October 7, 1970",
1622                Utc.ymd(1970, 10, 7).and_time(Utc::now().time()),
1623            ),
1624        ];
1625
1626        for &(input, want) in test_cases.iter() {
1627            assert_eq!(
1628                parse
1629                    .month_mdy(input)
1630                    .unwrap()
1631                    .unwrap()
1632                    .trunc_subsecs(0)
1633                    .with_second(0)
1634                    .unwrap(),
1635                want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
1636                "month_mdy/{}",
1637                input
1638            )
1639        }
1640        assert!(parse.month_mdy("not-date-time").is_none());
1641    }
1642
1643    #[test]
1644    fn month_dmy_hms() {
1645        let parse = Parse::new(&Utc, Utc::now().time());
1646
1647        let test_cases = [
1648            (
1649                "12 Feb 2006, 19:17",
1650                Utc.ymd(2006, 2, 12).and_hms(19, 17, 0),
1651            ),
1652            ("12 Feb 2006 19:17", Utc.ymd(2006, 2, 12).and_hms(19, 17, 0)),
1653            (
1654                "14 May 2019 19:11:40.164",
1655                Utc.ymd(2019, 5, 14).and_hms_milli(19, 11, 40, 164),
1656            ),
1657        ];
1658
1659        for &(input, want) in test_cases.iter() {
1660            assert_eq!(
1661                parse.month_dmy_hms(input).unwrap().unwrap(),
1662                want,
1663                "month_dmy_hms/{}",
1664                input
1665            )
1666        }
1667        assert!(parse.month_dmy_hms("not-date-time").is_none());
1668    }
1669
1670    #[test]
1671    fn month_dmy() {
1672        let parse = Parse::new(&Utc, Utc::now().time());
1673
1674        let test_cases = [
1675            ("7 oct 70", Utc.ymd(1970, 10, 7).and_time(Utc::now().time())),
1676            (
1677                "7 oct 1970",
1678                Utc.ymd(1970, 10, 7).and_time(Utc::now().time()),
1679            ),
1680            (
1681                "03 February 2013",
1682                Utc.ymd(2013, 2, 3).and_time(Utc::now().time()),
1683            ),
1684            (
1685                "1 July 2013",
1686                Utc.ymd(2013, 7, 1).and_time(Utc::now().time()),
1687            ),
1688        ];
1689
1690        for &(input, want) in test_cases.iter() {
1691            assert_eq!(
1692                parse
1693                    .month_dmy(input)
1694                    .unwrap()
1695                    .unwrap()
1696                    .trunc_subsecs(0)
1697                    .with_second(0)
1698                    .unwrap(),
1699                want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
1700                "month_dmy/{}",
1701                input
1702            )
1703        }
1704        assert!(parse.month_dmy("not-date-time").is_none());
1705    }
1706
1707    // Explicitly tests the `four_digit_year` fast path in `month_dmy` (skips `%d %B %y`) and
1708    // the else-branch fallback that tries `%d %B %y` first then `%d %B %Y`.
1709    #[test]
1710    fn month_dmy_year_fast_path() {
1711        let parse = Parse::new(&Utc, Utc::now().time());
1712
1713        // Fast path: 4-digit year — `four_digit_year` is true, goes directly to `%d %B %Y`
1714        let four_digit = parse.month_dmy("14 May 2019").unwrap().unwrap();
1715        assert_eq!(four_digit.year(), 2019);
1716        assert_eq!(four_digit.month(), 5);
1717        assert_eq!(four_digit.day(), 14);
1718
1719        // Else-branch: 2-digit year — `four_digit_year` is false, tries `%d %B %y` first
1720        // chrono %y: 00–68 → 2000–2068, so "19" → 2019 (not 1919)
1721        let two_digit = parse.month_dmy("14 May 19").unwrap().unwrap();
1722        assert_eq!(two_digit.year(), 2019);
1723        assert_eq!(two_digit.month(), 5);
1724        assert_eq!(two_digit.day(), 14);
1725    }
1726
1727    #[test]
1728    fn slash_mdy_hms() {
1729        let parse = Parse::new(&Utc, Utc::now().time());
1730
1731        let test_cases = vec![
1732            ("4/8/2014 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)),
1733            ("04/08/2014 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)),
1734            ("4/8/14 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)),
1735            ("04/2/2014 03:00:51", Utc.ymd(2014, 4, 2).and_hms(3, 0, 51)),
1736            ("8/8/1965 12:00:00 AM", Utc.ymd(1965, 8, 8).and_hms(0, 0, 0)),
1737            (
1738                "8/8/1965 01:00:01 PM",
1739                Utc.ymd(1965, 8, 8).and_hms(13, 0, 1),
1740            ),
1741            ("8/8/1965 01:00 PM", Utc.ymd(1965, 8, 8).and_hms(13, 0, 0)),
1742            ("8/8/1965 1:00 PM", Utc.ymd(1965, 8, 8).and_hms(13, 0, 0)),
1743            ("8/8/1965 12:00 AM", Utc.ymd(1965, 8, 8).and_hms(0, 0, 0)),
1744            ("4/02/2014 03:00:51", Utc.ymd(2014, 4, 2).and_hms(3, 0, 51)),
1745            (
1746                "03/19/2012 10:11:59",
1747                Utc.ymd(2012, 3, 19).and_hms(10, 11, 59),
1748            ),
1749            (
1750                "03/19/2012 10:11:59.3186369",
1751                Utc.ymd(2012, 3, 19).and_hms_nano(10, 11, 59, 318636900),
1752            ),
1753        ];
1754
1755        for &(input, want) in test_cases.iter() {
1756            assert_eq!(
1757                parse.slash_mdy_hms(input).unwrap().unwrap(),
1758                want,
1759                "slash_mdy_hms/{}",
1760                input
1761            )
1762        }
1763        assert!(parse.slash_mdy_hms("not-date-time").is_none());
1764    }
1765
1766    #[test]
1767    fn slash_mdy() {
1768        let parse = Parse::new(&Utc, Utc::now().time());
1769
1770        let test_cases = [
1771            (
1772                "3/31/2014",
1773                Utc.ymd(2014, 3, 31).and_time(Utc::now().time()),
1774            ),
1775            (
1776                "03/31/2014",
1777                Utc.ymd(2014, 3, 31).and_time(Utc::now().time()),
1778            ),
1779            ("08/21/71", Utc.ymd(1971, 8, 21).and_time(Utc::now().time())),
1780            ("8/1/71", Utc.ymd(1971, 8, 1).and_time(Utc::now().time())),
1781        ];
1782
1783        for &(input, want) in test_cases.iter() {
1784            assert_eq!(
1785                parse
1786                    .slash_mdy(input)
1787                    .unwrap()
1788                    .unwrap()
1789                    .trunc_subsecs(0)
1790                    .with_second(0)
1791                    .unwrap(),
1792                want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
1793                "slash_mdy/{}",
1794                input
1795            )
1796        }
1797        assert!(parse.slash_mdy("not-date-time").is_none());
1798    }
1799
1800    #[test]
1801    fn slash_dmy() {
1802        let mut parse = Parse::new(&Utc, Utc::now().time());
1803
1804        let test_cases = [
1805            (
1806                "31/3/2014",
1807                Utc.ymd(2014, 3, 31).and_time(Utc::now().time()),
1808            ),
1809            (
1810                "13/11/2014",
1811                Utc.ymd(2014, 11, 13).and_time(Utc::now().time()),
1812            ),
1813            ("21/08/71", Utc.ymd(1971, 8, 21).and_time(Utc::now().time())),
1814            ("1/8/71", Utc.ymd(1971, 8, 1).and_time(Utc::now().time())),
1815        ];
1816
1817        for &(input, want) in test_cases.iter() {
1818            assert_eq!(
1819                parse
1820                    .prefer_dmy(true)
1821                    .slash_dmy(input)
1822                    .unwrap()
1823                    .unwrap()
1824                    .trunc_subsecs(0)
1825                    .with_second(0)
1826                    .unwrap(),
1827                want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
1828                "slash_dmy/{}",
1829                input
1830            )
1831        }
1832        assert!(parse.slash_dmy("not-date-time").is_none());
1833    }
1834
1835    #[test]
1836    fn slash_ymd_hms() {
1837        let parse = Parse::new(&Utc, Utc::now().time());
1838
1839        let test_cases = [
1840            ("2014/4/8 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)),
1841            ("2014/04/08 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)),
1842            ("2014/04/2 03:00:51", Utc.ymd(2014, 4, 2).and_hms(3, 0, 51)),
1843            ("2014/4/02 03:00:51", Utc.ymd(2014, 4, 2).and_hms(3, 0, 51)),
1844            (
1845                "2012/03/19 10:11:59",
1846                Utc.ymd(2012, 3, 19).and_hms(10, 11, 59),
1847            ),
1848            (
1849                "2012/03/19 10:11:59.3186369",
1850                Utc.ymd(2012, 3, 19).and_hms_nano(10, 11, 59, 318636900),
1851            ),
1852        ];
1853
1854        for &(input, want) in test_cases.iter() {
1855            assert_eq!(
1856                parse.slash_ymd_hms(input).unwrap().unwrap(),
1857                want,
1858                "slash_ymd_hms/{}",
1859                input
1860            )
1861        }
1862        assert!(parse.slash_ymd_hms("not-date-time").is_none());
1863    }
1864
1865    #[test]
1866    fn slash_ymd() {
1867        let parse = Parse::new(&Utc, Utc::now().time());
1868
1869        let test_cases = [
1870            (
1871                "2014/3/31",
1872                Utc.ymd(2014, 3, 31).and_time(Utc::now().time()),
1873            ),
1874            (
1875                "2014/03/31",
1876                Utc.ymd(2014, 3, 31).and_time(Utc::now().time()),
1877            ),
1878        ];
1879
1880        for &(input, want) in test_cases.iter() {
1881            assert_eq!(
1882                parse
1883                    .slash_ymd(input)
1884                    .unwrap()
1885                    .unwrap()
1886                    .trunc_subsecs(0)
1887                    .with_second(0)
1888                    .unwrap(),
1889                want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
1890                "slash_ymd/{}",
1891                input
1892            )
1893        }
1894        assert!(parse.slash_ymd("not-date-time").is_none());
1895    }
1896
1897    #[test]
1898    fn time_shape_classification() {
1899        use TimeShape::{Hm, Hms, HmsF, HmsFP, ImP, ImsP};
1900
1901        let cases = [
1902            ("2021-04-30 21:14", Hm),
1903            ("2021-04-30T21:14", Hm),
1904            ("2021-04-30 21:14:10", Hms),
1905            ("2021-04-30 21:14:10.052282", HmsF),
1906            ("8/8/1965 12:00 AM", ImP),
1907            ("8/8/1965 12:00am", ImP),
1908            ("8/8/1965 01:00:01 PM", ImsP),
1909            ("September 17 2012 10:09am", ImP),
1910            ("03/19/2012 10:11:59.318 PM", HmsFP),
1911            // A one-colon time with a fraction has no format either; it lands
1912            // in a shape whose format still rejects it, so it keeps failing.
1913            ("03/19/2012 10:11.123", Hm),
1914        ];
1915        for (input, want) in cases {
1916            assert!(
1917                time_shape(input) == want,
1918                "time_shape misclassified {input}"
1919            );
1920        }
1921    }
1922
1923    /// Every `fmt_items!` literal in this file must be a valid strftime format.
1924    ///
1925    /// The macro compiles its literal lazily on first use and `expect`s the
1926    /// result, so an invalid format would panic the first time some input
1927    /// happened to reach that particular link of an `or_else` chain — possibly
1928    /// only in production. The literals are extracted from this file's own
1929    /// source rather than re-listed here, so the check cannot drift out of
1930    /// sync as parsers are added or reworked.
1931    #[test]
1932    fn every_fmt_items_literal_is_valid() {
1933        let src = include_str!("datetime.rs");
1934        // Matches uses, not the macro definition (which has no `!`).
1935        let call = Regex::new(r#"fmt_items!\("([^"]*)"\)"#).unwrap();
1936
1937        let mut checked = 0_usize;
1938        for caps in call.captures_iter(src) {
1939            let fmt = &caps[1];
1940            assert!(
1941                chrono::format::StrftimeItems::new(fmt).parse().is_ok(),
1942                "invalid strftime literal: {fmt}"
1943            );
1944            checked += 1;
1945        }
1946        // Guard against the extraction silently matching nothing.
1947        assert!(
1948            checked >= 50,
1949            "expected to check every fmt_items! literal, only found {checked}"
1950        );
1951    }
1952
1953    /// Fractional seconds combined with an AM/PM marker (issue #12).
1954    ///
1955    /// The four families whose regex admits both a fraction and an AM/PM
1956    /// marker parse this with `%I:%M:%S%.f %P`. That one format also covers
1957    /// the fraction-less `ImsP` shape, because `%.f` consumes nothing when
1958    /// there is no period, so it replaced `%I:%M:%S %P` rather than joining
1959    /// it — which is why the plain AM/PM cases are re-asserted here too.
1960    #[test]
1961    fn fractional_seconds_with_ampm() {
1962        let parse = Parse::new(&Utc, Utc::now().time());
1963
1964        let cases = [
1965            (
1966                "03/19/2012 10:11:59.318 PM",
1967                Utc.ymd(2012, 3, 19).and_hms_milli(22, 11, 59, 318),
1968            ),
1969            (
1970                "3/19/2012 1:11:59.318 am",
1971                Utc.ymd(2012, 3, 19).and_hms_milli(1, 11, 59, 318),
1972            ),
1973            (
1974                "03/19/12 10:11:59.318 PM",
1975                Utc.ymd(2012, 3, 19).and_hms_milli(22, 11, 59, 318),
1976            ),
1977            (
1978                "2012/03/19 10:11:59.318 PM",
1979                Utc.ymd(2012, 3, 19).and_hms_milli(22, 11, 59, 318),
1980            ),
1981            (
1982                "2012-03-19 10:11:59.318 PM",
1983                Utc.ymd(2012, 3, 19).and_hms_milli(22, 11, 59, 318),
1984            ),
1985            (
1986                "2012-03-19T10:11:59.318 PM",
1987                Utc.ymd(2012, 3, 19).and_hms_milli(22, 11, 59, 318),
1988            ),
1989            (
1990                "2012-03-19 10:11:59.3186369 PM",
1991                Utc.ymd(2012, 3, 19).and_hms_nano(22, 11, 59, 318636900),
1992            ),
1993            // Unchanged by the format swap: no period, so `%.f` matches empty.
1994            (
1995                "03/19/2012 10:11:59 PM",
1996                Utc.ymd(2012, 3, 19).and_hms(22, 11, 59),
1997            ),
1998            (
1999                "2012-03-19 10:11:59 PM",
2000                Utc.ymd(2012, 3, 19).and_hms(22, 11, 59),
2001            ),
2002        ];
2003
2004        for (input, want) in cases {
2005            assert_eq!(parse.parse(input).unwrap(), want, "parse/{input}");
2006        }
2007
2008        // slash_dmy_hms's arms are only reached day-first, so the cases above
2009        // never exercise them. Both year widths, since the parser dispatches
2010        // on year width before it dispatches on time shape.
2011        let dmy = Parse::new_with_preference(&Utc, Utc::now().time(), true);
2012        let dmy_cases = [
2013            (
2014                "19/03/2012 10:11:59.318 PM",
2015                Utc.ymd(2012, 3, 19).and_hms_milli(22, 11, 59, 318),
2016            ),
2017            (
2018                "19/03/12 10:11:59.318 PM",
2019                Utc.ymd(2012, 3, 19).and_hms_milli(22, 11, 59, 318),
2020            ),
2021            (
2022                "9/3/2012 1:11:59.318 am",
2023                Utc.ymd(2012, 3, 9).and_hms_milli(1, 11, 59, 318),
2024            ),
2025            // Fraction-less, to pin that the format swap left it alone.
2026            (
2027                "19/03/2012 10:11:59 PM",
2028                Utc.ymd(2012, 3, 19).and_hms(22, 11, 59),
2029            ),
2030        ];
2031
2032        for (input, want) in dmy_cases {
2033            assert_eq!(dmy.parse(input).unwrap(), want, "prefer_dmy/{input}");
2034        }
2035    }
2036
2037    /// Inputs that resemble an accepted shape but do not parse. Pinned so that
2038    /// format-chain refactors stay result-preserving: a classifier that newly
2039    /// accepts any of these has widened the accepted input set, which is a
2040    /// behavior change rather than a performance optimization.
2041    ///
2042    /// These are independent gaps, not one gap. Some are turned away by their
2043    /// family regex before any format is tried; others clear the regex and
2044    /// then match no format. Each case carries its own reason below.
2045    #[test]
2046    fn unsupported_shapes_still_fail() {
2047        let parse = Parse::new(&Utc, Utc::now().time());
2048
2049        for input in [
2050            // Rejected at the regex gate: month_mdy_hms has no
2051            // fractional-seconds group at all, and strips `.` before parsing.
2052            "May 8, 2009 5:57:51.123 PM",
2053            // Rejected at the regex gate: month_dmy_hms has no am/pm
2054            // alternative, which is also why that chain's two `%I ... %P`
2055            // formats were unreachable and have been removed. Note the first
2056            // of these carries no fractional seconds — it is the missing
2057            // am/pm alternative alone that rejects it.
2058            "14 May 2019 07:11:40 PM",
2059            "14 May 2019 07:11:40.164 PM",
2060            // Clears the regex, matches no format: a fraction with no seconds
2061            // field. The optional groups admit it, but it buckets as Hm/ImP
2062            // and fails there. Malformed rather than a real shape. Note the
2063            // first two carry no AM/PM marker.
2064            "03/19/2012 10:11.123",
2065            "2021-04-30 21:14.052282",
2066            "03/19/2012 10:11.123 PM",
2067            // Clears the regex, matches no format: `%I` only accepts a 1-12
2068            // hour, so a 24-hour reading cannot carry an AM/PM marker.
2069            "03/19/2012 22:11:59.318 PM",
2070        ] {
2071            assert!(
2072                parse.parse(input).is_err(),
2073                "{input} is expected to remain unsupported"
2074            );
2075        }
2076    }
2077}