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