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}