Skip to main content

qsv_dateparser/
lib.rs

1//! A rust library for parsing date strings in commonly used formats. Parsed date will be returned
2//! as `chrono`'s `DateTime<Utc>`.
3//!
4//! # Quick Start
5//!
6//!
7//! Use `str`'s `parse` method:
8//!
9//! ```
10//! use chrono::prelude::*;
11//! use qsv_dateparser::DateTimeUtc;
12//! use std::error::Error;
13//!
14//! fn main() -> Result<(), Box<dyn Error>> {
15//!     assert_eq!(
16//!         "2021-05-14 18:51 PDT".parse::<DateTimeUtc>()?.0,
17//!         Utc.ymd(2021, 5, 15).and_hms(1, 51, 0),
18//!     );
19//!     Ok(())
20//! }
21//! ```
22//!
23//! ## Accepted date formats
24//!
25//! ```
26//! use qsv_dateparser::DateTimeUtc;
27//!
28//! let accepted = vec![
29//!     // unix timestamp
30//!     "1511648546",
31//!     "1620021848429",
32//!     "1620024872717915000",
33//!     "0",
34//!     "-770172300",
35//!     "1671673426.123456789",
36//!     // rfc3339
37//!     "2021-05-01T01:17:02.604456Z",
38//!     "2017-11-25T22:34:50Z",
39//!     // rfc2822
40//!     "Wed, 02 Jun 2021 06:31:39 GMT",
41//!     // yyyy-mm-dd hh:mm:ss
42//!     "2014-04-26 05:24:37 PM",
43//!     "2021-04-30 21:14",
44//!     "2021-04-30 21:14:10",
45//!     "2021-04-30 21:14:10.052282",
46//!     "2014-04-26 17:24:37.123",
47//!     "2014-04-26 17:24:37.3186369",
48//!     "2012-08-03 18:31:59.257000000",
49//!     // yyyy-mm-dd hh:mm:ss z
50//!     "2017-11-25 13:31:15 PST",
51//!     "2017-11-25 13:31 PST",
52//!     "2014-12-16 06:20:00 UTC",
53//!     "2014-12-16 06:20:00 GMT",
54//!     "2014-04-26 13:13:43 +0800",
55//!     "2014-04-26 13:13:44 +09:00",
56//!     "2012-08-03 18:31:59.257000000 +0000",
57//!     "2015-09-30 18:48:56.35272715 UTC",
58//!     // yyyy-mm-dd
59//!     "2021-02-21",
60//!     // yyyy-mm-dd z
61//!     "2021-02-21 PST",
62//!     "2021-02-21 UTC",
63//!     "2020-07-20+08:00",
64//!     // Mon dd, yyyy, hh:mm:ss
65//!     "May 8, 2009 5:57:51 PM",
66//!     "September 17, 2012 10:09am",
67//!     "September 17, 2012, 10:10:09",
68//!     // Mon dd, yyyy hh:mm:ss z
69//!     "May 02, 2021 15:51:31 UTC",
70//!     "May 02, 2021 15:51 UTC",
71//!     "May 26, 2021, 12:49 AM PDT",
72//!     "September 17, 2012 at 10:09am PST",
73//!     // yyyy-mon-dd
74//!     "2021-Feb-21",
75//!     // Mon dd, yyyy
76//!     "May 25, 2021",
77//!     "oct 7, 1970",
78//!     "oct 7, 70",
79//!     "oct. 7, 1970",
80//!     "oct. 7, 70",
81//!     "October 7, 1970",
82//!     // dd Mon yyyy hh:mm:ss
83//!     "12 Feb 2006, 19:17",
84//!     "12 Feb 2006 19:17",
85//!     "14 May 2019 19:11:40.164",
86//!     // dd Mon yyyy
87//!     "7 oct 70",
88//!     "7 oct 1970",
89//!     "03 February 2013",
90//!     "1 July 2013",
91//!     // mm/dd/yyyy hh:mm:ss
92//!     "4/8/2014 22:05",
93//!     "04/08/2014 22:05",
94//!     "4/8/14 22:05",
95//!     "04/2/2014 03:00:51",
96//!     "8/8/1965 12:00:00 AM",
97//!     "8/8/1965 01:00:01 PM",
98//!     "8/8/1965 01:00 PM",
99//!     "8/8/1965 1:00 PM",
100//!     "8/8/1965 12:00 AM",
101//!     "4/02/2014 03:00:51",
102//!     "03/19/2012 10:11:59",
103//!     "03/19/2012 10:11:59.3186369",
104//!     // mm/dd/yyyy
105//!     "3/31/2014",
106//!     "03/31/2014",
107//!     "08/21/71",
108//!     "8/1/71",
109//!     // yyyy/mm/dd hh:mm:ss
110//!     "2014/4/8 22:05",
111//!     "2014/04/08 22:05",
112//!     "2014/04/2 03:00:51",
113//!     "2014/4/02 03:00:51",
114//!     "2012/03/19 10:11:59",
115//!     "2012/03/19 10:11:59.3186369",
116//!     // yyyy/mm/dd
117//!     "2014/3/31",
118//!     "2014/03/31",
119//! ];
120//!
121//! for date_str in accepted {
122//!     let result = date_str.parse::<DateTimeUtc>();
123//!     assert!(result.is_ok())
124//! }
125//! ```
126//!
127//! ### DMY Format
128//!
129//! It also accepts dates in DMY format with `parse_with_preference`,
130//! and the `prefer_dmy` parameter set to true.
131//!
132//! ```
133//! use qsv_dateparser::parse_with_preference;
134//!
135//! let accepted = vec![
136//!     // dd/mm/yyyy
137//!     "31/12/2020",
138//!     "12/10/2019",
139//!     "03/06/2018",
140//!     "27/06/68",
141//!     // dd/mm/yyyy hh:mm:ss
142//!     "4/8/2014 22:05",
143//!     "04/08/2014 22:05",
144//!     "4/8/14 22:05",
145//!     "04/2/2014 03:00:51",
146//!     "8/8/1965 12:00:00 AM",
147//!     "8/8/1965 01:00:01 PM",
148//!     "8/8/1965 01:00 PM",
149//!     "31/12/22 15:00"
150//! ];
151//!
152//! for date_str in accepted {
153//!     let result = parse_with_preference(date_str, true);
154//!     assert!(result.is_ok());
155//! }
156//! ```
157
158/// Datetime string parser
159///
160/// ```
161/// use chrono::prelude::*;
162/// use qsv_dateparser::datetime::Parse;
163/// use std::error::Error;
164///
165/// fn main() -> Result<(), Box<dyn Error>> {
166///     let utc_now_time = Utc::now().time();
167///     let parse_with_local = Parse::new(&Local, utc_now_time);
168///     assert_eq!(
169///         parse_with_local.parse("2021-06-05 06:19 PM")?,
170///         Local.ymd(2021, 6, 5).and_hms(18, 19, 0).with_timezone(&Utc),
171///     );
172///
173///     let parse_with_utc = Parse::new(&Utc, utc_now_time);
174///     assert_eq!(
175///         parse_with_utc.parse("2021-06-05 06:19 PM")?,
176///         Utc.ymd(2021, 6, 5).and_hms(18, 19, 0),
177///     );
178///
179///     Ok(())
180/// }
181/// ```
182pub mod datetime;
183
184/// Timezone offset string parser
185///
186/// ```
187/// use chrono::prelude::*;
188/// use qsv_dateparser::timezone::parse;
189/// use std::error::Error;
190///
191/// fn main() -> Result<(), Box<dyn Error>> {
192///     assert_eq!(parse("-0800")?, FixedOffset::west(8 * 3600));
193///     assert_eq!(parse("+10:00")?, FixedOffset::east(10 * 3600));
194///     assert_eq!(parse("PST")?, FixedOffset::west(8 * 3600));
195///     assert_eq!(parse("PDT")?, FixedOffset::west(7 * 3600));
196///     assert_eq!(parse("UTC")?, FixedOffset::west(0));
197///     assert_eq!(parse("GMT")?, FixedOffset::west(0));
198///
199///     Ok(())
200/// }
201/// ```
202pub mod timezone;
203
204use crate::datetime::Parse;
205use anyhow::{Error, Result};
206use chrono::prelude::*;
207
208/// `DateTimeUtc` is an alias for `chrono`'s `DateTime<UTC>`. It implements `std::str::FromStr`'s
209/// `from_str` method, and it makes `str`'s `parse` method to understand the accepted date formats
210/// from this crate.
211///
212/// ```
213/// use qsv_dateparser::DateTimeUtc;
214///
215/// // parsed is DateTimeUTC and parsed.0 is chrono's DateTime<Utc>
216/// match "May 02, 2021 15:51:31 UTC".parse::<DateTimeUtc>() {
217///     Ok(parsed) => println!("PARSED into UTC datetime {:?}", parsed.0),
218///     Err(err) => println!("ERROR from parsing datetime string: {}", err)
219/// }
220/// ```
221pub struct DateTimeUtc(pub DateTime<Utc>);
222
223impl std::str::FromStr for DateTimeUtc {
224    type Err = Error;
225
226    fn from_str(s: &str) -> Result<Self> {
227        parse(s).map(DateTimeUtc)
228    }
229}
230
231const MIDNIGHT: NaiveTime = NaiveTime::MIN;
232
233/// This function tries to recognize the input datetime string with a list of accepted formats.
234/// When timezone is not provided, this function assumes it's a [`chrono::Local`] datetime. For
235/// custom timezone, use [`parse_with_timezone()`] instead.If all options are exhausted,
236/// [`parse()`] will return an error to let the caller know that no formats were matched.
237#[inline]
238pub fn parse(input: &str) -> Result<DateTime<Utc>> {
239    Parse::new(&Local, Utc::now().time()).parse(input)
240}
241
242/// Similar to [`parse()`], this function takes a datetime string and a boolean `dmy_preference`.
243/// When `dmy_preference` is `true`, it will parse strings using the DMY format. Otherwise, it
244/// parses them using an MDY format.
245#[inline]
246pub fn parse_with_preference(input: &str, dmy_preference: bool) -> Result<DateTime<Utc>> {
247    Parse::new_with_preference(&Utc, MIDNIGHT, dmy_preference).parse(input)
248}
249
250/// Similar to [`parse()`], this function takes a datetime string and a custom [`chrono::TimeZone`],
251/// and tries to parse the datetime string. When timezone is not given in the string, this function
252/// will assume and parse the datetime by the custom timezone provided in this function's arguments.
253///
254#[inline]
255pub fn parse_with_timezone<Tz2: TimeZone>(input: &str, tz: &Tz2) -> Result<DateTime<Utc>> {
256    Parse::new(tz, Utc::now().time()).parse(input)
257}
258
259/// Similar to [`parse()`], this function takes a datetime string and a boolean `dmy_preference`
260/// and a timezone. When timezone is not given in the input string, this function will
261/// assume and parse the datetime by the custom timezone provided in this function's arguments.
262/// When `dmy_preference` is `true`, it will parse strings using the DMY format. Otherwise, it
263/// parses them using an MDY format.
264#[inline]
265pub fn parse_with_preference_and_timezone<Tz2: TimeZone>(
266    input: &str,
267    dmy_preference: bool,
268    tz: &Tz2,
269) -> Result<DateTime<Utc>> {
270    Parse::new_with_preference(tz, MIDNIGHT, dmy_preference).parse(input)
271}
272
273/// Similar to [`parse()`] and [`parse_with_timezone()`], this function takes a datetime string, a
274/// custom [`chrono::TimeZone`] and a default naive time. In addition to assuming timezone when
275/// it's not given in datetime string, this function also use provided default naive time in parsed
276/// [`chrono::DateTime`].
277///
278#[inline]
279pub fn parse_with<Tz2: TimeZone>(
280    input: &str,
281    tz: &Tz2,
282    default_time: NaiveTime,
283) -> Result<DateTime<Utc>> {
284    Parse::new(tz, default_time).parse(input)
285}
286
287#[cfg(test)]
288#[allow(deprecated)]
289mod tests {
290    use super::*;
291
292    #[derive(Clone, Copy)]
293    enum Trunc {
294        Seconds,
295        None,
296    }
297
298    #[test]
299    fn parse_in_local() {
300        let test_cases = vec![
301            (
302                "rfc3339",
303                "2017-11-25T22:34:50Z",
304                Utc.ymd(2017, 11, 25).and_hms(22, 34, 50),
305                Trunc::None,
306            ),
307            (
308                "rfc2822",
309                "Wed, 02 Jun 2021 06:31:39 GMT",
310                Utc.ymd(2021, 6, 2).and_hms(6, 31, 39),
311                Trunc::None,
312            ),
313            (
314                "ymd_hms",
315                "2021-04-30 21:14:10",
316                Local
317                    .ymd(2021, 4, 30)
318                    .and_hms(21, 14, 10)
319                    .with_timezone(&Utc),
320                Trunc::None,
321            ),
322            (
323                "ymd_hms_z",
324                "2017-11-25 13:31:15 PST",
325                Utc.ymd(2017, 11, 25).and_hms(21, 31, 15),
326                Trunc::None,
327            ),
328            (
329                "ymd",
330                "2021-02-21",
331                Local
332                    .ymd(2021, 2, 21)
333                    .and_time(Local::now().time())
334                    .unwrap()
335                    .with_timezone(&Utc),
336                Trunc::Seconds,
337            ),
338            (
339                "ymd_z",
340                "2021-02-21 PST",
341                FixedOffset::west(8 * 3600)
342                    .ymd(2021, 2, 21)
343                    .and_time(
344                        Utc::now()
345                            .with_timezone(&FixedOffset::west(8 * 3600))
346                            .time(),
347                    )
348                    .unwrap()
349                    .with_timezone(&Utc),
350                Trunc::Seconds,
351            ),
352            (
353                "month_ymd",
354                "2021-Feb-21",
355                Local
356                    .ymd(2021, 2, 21)
357                    .and_time(Local::now().time())
358                    .unwrap()
359                    .with_timezone(&Utc),
360                Trunc::Seconds,
361            ),
362            (
363                "month_mdy_hms",
364                "May 8, 2009 5:57:51 PM",
365                Local
366                    .ymd(2009, 5, 8)
367                    .and_hms(17, 57, 51)
368                    .with_timezone(&Utc),
369                Trunc::None,
370            ),
371            (
372                "month_mdy_hms_z",
373                "May 02, 2021 15:51 UTC",
374                Utc.ymd(2021, 5, 2).and_hms(15, 51, 0),
375                Trunc::None,
376            ),
377            (
378                "month_mdy",
379                "May 25, 2021",
380                Local
381                    .ymd(2021, 5, 25)
382                    .and_time(Local::now().time())
383                    .unwrap()
384                    .with_timezone(&Utc),
385                Trunc::Seconds,
386            ),
387            (
388                "month_dmy_hms",
389                "14 May 2019 19:11:40.164",
390                Local
391                    .ymd(2019, 5, 14)
392                    .and_hms_milli(19, 11, 40, 164)
393                    .with_timezone(&Utc),
394                Trunc::None,
395            ),
396            (
397                "month_dmy",
398                "1 July 2013",
399                Local
400                    .ymd(2013, 7, 1)
401                    .and_time(Local::now().time())
402                    .unwrap()
403                    .with_timezone(&Utc),
404                Trunc::Seconds,
405            ),
406            (
407                "slash_mdy_hms",
408                "03/19/2012 10:11:59",
409                Local
410                    .ymd(2012, 3, 19)
411                    .and_hms(10, 11, 59)
412                    .with_timezone(&Utc),
413                Trunc::None,
414            ),
415            (
416                "slash_mdy",
417                "08/21/71",
418                Local
419                    .ymd(1971, 8, 21)
420                    .and_time(Local::now().time())
421                    .unwrap()
422                    .with_timezone(&Utc),
423                Trunc::Seconds,
424            ),
425            (
426                "slash_ymd_hms",
427                "2012/03/19 10:11:59",
428                Local
429                    .ymd(2012, 3, 19)
430                    .and_hms(10, 11, 59)
431                    .with_timezone(&Utc),
432                Trunc::None,
433            ),
434            (
435                "slash_ymd",
436                "2014/3/31",
437                Local
438                    .ymd(2014, 3, 31)
439                    .and_time(Local::now().time())
440                    .unwrap()
441                    .with_timezone(&Utc),
442                Trunc::Seconds,
443            ),
444        ];
445
446        for &(test, input, want, trunc) in test_cases.iter() {
447            match trunc {
448                Trunc::None => {
449                    assert_eq!(
450                        super::parse(input).unwrap(),
451                        want,
452                        "parse_in_local/{}/{}",
453                        test,
454                        input
455                    )
456                }
457                Trunc::Seconds => assert_eq!(
458                    super::parse(input)
459                        .unwrap()
460                        .trunc_subsecs(0)
461                        .with_second(0)
462                        .unwrap(),
463                    want.trunc_subsecs(0).with_second(0).unwrap(),
464                    "parse_in_local/{}/{}",
465                    test,
466                    input
467                ),
468            };
469        }
470    }
471
472    #[test]
473    fn parse_with_timezone_in_utc() {
474        let test_cases = vec![
475            (
476                "rfc3339",
477                "2017-11-25T22:34:50Z",
478                Utc.ymd(2017, 11, 25).and_hms(22, 34, 50),
479                Trunc::None,
480            ),
481            (
482                "rfc2822",
483                "Wed, 02 Jun 2021 06:31:39 GMT",
484                Utc.ymd(2021, 6, 2).and_hms(6, 31, 39),
485                Trunc::None,
486            ),
487            (
488                "ymd_hms",
489                "2021-04-30 21:14:10",
490                Utc.ymd(2021, 4, 30).and_hms(21, 14, 10),
491                Trunc::None,
492            ),
493            (
494                "ymd_hms_z",
495                "2017-11-25 13:31:15 PST",
496                Utc.ymd(2017, 11, 25).and_hms(21, 31, 15),
497                Trunc::None,
498            ),
499            (
500                "ymd",
501                "2021-02-21",
502                Utc.ymd(2021, 2, 21).and_time(Utc::now().time()).unwrap(),
503                Trunc::Seconds,
504            ),
505            (
506                "ymd_z",
507                "2021-02-21 PST",
508                FixedOffset::west(8 * 3600)
509                    .ymd(2021, 2, 21)
510                    .and_time(
511                        Utc::now()
512                            .with_timezone(&FixedOffset::west(8 * 3600))
513                            .time(),
514                    )
515                    .unwrap()
516                    .with_timezone(&Utc),
517                Trunc::Seconds,
518            ),
519            (
520                "month_ymd",
521                "2021-Feb-21",
522                Utc.ymd(2021, 2, 21).and_time(Utc::now().time()).unwrap(),
523                Trunc::Seconds,
524            ),
525            (
526                "month_mdy_hms",
527                "May 8, 2009 5:57:51 PM",
528                Utc.ymd(2009, 5, 8).and_hms(17, 57, 51),
529                Trunc::None,
530            ),
531            (
532                "month_mdy_hms_z",
533                "May 02, 2021 15:51 UTC",
534                Utc.ymd(2021, 5, 2).and_hms(15, 51, 0),
535                Trunc::None,
536            ),
537            (
538                "month_mdy",
539                "May 25, 2021",
540                Utc.ymd(2021, 5, 25).and_time(Utc::now().time()).unwrap(),
541                Trunc::Seconds,
542            ),
543            (
544                "month_dmy_hms",
545                "14 May 2019 19:11:40.164",
546                Utc.ymd(2019, 5, 14).and_hms_milli(19, 11, 40, 164),
547                Trunc::None,
548            ),
549            (
550                "month_dmy",
551                "1 July 2013",
552                Utc.ymd(2013, 7, 1).and_time(Utc::now().time()).unwrap(),
553                Trunc::Seconds,
554            ),
555            (
556                "slash_mdy_hms",
557                "03/19/2012 10:11:59",
558                Utc.ymd(2012, 3, 19).and_hms(10, 11, 59),
559                Trunc::None,
560            ),
561            (
562                "slash_mdy",
563                "08/21/71",
564                Utc.ymd(1971, 8, 21).and_time(Utc::now().time()).unwrap(),
565                Trunc::Seconds,
566            ),
567            (
568                "slash_ymd_hms",
569                "2012/03/19 10:11:59",
570                Utc.ymd(2012, 3, 19).and_hms(10, 11, 59),
571                Trunc::None,
572            ),
573            (
574                "slash_ymd",
575                "2014/3/31",
576                Utc.ymd(2014, 3, 31).and_time(Utc::now().time()).unwrap(),
577                Trunc::Seconds,
578            ),
579        ];
580
581        for &(test, input, want, trunc) in test_cases.iter() {
582            match trunc {
583                Trunc::None => {
584                    assert_eq!(
585                        super::parse_with_timezone(input, &Utc).unwrap(),
586                        want,
587                        "parse_with_timezone_in_utc/{}/{}",
588                        test,
589                        input
590                    )
591                }
592                Trunc::Seconds => assert_eq!(
593                    super::parse_with_timezone(input, &Utc)
594                        .unwrap()
595                        .trunc_subsecs(0)
596                        .with_second(0)
597                        .unwrap(),
598                    want.trunc_subsecs(0).with_second(0).unwrap(),
599                    "parse_with_timezone_in_utc/{}/{}",
600                    test,
601                    input
602                ),
603            };
604        }
605    }
606
607    #[test]
608    fn parse_with_preference_and_timezone_in_utc() {
609        let current_time = Utc::now().time();
610        let current_hour = current_time.hour();
611        let current_minute = current_time.minute();
612        // let current_second = current_time.second();
613        let test_cases = vec![
614            (
615                "rfc3339",
616                "2017-11-25T22:34:50Z",
617                Utc.ymd(2017, 11, 25).and_hms(22, 34, 50),
618                Trunc::None,
619            ),
620            (
621                "rfc2822",
622                "Wed, 02 Jun 2021 06:31:39 GMT",
623                Utc.ymd(2021, 6, 2).and_hms(6, 31, 39),
624                Trunc::None,
625            ),
626            // we currently do not parse dmy format using hyphens,
627            // so the following tests are commented out
628            // (
629            //     "dmy_hms",
630            //     "30-04-2021 21:14:10",
631            //     Utc.ymd(2021, 4, 30).and_hms(21, 14, 10),
632            //     Trunc::None,
633            // ),
634            // (
635            //     "dmy_hms_z",
636            //     "25-11-2017 13:31:15 PST",
637            //     Utc.ymd(2017, 11, 25).and_hms(21, 31, 15),
638            //     Trunc::None,
639            // ),
640            // (
641            //     "dmy",
642            //     "21-02-2021",
643            //     // Utc.ymd(2021, 2, 21).and_time(Utc::now().time()).unwrap(),
644            //     Utc.with_ymd_and_hms(2021, 2, 21, current_hour, current_minute, current_second)
645            //         .unwrap(),
646            //     Trunc::Seconds,
647            // ),
648            // (
649            //     "dmy_z",
650            //     "21-02-2021 PST",
651            //     FixedOffset::west(8 * 3600)
652            //         .ymd(2021, 2, 21)
653            //         .and_time(
654            //             Utc::now()
655            //                 .with_timezone(&FixedOffset::west(8 * 3600))
656            //                 .time(),
657            //         )
658            //         .unwrap()
659            //         .with_timezone(&Utc),
660            //     Trunc::Seconds,
661            // ),
662            // (
663            //     "month_dmy",
664            //     "21-Feb-2021",
665            //     Utc.ymd(2021, 2, 21).and_time(Utc::now().time()).unwrap(),
666            //     Trunc::Seconds,
667            // ),
668            (
669                "month_mdy_hms",
670                "May 8, 2009 5:57:51 PM",
671                Utc.ymd(2009, 5, 8).and_hms(17, 57, 51),
672                Trunc::None,
673            ),
674            (
675                "month_mdy_hms_z",
676                "May 02, 2021 15:51 UTC",
677                Utc.ymd(2021, 5, 2).and_hms(15, 51, 0),
678                Trunc::None,
679            ),
680            (
681                "month_mdy",
682                "May 25, 2021",
683                Utc.ymd(2021, 5, 25).and_time(Utc::now().time()).unwrap(),
684                Trunc::Seconds,
685            ),
686            (
687                "month_dmy_hms",
688                "14 May 2019 19:11:40.164",
689                Utc.ymd(2019, 5, 14).and_hms_milli(19, 11, 40, 164),
690                Trunc::None,
691            ),
692            (
693                "month_dmy",
694                "1 July 2013",
695                Utc.ymd(2013, 7, 1).and_time(Utc::now().time()).unwrap(),
696                Trunc::Seconds,
697            ),
698            (
699                "slash_dmy_hms",
700                "19/03/2012 10:11:59",
701                Utc.ymd(2012, 3, 19).and_hms(10, 11, 59),
702                Trunc::None,
703            ),
704            (
705                "slash_dmy",
706                "21/08/71",
707                Utc.ymd(1971, 8, 21).and_time(Utc::now().time()).unwrap(),
708                Trunc::Seconds,
709            ),
710            (
711                "slash_dmy_hms",
712                "19/03/2012 10:11:59",
713                Utc.ymd(2012, 3, 19).and_hms(10, 11, 59),
714                Trunc::None,
715            ),
716            (
717                "slash_dmy",
718                "31/3/2014",
719                Utc.ymd(2014, 3, 31).and_time(Utc::now().time()).unwrap(),
720                Trunc::Seconds,
721            ),
722        ];
723
724        for &(test, input, want, trunc) in test_cases.iter() {
725            match trunc {
726                Trunc::None => {
727                    assert_eq!(
728                        super::parse_with_preference_and_timezone(input, true, &Utc).unwrap(),
729                        want,
730                        "parse_with_preference_and_timezone_in_utc/{}/{}",
731                        test,
732                        input
733                    )
734                }
735                Trunc::Seconds => assert_eq!(
736                    super::parse_with_preference_and_timezone(input, true, &Utc)
737                        .unwrap()
738                        .trunc_subsecs(0)
739                        .with_hour(current_hour)
740                        .unwrap()
741                        .with_minute(current_minute)
742                        .unwrap()
743                        .with_second(0)
744                        .unwrap(),
745                    want.trunc_subsecs(0).with_second(0).unwrap(),
746                    "parse_with_preference_and_timezone_in_utc/{}/{}",
747                    test,
748                    input
749                ),
750            };
751        }
752    }
753
754    #[test]
755    fn parse_unambiguous_dmy() {
756        // `parse()` uses Local timezone and pads date-only inputs with the
757        // current time of day, so the resulting UTC date can roll by ±1 day
758        // depending on host TZ and the moment the test runs. Assert on the
759        // Local date — that's what `parse()` actually models for this input.
760        assert_eq!(
761            super::parse("31/3/22")
762                .unwrap()
763                .with_timezone(&Local)
764                .date(),
765            Local.ymd(2022, 3, 31)
766        );
767        assert_eq!(
768            super::parse_with_preference("3/31/22", true)
769                .unwrap()
770                .date(),
771            Utc.ymd(2022, 3, 31)
772        );
773        assert_eq!(
774            super::parse_with_preference("31/07/2021", true)
775                .unwrap()
776                .date(),
777            Utc.ymd(2021, 7, 31)
778        );
779    }
780
781    // Regression: ISO 8601 with 'T' separator and no timezone (e.g. Python's
782    // datetime.isoformat() without astimezone) must parse via the naive
783    // wall-clock path, matching the equivalent space-separated form.
784    #[test]
785    fn parse_iso_t_no_tz() {
786        // Bare T, no fractional, no tz.
787        let got = super::parse_with_preference("2020-01-15T08:00:00", false).unwrap();
788        assert_eq!(got, Utc.ymd(2020, 1, 15).and_hms(8, 0, 0));
789
790        // T, no seconds, no tz.
791        let got = super::parse_with_preference("2020-01-15T08:00", false).unwrap();
792        assert_eq!(got, Utc.ymd(2020, 1, 15).and_hms(8, 0, 0));
793
794        // T with millisecond + microsecond + nanosecond precision.
795        for (input, want) in [
796            (
797                "2020-01-15T08:00:00.123",
798                Utc.ymd(2020, 1, 15).and_hms_milli(8, 0, 0, 123),
799            ),
800            (
801                "2020-01-15T08:00:00.123456",
802                Utc.ymd(2020, 1, 15).and_hms_micro(8, 0, 0, 123456),
803            ),
804            (
805                "2020-01-15T08:00:00.123456789",
806                Utc.ymd(2020, 1, 15).and_hms_nano(8, 0, 0, 123456789),
807            ),
808        ] {
809            assert_eq!(
810                super::parse_with_preference(input, false).unwrap(),
811                want,
812                "parse_iso_t_no_tz/{input}"
813            );
814        }
815
816        // T-form and space-form must produce the same instant.
817        assert_eq!(
818            super::parse_with_preference("2020-01-15T08:00:00", false).unwrap(),
819            super::parse_with_preference("2020-01-15 08:00:00", false).unwrap(),
820        );
821
822        // Existing tz-bearing T-forms must continue to parse (no regression).
823        assert!(super::parse_with_preference("2020-01-15T08:00:00Z", false).is_ok());
824        assert!(super::parse_with_preference("2020-01-15T08:00:00+00:00", false).is_ok());
825    }
826
827    // Structural pre-filter: inputs containing a byte that cannot appear in any
828    // accepted date format (e.g. '_', '#', non-ASCII) must fail fast, while every
829    // currently-accepted input must still parse. Correctness guard for the
830    // pre-filter optimization (must not change which strings parse).
831    #[test]
832    fn prefilter_rejects_non_date_strings() {
833        // The qsv-dateparser-opt failure hot path: '_' is not a valid date byte.
834        for input in [
835            "category_value_123",
836            "first_name",
837            "value#42",
838            "a(b)c1",
839            "100%",
840            "naïve_2020", // non-ASCII byte
841        ] {
842            assert!(
843                super::parse(input).is_err(),
844                "prefilter should reject {input}"
845            );
846        }
847
848        // Pre-filter must NOT reject anything that currently parses. Spot-check
849        // every separator family.
850        for input in [
851            "2021-04-30 21:14:10",           // '-' ':' space
852            "2020-07-20+08:00",              // '+'
853            "03/19/2012 10:11:59.3186369",   // '/' '.'
854            "May 26, 2021, 12:49 AM PDT",    // ',' letters
855            "Wed, 02 Jun 2021 06:31:39 GMT", // rfc2822
856            "1671673426.123456789",          // timestamp with '.'
857            "-770172300",                    // negative timestamp
858        ] {
859            assert!(
860                super::parse(input).is_ok(),
861                "prefilter must not reject {input}"
862            );
863        }
864    }
865}