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