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