Skip to main content

qsv_dateparser/
datetime.rs

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