1#![allow(deprecated)]
2use crate::timezone;
3use anyhow::{Result, anyhow};
4use chrono::format::{Item, ParseResult, Parsed, parse as parse_items};
5use chrono::prelude::*;
6use regex::Regex;
7
8macro_rules! regex {
9 ($re:literal $(,)?) => {{
10 static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
11 RE.get_or_init(|| {
12 regex::RegexBuilder::new($re)
13 .unicode(false)
14 .build()
15 .expect("invalid regex literal")
16 })
17 }};
18}
19
20macro_rules! fmt_items {
35 ($fmt:literal $(,)?) => {{
36 static ITEMS: std::sync::OnceLock<Vec<chrono::format::Item<'static>>> =
37 std::sync::OnceLock::new();
38 ITEMS
39 .get_or_init(|| {
40 chrono::format::StrftimeItems::new($fmt)
41 .parse()
42 .expect("invalid strftime literal")
43 })
44 .as_slice()
45 }};
46}
47const fn build_date_byte_table() -> [bool; 256] {
51 let mut table = [false; 256];
52 let mut i = 0usize;
53 while i < 256 {
54 let b = i as u8;
55 table[i] = b.is_ascii_alphanumeric()
56 || matches!(b, b' ' | 0x09..=0x0D)
57 || matches!(b, b'-' | b'+' | b'/' | b':' | b'.' | b',');
58 i += 1;
59 }
60 table
61}
62
63static DATE_BYTE: [bool; 256] = build_date_byte_table();
64
65const fn build_ws_table() -> [bool; 256] {
71 let mut table = [false; 256];
72 let mut i = 0usize;
73 while i < 256 {
74 table[i] = matches!(i as u8, b' ' | 0x09..=0x0D);
75 i += 1;
76 }
77 table
78}
79
80static DATE_WS: [bool; 256] = build_ws_table();
81
82#[inline]
90fn cannot_be_date(input: &str) -> bool {
91 input.bytes().any(|b| !DATE_BYTE[b as usize])
92}
93
94#[inline]
119fn tz_suffix(input: &str, prefix_end: usize, require_ws: bool) -> Option<&str> {
120 let bytes = input.as_bytes();
121 let rest = input.get(prefix_end..)?;
122
123 if require_ws {
124 let leading_ws = rest
125 .as_bytes()
126 .first()
127 .is_some_and(|&b| DATE_WS[b as usize]);
128 let trailing_ws = prefix_end
129 .checked_sub(1)
130 .is_some_and(|i| DATE_WS[bytes[i] as usize]);
131 if !leading_ws && !trailing_ws {
132 return None;
133 }
134 }
135
136 let token = rest.trim_start_matches(|c: char| (c as u32) < 128 && DATE_WS[c as usize]);
138 if !(3..=6).contains(&token.len()) {
139 return None;
140 }
141 token
142 .bytes()
143 .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'+' | b'-' | b':'))
144 .then_some(token)
145}
146
147const NORMALIZE_SCRATCH: usize = 64;
156
157#[inline]
170fn normalize_into<'b>(
171 input: &str,
172 buf: &'b mut [u8; NORMALIZE_SCRATCH],
173 strip: &[u8],
174 drop_at: bool,
175) -> Option<&'b str> {
176 if input.len() > buf.len() {
177 return None;
178 }
179
180 let mut n = 0;
181 for &b in input.as_bytes() {
182 if !strip.contains(&b) {
183 buf[n] = b;
184 n += 1;
185 }
186 }
187
188 if drop_at && let Some(pos) = buf[..n].windows(2).position(|w| w == b"at") {
189 buf.copy_within(pos + 2..n, pos);
190 n -= 2;
191 }
192
193 std::str::from_utf8(&buf[..n]).ok()
196}
197
198#[derive(Clone, Copy, PartialEq, Eq)]
211enum TimeShape {
212 Hm,
214 Hms,
216 HmsF,
218 ImP,
220 ImsP,
222 HmsFP,
229}
230
231#[inline]
245fn time_shape(input: &str) -> TimeShape {
246 let bytes = input.as_bytes();
247
248 let ampm = bytes.len() >= 2 && {
249 let [.., ap, m] = bytes else { unreachable!() };
250 m | 32 == b'm' && matches!(ap | 32, b'a' | b'p')
251 };
252
253 let mut colons = 0_u8;
254 let mut fraction = false;
255 for &b in bytes {
256 match b {
257 b':' => colons += 1,
258 b'.' => fraction = true,
259 _ => {}
260 }
261 }
262
263 match (colons >= 2, fraction, ampm) {
264 (true, true, true) => TimeShape::HmsFP,
265 (true, true, false) => TimeShape::HmsF,
266 (true, false, true) => TimeShape::ImsP,
267 (true, false, false) => TimeShape::Hms,
268 (false, _, true) => TimeShape::ImP,
269 (false, _, false) => TimeShape::Hm,
270 }
271}
272
273#[inline]
283fn slash_year_is_two_digits(bytes: &[u8]) -> bool {
284 let mut slashes = 0u8;
285 let mut year_len = 0usize;
286 for &b in bytes {
287 if b == b'/' {
288 slashes += 1;
289 } else if slashes == 2 {
290 if b.is_ascii_digit() {
291 year_len += 1;
292 } else {
293 break;
294 }
295 }
296 }
297 year_len == 2
298}
299
300pub struct Parse<'z, Tz2> {
302 tz: &'z Tz2,
303 default_time: NaiveTime,
304 prefer_dmy: bool,
305}
306
307impl<'z, Tz2> Parse<'z, Tz2>
308where
309 Tz2: TimeZone,
310{
311 pub const fn new(tz: &'z Tz2, default_time: NaiveTime) -> Self {
314 Self {
315 tz,
316 default_time,
317 prefer_dmy: false,
318 }
319 }
320
321 pub const fn prefer_dmy(&mut self, yes: bool) -> &Self {
322 self.prefer_dmy = yes;
323 self
324 }
325
326 pub const fn new_with_preference(
329 tz: &'z Tz2,
330 default_time: NaiveTime,
331 prefer_dmy: bool,
332 ) -> Self {
333 Self {
334 tz,
335 default_time,
336 prefer_dmy,
337 }
338 }
339
340 #[inline]
347 fn dt_from_items(&self, input: &str, items: &[Item<'static>]) -> ParseResult<DateTime<Tz2>> {
348 let mut parsed = Parsed::new();
349 parse_items(&mut parsed, input, items.iter())?;
350 parsed.to_datetime_with_timezone(self.tz)
351 }
352
353 #[inline]
356 fn naive_dt_from_items(input: &str, items: &[Item<'static>]) -> ParseResult<NaiveDateTime> {
357 let mut parsed = Parsed::new();
358 parse_items(&mut parsed, input, items.iter())?;
359 parsed.to_naive_datetime_with_offset(0)
360 }
361
362 #[inline]
365 fn naive_date_from_items(input: &str, items: &[Item<'static>]) -> ParseResult<NaiveDate> {
366 let mut parsed = Parsed::new();
367 parse_items(&mut parsed, input, items.iter())?;
368 parsed.to_naive_date()
369 }
370
371 #[inline]
404 pub fn parse(&self, input: &str) -> Result<DateTime<Utc>> {
405 if cannot_be_date(input) {
406 return Err(anyhow!("{} did not match any formats.", input));
407 }
408 let Some(&first) = input.as_bytes().first() else {
409 return Err(anyhow!("{} did not match any formats.", input));
410 };
411
412 let parsed = if first.is_ascii_digit() {
413 self.slash_mdy_family(input)
414 .or_else(|| self.slash_ymd_family(input))
415 .or_else(|| self.ymd_family(input))
416 .or_else(|| self.month_ymd(input))
417 .or_else(|| self.month_dmy_family(input))
418 .or_else(|| self.unix_timestamp(input))
419 .or_else(|| self.rfc2822(input))
420 } else if first.is_ascii_alphabetic() {
421 self.month_mdy_family(input).or_else(|| self.rfc2822(input))
424 } else {
425 self.unix_timestamp(input).or_else(|| self.rfc2822(input))
429 };
430
431 parsed.unwrap_or_else(|| Err(anyhow!("{} did not match any formats.", input)))
432 }
433
434 #[inline]
435 fn ymd_family(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
436 let re: &Regex = regex! {
437 r"^\d{4}-\d{2}"
438
439 };
440
441 if !re.is_match(input) {
442 return None;
443 }
444 self.rfc3339(input)
445 .or_else(|| self.ymd_hms(input))
446 .or_else(|| self.ymd_hms_z(input))
447 .or_else(|| self.ymd(input))
448 .or_else(|| self.ymd_z(input))
449 }
450
451 #[inline]
452 fn month_mdy_family(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
453 let re: &Regex = regex! {
454 r"^[a-zA-Z]{3,9}\.?\s+\d{1,2}"
455 };
456
457 if !re.is_match(input) {
458 return None;
459 }
460 self.month_mdy_hms(input)
461 .or_else(|| self.month_mdy_hms_z(input))
462 .or_else(|| self.month_mdy(input))
463 }
464
465 #[inline]
466 fn month_dmy_family(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
467 let re: &Regex = regex! {r"^\d{1,2}\s+[a-zA-Z]{3,9}"
468 };
469
470 if !re.is_match(input) {
471 return None;
472 }
473 self.month_dmy_hms(input).or_else(|| self.month_dmy(input))
474 }
475
476 #[inline]
477 fn slash_mdy_family(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
478 let re: &Regex = regex! {r"^\d{1,2}/\d{1,2}"
479 };
480 if !re.is_match(input) {
481 return None;
482 }
483 if self.prefer_dmy {
484 self.slash_dmy_hms(input)
485 .or_else(|| self.slash_dmy(input))
486 .or_else(|| self.slash_mdy_hms(input))
487 .or_else(|| self.slash_mdy(input))
488 } else {
489 self.slash_mdy_hms(input)
490 .or_else(|| self.slash_mdy(input))
491 .or_else(|| self.slash_dmy_hms(input))
492 .or_else(|| self.slash_dmy(input))
493 }
494 }
495
496 #[inline]
497 fn slash_ymd_family(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
498 let re: &Regex = regex! {r"^[0-9]{4}/[0-9]{1,2}"};
499 if !re.is_match(input) {
500 return None;
501 }
502 self.slash_ymd_hms(input).or_else(|| self.slash_ymd(input))
503 }
504
505 #[inline]
510 fn unix_timestamp(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
511 let &b0 = input.as_bytes().first()?;
518 if !(b0.is_ascii_digit() || matches!(b0, b'+' | b'-' | b'.')) {
519 return None;
520 }
521
522 let ts_sec_val: f64 = if let Ok(val) = fast_float2::parse(input) {
523 val
524 } else {
525 return None;
526 };
527
528 if !ts_sec_val.is_finite() {
533 return None;
534 }
535
536 let ts_ns_val = ts_sec_val * 1_000_000_000_f64;
538
539 let result = Utc.timestamp_nanos(ts_ns_val as i64).with_timezone(&Utc);
540 Some(Ok(result))
541 }
542
543 #[inline]
547 fn rfc3339(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
548 DateTime::parse_from_rfc3339(input)
549 .ok()
550 .map(|parsed| parsed.with_timezone(&Utc))
551 .map(Ok)
552 }
553
554 #[inline]
557 fn rfc2822(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
558 if !input.as_bytes().contains(&b':') {
563 return None;
564 }
565 DateTime::parse_from_rfc2822(input)
566 .ok()
567 .map(|parsed| parsed.with_timezone(&Utc))
568 .map(Ok)
569 }
570
571 #[inline]
584 fn ymd_hms(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
585 let re: &Regex = regex! {
586 r"^\d{4}-\d{2}-\d{2}[T\s]+\d{2}:\d{2}(?::\d{2})?(?:\.\d{1,9})?\s*(?:am|pm|AM|PM)?$"
587
588 };
589 if !re.is_match(input) {
590 return None;
591 }
592
593 let items = match (input.as_bytes()[10] == b'T', time_shape(input)) {
597 (true, TimeShape::Hms) => fmt_items!("%Y-%m-%dT%H:%M:%S"),
598 (true, TimeShape::Hm) => fmt_items!("%Y-%m-%dT%H:%M"),
599 (true, TimeShape::HmsF) => fmt_items!("%Y-%m-%dT%H:%M:%S%.f"),
600 (true, TimeShape::ImsP | TimeShape::HmsFP) => {
601 fmt_items!("%Y-%m-%dT%I:%M:%S%.f %P")
602 }
603 (true, TimeShape::ImP) => fmt_items!("%Y-%m-%dT%I:%M %P"),
604 (false, TimeShape::Hms) => fmt_items!("%Y-%m-%d %H:%M:%S"),
605 (false, TimeShape::Hm) => fmt_items!("%Y-%m-%d %H:%M"),
606 (false, TimeShape::HmsF) => fmt_items!("%Y-%m-%d %H:%M:%S%.f"),
607 (false, TimeShape::ImsP | TimeShape::HmsFP) => {
608 fmt_items!("%Y-%m-%d %I:%M:%S%.f %P")
609 }
610 (false, TimeShape::ImP) => fmt_items!("%Y-%m-%d %I:%M %P"),
611 };
612
613 self.dt_from_items(input, items)
614 .ok()
615 .map(|parsed| parsed.with_timezone(&Utc))
616 .map(Ok)
617 }
618
619 #[inline]
629 fn ymd_hms_z(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
630 if input.len() < 17 || !input.as_bytes()[10].is_ascii_whitespace() {
632 return None;
633 }
634 let prefix: &Regex = regex! {r"^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}(?::\d{2})?(?:\.\d{1,9})?"};
639 let fast_tz = prefix
640 .find(input)
641 .and_then(|m| tz_suffix(input, m.end(), false));
642
643 let re: &Regex = regex! {
644 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})$"
645 };
646
647 if let Some(tz) = fast_tz.or_else(|| {
648 re.captures(input)
649 .and_then(|caps| caps.name("tz").map(|m| m.as_str().trim()))
650 }) {
651 let parse_from_str = Self::naive_dt_from_items;
652 return match timezone::parse(tz) {
653 Ok(offset) => parse_from_str(input, fmt_items!("%Y-%m-%d %H:%M:%S %Z"))
654 .or_else(|_| parse_from_str(input, fmt_items!("%Y-%m-%d %H:%M %Z")))
655 .or_else(|_| parse_from_str(input, fmt_items!("%Y-%m-%d %H:%M:%S%.f %Z")))
656 .ok()
657 .and_then(|parsed| offset.from_local_datetime(&parsed).single())
658 .map(|datetime| datetime.with_timezone(&Utc))
659 .map(Ok),
660 Err(err) => Some(Err(err)),
661 };
662 }
663 None
664 }
665
666 #[inline]
669 fn ymd(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
670 let re: &Regex = regex! {r"^\d{4}-\d{2}-\d{2}$"
671 };
672
673 if !re.is_match(input) {
674 return None;
675 }
676 let now = Utc::now()
677 .date()
678 .and_time(self.default_time)?
679 .with_timezone(self.tz);
680 Self::naive_date_from_items(input, fmt_items!("%Y-%m-%d"))
681 .ok()
682 .map(|parsed| parsed.and_time(now.time()))
683 .and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
684 .map(|at_tz| at_tz.with_timezone(&Utc))
685 .map(Ok)
686 }
687
688 #[inline]
693 fn ymd_z(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
694 if input.len() <= 10 {
696 return None;
697 }
698 let re: &Regex = regex! {r"^\d{4}-\d{2}-\d{2}(?P<tz>\s*[+-:a-zA-Z0-9]{3,6})$"
699 };
700 if let Some(caps) = re.captures(input)
701 && let Some(matched_tz) = caps.name("tz")
702 {
703 return match timezone::parse(matched_tz.as_str().trim()) {
704 Ok(offset) => {
705 let now = Utc::now()
706 .date()
707 .and_time(self.default_time)?
708 .with_timezone(&offset);
709 Self::naive_date_from_items(input, fmt_items!("%Y-%m-%d %Z"))
710 .ok()
711 .map(|parsed| parsed.and_time(now.time()))
712 .and_then(|datetime| offset.from_local_datetime(&datetime).single())
713 .map(|at_tz| at_tz.with_timezone(&Utc))
714 .map(Ok)
715 }
716 Err(err) => Some(Err(err)),
717 };
718 }
719 None
720 }
721
722 #[inline]
725 fn month_ymd(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
726 let re: &Regex = regex! {r"^\d{4}-\w{3,9}-\d{2}$"
727 };
728 if !re.is_match(input) {
729 return None;
730 }
731
732 let now = Utc::now()
733 .date()
734 .and_time(self.default_time)?
735 .with_timezone(self.tz);
736 Self::naive_date_from_items(input, fmt_items!("%Y-%m-%d"))
737 .or_else(|_| Self::naive_date_from_items(input, fmt_items!("%Y-%b-%d")))
738 .ok()
739 .map(|parsed| parsed.and_time(now.time()))
740 .and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
741 .map(|at_tz| at_tz.with_timezone(&Utc))
742 .map(Ok)
743 }
744
745 #[inline]
750 fn month_mdy_hms(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
751 let re: &Regex = regex! {
752 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)?$"
753 };
754 if !re.is_match(input) {
755 return None;
756 }
757
758 let mut buf = [0_u8; NORMALIZE_SCRATCH];
763 let fallback;
764 let dt = match normalize_into(input, &mut buf, b",.", false) {
765 Some(s) => s,
766 None => {
767 fallback = input.replace([',', '.'], "");
768 fallback.as_str()
769 }
770 };
771 let items = match time_shape(dt) {
777 TimeShape::Hms => fmt_items!("%B %d %Y %H:%M:%S"),
778 TimeShape::Hm => fmt_items!("%B %d %Y %H:%M"),
779 TimeShape::ImsP => fmt_items!("%B %d %Y %I:%M:%S %P"),
780 TimeShape::ImP => fmt_items!("%B %d %Y %I:%M %P"),
781 TimeShape::HmsF | TimeShape::HmsFP => return None,
782 };
783 self.dt_from_items(dt, items)
784 .ok()
785 .map(|at_tz| at_tz.with_timezone(&Utc))
786 .map(Ok)
787 }
788
789 #[inline]
795 fn month_mdy_hms_z(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
796 if input.len() < 20 {
799 return None;
800 }
801 let bytes = input.as_bytes();
802 let has_year = (0..bytes.len().saturating_sub(3)).any(|i| {
803 bytes[i..i + 4].iter().all(|b| b.is_ascii_digit())
804 && (i == 0 || !bytes[i - 1].is_ascii_digit())
805 && bytes.get(i + 4).is_none_or(|b| !b.is_ascii_digit())
806 });
807 if !has_year {
808 return None;
809 }
810 let prefix: &Regex = regex! {
815 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)?"
816 };
817 let fast_tz = prefix
818 .find(input)
819 .and_then(|m| tz_suffix(input, m.end(), true));
820
821 let re: &Regex = regex! {
822 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})$",
823 };
824 if let Some(tz) = fast_tz.or_else(|| {
825 re.captures(input)
826 .and_then(|caps| caps.name("tz").map(|m| m.as_str().trim()))
827 }) {
828 let parse_from_str = Self::naive_dt_from_items;
829 return match timezone::parse(tz) {
830 Ok(offset) => {
831 let mut buf = [0_u8; NORMALIZE_SCRATCH];
832 let fallback;
833 let dt: &str = match normalize_into(input, &mut buf, b",", true) {
834 Some(s) => s,
835 None => {
836 let mut owned = input.replace(',', "");
837 if let Some(pos) = owned.find("at") {
838 owned.replace_range(pos..pos + 2, "");
839 }
840 fallback = owned;
841 fallback.as_str()
842 }
843 };
844 parse_from_str(dt, fmt_items!("%B %d %Y %H:%M:%S %Z"))
845 .or_else(|_| parse_from_str(dt, fmt_items!("%B %d %Y %H:%M %Z")))
846 .or_else(|_| parse_from_str(dt, fmt_items!("%B %d %Y %I:%M:%S %P %Z")))
847 .or_else(|_| parse_from_str(dt, fmt_items!("%B %d %Y %I:%M %P %Z")))
848 .ok()
849 .and_then(|parsed| offset.from_local_datetime(&parsed).single())
850 .map(|datetime| datetime.with_timezone(&Utc))
851 .map(Ok)
852 }
853 Err(err) => Some(Err(err)),
854 };
855 }
856 None
857 }
858
859 #[inline]
867 fn month_mdy(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
868 let re: &Regex = regex! {r"^[a-zA-Z]{3,9}\.?\s+\d{1,2},\s+\d{2,4}$"
869 };
870 if !re.is_match(input) {
871 return None;
872 }
873
874 let now = Utc::now()
875 .date()
876 .and_time(self.default_time)?
877 .with_timezone(self.tz);
878 let mut buf = [0_u8; NORMALIZE_SCRATCH];
882 let fallback;
883 let dt = match normalize_into(input, &mut buf, b",.", false) {
884 Some(s) => s,
885 None => {
886 fallback = input.replace([',', '.'], "");
887 fallback.as_str()
888 }
889 };
890 Self::naive_date_from_items(dt, fmt_items!("%B %d %y"))
891 .or_else(|_| Self::naive_date_from_items(dt, fmt_items!("%B %d %Y")))
892 .ok()
893 .map(|parsed| parsed.and_time(now.time()))
894 .and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
895 .map(|at_tz| at_tz.with_timezone(&Utc))
896 .map(Ok)
897 }
898
899 #[inline]
904 fn month_dmy_hms(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
905 if !input.as_bytes().contains(&b':') {
907 return None;
908 }
909 let re: &Regex = regex! {
910 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})?$"
911 };
912 if !re.is_match(input) {
913 return None;
914 }
915
916 let mut buf = [0_u8; NORMALIZE_SCRATCH];
917 let fallback;
918 let dt = match normalize_into(input, &mut buf, b",", false) {
919 Some(s) => s,
920 None => {
921 fallback = input.replace(',', "");
922 fallback.as_str()
923 }
924 };
925 let items = match time_shape(dt) {
930 TimeShape::Hms => fmt_items!("%d %B %Y %H:%M:%S"),
931 TimeShape::Hm => fmt_items!("%d %B %Y %H:%M"),
932 TimeShape::HmsF => fmt_items!("%d %B %Y %H:%M:%S%.f"),
933 TimeShape::ImP | TimeShape::ImsP | TimeShape::HmsFP => return None,
934 };
935 self.dt_from_items(dt, items)
936 .ok()
937 .map(|at_tz| at_tz.with_timezone(&Utc))
938 .map(Ok)
939 }
940
941 #[inline]
947 fn month_dmy(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
948 let re: &Regex = regex! {r"^\d{1,2}\s+[a-zA-Z]{3,9}\s+\d{2,4}$"
949 };
950 if !re.is_match(input) {
951 return None;
952 }
953
954 let now = Utc::now()
955 .date()
956 .and_time(self.default_time)?
957 .with_timezone(self.tz);
958 let bytes = input.as_bytes();
961 let len = bytes.len();
962 let four_digit_year = len >= 5
963 && bytes[len - 4..].iter().all(|b| b.is_ascii_digit())
964 && bytes[len - 5].is_ascii_whitespace();
965 let parsed = if four_digit_year {
966 Self::naive_date_from_items(input, fmt_items!("%d %B %Y"))
967 } else {
968 Self::naive_date_from_items(input, fmt_items!("%d %B %y"))
969 .or_else(|_| Self::naive_date_from_items(input, fmt_items!("%d %B %Y")))
970 };
971 parsed
972 .ok()
973 .map(|parsed| parsed.and_time(now.time()))
974 .and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
975 .map(|at_tz| at_tz.with_timezone(&Utc))
976 .map(Ok)
977 }
978
979 #[inline]
994 fn slash_mdy_hms(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
995 let re: &Regex = regex! {
996 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)?$"
997 };
998 if !re.is_match(input) {
999 return None;
1000 }
1001
1002 let items = match (
1007 slash_year_is_two_digits(input.as_bytes()),
1008 time_shape(input),
1009 ) {
1010 (true, TimeShape::Hms) => fmt_items!("%m/%d/%y %H:%M:%S"),
1011 (true, TimeShape::Hm) => fmt_items!("%m/%d/%y %H:%M"),
1012 (true, TimeShape::HmsF) => fmt_items!("%m/%d/%y %H:%M:%S%.f"),
1013 (true, TimeShape::ImsP | TimeShape::HmsFP) => {
1014 fmt_items!("%m/%d/%y %I:%M:%S%.f %P")
1015 }
1016 (true, TimeShape::ImP) => fmt_items!("%m/%d/%y %I:%M %P"),
1017 (false, TimeShape::Hms) => fmt_items!("%m/%d/%Y %H:%M:%S"),
1018 (false, TimeShape::Hm) => fmt_items!("%m/%d/%Y %H:%M"),
1019 (false, TimeShape::HmsF) => fmt_items!("%m/%d/%Y %H:%M:%S%.f"),
1020 (false, TimeShape::ImsP | TimeShape::HmsFP) => {
1021 fmt_items!("%m/%d/%Y %I:%M:%S%.f %P")
1022 }
1023 (false, TimeShape::ImP) => fmt_items!("%m/%d/%Y %I:%M %P"),
1024 };
1025 self.dt_from_items(input, items)
1026 .ok()
1027 .map(|at_tz| at_tz.with_timezone(&Utc))
1028 .map(Ok)
1029 }
1030
1031 #[inline]
1046 fn slash_dmy_hms(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
1047 let re: &Regex = regex! {
1048 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)?$"
1049 };
1050 if !re.is_match(input) {
1051 return None;
1052 }
1053
1054 let items = match (
1057 slash_year_is_two_digits(input.as_bytes()),
1058 time_shape(input),
1059 ) {
1060 (true, TimeShape::Hms) => fmt_items!("%d/%m/%y %H:%M:%S"),
1061 (true, TimeShape::Hm) => fmt_items!("%d/%m/%y %H:%M"),
1062 (true, TimeShape::HmsF) => fmt_items!("%d/%m/%y %H:%M:%S%.f"),
1063 (true, TimeShape::ImsP | TimeShape::HmsFP) => {
1064 fmt_items!("%d/%m/%y %I:%M:%S%.f %P")
1065 }
1066 (true, TimeShape::ImP) => fmt_items!("%d/%m/%y %I:%M %P"),
1067 (false, TimeShape::Hms) => fmt_items!("%d/%m/%Y %H:%M:%S"),
1068 (false, TimeShape::Hm) => fmt_items!("%d/%m/%Y %H:%M"),
1069 (false, TimeShape::HmsF) => fmt_items!("%d/%m/%Y %H:%M:%S%.f"),
1070 (false, TimeShape::ImsP | TimeShape::HmsFP) => {
1071 fmt_items!("%d/%m/%Y %I:%M:%S%.f %P")
1072 }
1073 (false, TimeShape::ImP) => fmt_items!("%d/%m/%Y %I:%M %P"),
1074 };
1075 self.dt_from_items(input, items)
1076 .ok()
1077 .map(|at_tz| at_tz.with_timezone(&Utc))
1078 .map(Ok)
1079 }
1080
1081 #[inline]
1087 fn slash_mdy(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
1088 let re: &Regex = regex! {r"^\d{1,2}/\d{1,2}/\d{2,4}$"
1089 };
1090 if !re.is_match(input) {
1091 return None;
1092 }
1093
1094 let now = Utc::now()
1095 .date()
1096 .and_time(self.default_time)?
1097 .with_timezone(self.tz);
1098 let fmt = if slash_year_is_two_digits(input.as_bytes()) {
1099 fmt_items!("%m/%d/%y")
1100 } else {
1101 fmt_items!("%m/%d/%Y")
1102 };
1103 Self::naive_date_from_items(input, fmt)
1104 .ok()
1105 .map(|parsed| parsed.and_time(now.time()))
1106 .and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
1107 .map(|at_tz| at_tz.with_timezone(&Utc))
1108 .map(Ok)
1109 }
1110
1111 #[inline]
1117 fn slash_dmy(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
1118 let re: &Regex = regex! {r"^[0-9]{1,2}/[0-9]{1,2}/[0-9]{2,4}$"
1119 };
1120 if !re.is_match(input) {
1121 return None;
1122 }
1123
1124 let now = Utc::now()
1125 .date()
1126 .and_time(self.default_time)?
1127 .with_timezone(self.tz);
1128 let fmt = if slash_year_is_two_digits(input.as_bytes()) {
1129 fmt_items!("%d/%m/%y")
1130 } else {
1131 fmt_items!("%d/%m/%Y")
1132 };
1133 Self::naive_date_from_items(input, fmt)
1134 .ok()
1135 .map(|parsed| parsed.and_time(now.time()))
1136 .and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
1137 .map(|at_tz| at_tz.with_timezone(&Utc))
1138 .map(Ok)
1139 }
1140
1141 #[inline]
1150 fn slash_ymd_hms(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
1151 let re: &Regex = regex! {
1152 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)?$"
1153 };
1154 if !re.is_match(input) {
1155 return None;
1156 }
1157
1158 let items = match time_shape(input) {
1159 TimeShape::Hms => fmt_items!("%Y/%m/%d %H:%M:%S"),
1160 TimeShape::Hm => fmt_items!("%Y/%m/%d %H:%M"),
1161 TimeShape::HmsF => fmt_items!("%Y/%m/%d %H:%M:%S%.f"),
1162 TimeShape::ImsP | TimeShape::HmsFP => fmt_items!("%Y/%m/%d %I:%M:%S%.f %P"),
1163 TimeShape::ImP => fmt_items!("%Y/%m/%d %I:%M %P"),
1164 };
1165 self.dt_from_items(input, items)
1166 .ok()
1167 .map(|at_tz| at_tz.with_timezone(&Utc))
1168 .map(Ok)
1169 }
1170
1171 #[inline]
1175 fn slash_ymd(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
1176 let re: &Regex = regex! {r"^[0-9]{4}/[0-9]{1,2}/[0-9]{1,2}$"
1177 };
1178 if !re.is_match(input) {
1179 return None;
1180 }
1181
1182 let now = Utc::now()
1183 .date()
1184 .and_time(self.default_time)?
1185 .with_timezone(self.tz);
1186 Self::naive_date_from_items(input, fmt_items!("%Y/%m/%d"))
1187 .ok()
1188 .map(|parsed| parsed.and_time(now.time()))
1189 .and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
1190 .map(|at_tz| at_tz.with_timezone(&Utc))
1191 .map(Ok)
1192 }
1193}
1194
1195#[cfg(test)]
1196mod tests {
1197 use super::*;
1198
1199 #[test]
1200 fn unix_timestamp() {
1201 let parse = Parse::new(&Utc, Utc::now().time());
1202
1203 let test_cases = vec![
1204 ("0", Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)),
1205 ("0000000000", Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)),
1206 ("0000000000000", Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)),
1207 ("0000000000000000000", Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)),
1208 ("-770172300", Utc.ymd(1945, 8, 5).and_hms(23, 15, 0)),
1209 (
1210 "1671673426.123456789",
1211 Utc.ymd(2022, 12, 22).and_hms_nano(1, 43, 46, 123456768),
1212 ),
1213 ("1511648546", Utc.ymd(2017, 11, 25).and_hms(22, 22, 26)),
1214 (
1215 "1620036248.420",
1216 Utc.ymd(2021, 5, 3).and_hms_milli(10, 4, 8, 420),
1217 ),
1218 (
1219 "1620036248.717915136",
1220 Utc.ymd(2021, 5, 3).and_hms_nano(10, 4, 8, 717915136),
1221 ),
1222 ];
1223
1224 for &(input, want) in test_cases.iter() {
1225 assert_eq!(
1226 parse.unix_timestamp(input).unwrap().unwrap(),
1227 want,
1228 "unix_timestamp/{}",
1229 input
1230 )
1231 }
1232 assert!(parse.unix_timestamp("15116").is_some());
1233 assert!(
1234 parse
1235 .unix_timestamp("16200248727179150001620024872717915000") .is_some()
1237 );
1238 assert!(parse.unix_timestamp("not-a-ts").is_none());
1239 for input in [
1242 "inf", "nan", "INF", "NaN", "infinity", "+inf", "-inf", "-nan",
1243 ] {
1244 assert!(
1245 parse.unix_timestamp(input).is_none(),
1246 "unix_timestamp must reject non-finite {input}"
1247 );
1248 }
1249 }
1250
1251 #[test]
1252 fn rfc3339() {
1253 let parse = Parse::new(&Utc, Utc::now().time());
1254
1255 let test_cases = [
1256 (
1257 "2021-05-01T01:17:02.604456Z",
1258 Utc.ymd(2021, 5, 1).and_hms_nano(1, 17, 2, 604456000),
1259 ),
1260 (
1261 "2017-11-25T22:34:50Z",
1262 Utc.ymd(2017, 11, 25).and_hms(22, 34, 50),
1263 ),
1264 ];
1265
1266 for &(input, want) in test_cases.iter() {
1267 assert_eq!(
1268 parse.rfc3339(input).unwrap().unwrap(),
1269 want,
1270 "rfc3339/{}",
1271 input
1272 )
1273 }
1274 assert!(parse.rfc3339("2017-11-25 22:34:50").is_none());
1275 assert!(parse.rfc3339("not-date-time").is_none());
1276 }
1277
1278 #[test]
1279 fn rfc2822() {
1280 let parse = Parse::new(&Utc, Utc::now().time());
1281
1282 let test_cases = [
1283 (
1284 "Wed, 02 Jun 2021 06:31:39 GMT",
1285 Utc.ymd(2021, 6, 2).and_hms(6, 31, 39),
1286 ),
1287 (
1288 "Wed, 02 Jun 2021 06:31:39 PDT",
1289 Utc.ymd(2021, 6, 2).and_hms(13, 31, 39),
1290 ),
1291 ];
1292
1293 for &(input, want) in test_cases.iter() {
1294 assert_eq!(
1295 parse.rfc2822(input).unwrap().unwrap(),
1296 want,
1297 "rfc2822/{}",
1298 input
1299 )
1300 }
1301 assert!(parse.rfc2822("02 Jun 2021 06:31:39").is_none());
1302 assert!(parse.rfc2822("not-date-time").is_none());
1303 }
1304
1305 #[test]
1306 fn ymd_hms() {
1307 let parse = Parse::new(&Utc, Utc::now().time());
1308
1309 let test_cases = [
1310 ("2021-04-30 21:14", Utc.ymd(2021, 4, 30).and_hms(21, 14, 0)),
1311 (
1312 "2021-04-30 21:14:10",
1313 Utc.ymd(2021, 4, 30).and_hms(21, 14, 10),
1314 ),
1315 (
1316 "2021-04-30 21:14:10.052282",
1317 Utc.ymd(2021, 4, 30).and_hms_micro(21, 14, 10, 52282),
1318 ),
1319 (
1320 "2014-04-26 05:24:37 PM",
1321 Utc.ymd(2014, 4, 26).and_hms(17, 24, 37),
1322 ),
1323 (
1324 "2014-04-26 17:24:37.123",
1325 Utc.ymd(2014, 4, 26).and_hms_milli(17, 24, 37, 123),
1326 ),
1327 (
1328 "2014-04-26 17:24:37.3186369",
1329 Utc.ymd(2014, 4, 26).and_hms_nano(17, 24, 37, 318636900),
1330 ),
1331 (
1332 "2012-08-03 18:31:59.257000000",
1333 Utc.ymd(2012, 8, 3).and_hms_nano(18, 31, 59, 257000000),
1334 ),
1335 ("2020-01-15T08:00", Utc.ymd(2020, 1, 15).and_hms(8, 0, 0)),
1338 ("2020-01-15T08:00:00", Utc.ymd(2020, 1, 15).and_hms(8, 0, 0)),
1339 (
1340 "2020-01-15T08:00:00.123",
1341 Utc.ymd(2020, 1, 15).and_hms_milli(8, 0, 0, 123),
1342 ),
1343 (
1344 "2020-01-15T08:00:00.123456",
1345 Utc.ymd(2020, 1, 15).and_hms_micro(8, 0, 0, 123456),
1346 ),
1347 (
1348 "2020-01-15T08:00:00.123456789",
1349 Utc.ymd(2020, 1, 15).and_hms_nano(8, 0, 0, 123456789),
1350 ),
1351 ];
1352
1353 for &(input, want) in test_cases.iter() {
1354 assert_eq!(
1355 parse.ymd_hms(input).unwrap().unwrap(),
1356 want,
1357 "ymd_hms/{}",
1358 input
1359 )
1360 }
1361 assert!(parse.ymd_hms("not-date-time").is_none());
1362
1363 let t_form = parse.ymd_hms("2020-01-15T08:00:00").unwrap().unwrap();
1365 let space_form = parse.ymd_hms("2020-01-15 08:00:00").unwrap().unwrap();
1366 assert_eq!(t_form, space_form, "T-separator vs space disagree");
1367 }
1368
1369 #[test]
1370 fn ymd_hms_z() {
1371 let parse = Parse::new(&Utc, Utc::now().time());
1372
1373 let test_cases = [
1374 (
1375 "2017-11-25 13:31:15 PST",
1376 Utc.ymd(2017, 11, 25).and_hms(21, 31, 15),
1377 ),
1378 (
1379 "2017-11-25 13:31 PST",
1380 Utc.ymd(2017, 11, 25).and_hms(21, 31, 0),
1381 ),
1382 (
1383 "2014-12-16 06:20:00 UTC",
1384 Utc.ymd(2014, 12, 16).and_hms(6, 20, 0),
1385 ),
1386 (
1387 "2014-12-16 06:20:00 GMT",
1388 Utc.ymd(2014, 12, 16).and_hms(6, 20, 0),
1389 ),
1390 (
1391 "2014-04-26 13:13:43 +0800",
1392 Utc.ymd(2014, 4, 26).and_hms(5, 13, 43),
1393 ),
1394 (
1395 "2014-04-26 13:13:44 +09:00",
1396 Utc.ymd(2014, 4, 26).and_hms(4, 13, 44),
1397 ),
1398 (
1399 "2012-08-03 18:31:59.257000000 +0000",
1400 Utc.ymd(2012, 8, 3).and_hms_nano(18, 31, 59, 257000000),
1401 ),
1402 (
1403 "2015-09-30 18:48:56.35272715 UTC",
1404 Utc.ymd(2015, 9, 30).and_hms_nano(18, 48, 56, 352727150),
1405 ),
1406 ];
1407
1408 for &(input, want) in test_cases.iter() {
1409 assert_eq!(
1410 parse.ymd_hms_z(input).unwrap().unwrap(),
1411 want,
1412 "ymd_hms_z/{}",
1413 input
1414 )
1415 }
1416 assert!(parse.ymd_hms_z("not-date-time").is_none());
1417 assert!(parse.ymd_hms_z("2021-04-30 21:14").is_none()); assert!(parse.ymd_hms_z("2021-04-30X21:14Z").is_none()); assert!(parse.ymd_hms_z("2021-04-30 21:1XZ").is_none()); }
1424
1425 #[test]
1426 fn ymd() {
1427 let parse = Parse::new(&Utc, Utc::now().time());
1428
1429 let test_cases = [(
1430 "2021-02-21",
1431 Utc.ymd(2021, 2, 21).and_time(Utc::now().time()),
1432 )];
1433
1434 for &(input, want) in test_cases.iter() {
1435 assert_eq!(
1436 parse
1437 .ymd(input)
1438 .unwrap()
1439 .unwrap()
1440 .trunc_subsecs(0)
1441 .with_second(0)
1442 .unwrap(),
1443 want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
1444 "ymd/{}",
1445 input
1446 )
1447 }
1448 assert!(parse.ymd("not-date-time").is_none());
1449 }
1450
1451 #[test]
1452 fn ymd_z() {
1453 let parse = Parse::new(&Utc, Utc::now().time());
1454 let now_at_pst = Utc::now().with_timezone(&FixedOffset::west(8 * 3600));
1455 let now_at_cst = Utc::now().with_timezone(&FixedOffset::east(8 * 3600));
1456
1457 let test_cases = [
1458 (
1459 "2021-02-21 PST",
1460 FixedOffset::west(8 * 3600)
1461 .ymd(2021, 2, 21)
1462 .and_time(now_at_pst.time())
1463 .map(|dt| dt.with_timezone(&Utc)),
1464 ),
1465 (
1466 "2021-02-21 UTC",
1467 FixedOffset::west(0)
1468 .ymd(2021, 2, 21)
1469 .and_time(Utc::now().time())
1470 .map(|dt| dt.with_timezone(&Utc)),
1471 ),
1472 (
1473 "2020-07-20+08:00",
1474 FixedOffset::east(8 * 3600)
1475 .ymd(2020, 7, 20)
1476 .and_time(now_at_cst.time())
1477 .map(|dt| dt.with_timezone(&Utc)),
1478 ),
1479 ];
1480
1481 for &(input, want) in test_cases.iter() {
1482 assert_eq!(
1483 parse
1484 .ymd_z(input)
1485 .unwrap()
1486 .unwrap()
1487 .trunc_subsecs(0)
1488 .with_second(0)
1489 .unwrap(),
1490 want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
1491 "ymd_z/{}",
1492 input
1493 )
1494 }
1495 assert!(parse.ymd_z("not-date-time").is_none());
1496 assert!(parse.ymd_z("2021-02-21").is_none()); assert!(parse.ymd_z("2021-02-21X").is_none()); }
1500
1501 #[test]
1502 fn month_ymd() {
1503 let parse = Parse::new(&Utc, Utc::now().time());
1504
1505 let test_cases = [(
1506 "2021-Feb-21",
1507 Utc.ymd(2021, 2, 21).and_time(Utc::now().time()),
1508 )];
1509
1510 for &(input, want) in test_cases.iter() {
1511 assert_eq!(
1512 parse
1513 .month_ymd(input)
1514 .unwrap()
1515 .unwrap()
1516 .trunc_subsecs(0)
1517 .with_second(0)
1518 .unwrap(),
1519 want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
1520 "month_ymd/{}",
1521 input
1522 )
1523 }
1524 assert!(parse.month_ymd("not-date-time").is_none());
1525 }
1526
1527 #[test]
1528 fn month_mdy_hms() {
1529 let parse = Parse::new(&Utc, Utc::now().time());
1530
1531 let test_cases = [
1532 (
1533 "May 8, 2009 5:57:51 PM",
1534 Utc.ymd(2009, 5, 8).and_hms(17, 57, 51),
1535 ),
1536 (
1537 "September 17, 2012 10:09am",
1538 Utc.ymd(2012, 9, 17).and_hms(10, 9, 0),
1539 ),
1540 (
1541 "September 17, 2012, 10:10:09",
1542 Utc.ymd(2012, 9, 17).and_hms(10, 10, 9),
1543 ),
1544 ];
1545
1546 for &(input, want) in test_cases.iter() {
1547 assert_eq!(
1548 parse.month_mdy_hms(input).unwrap().unwrap(),
1549 want,
1550 "month_mdy_hms/{}",
1551 input
1552 )
1553 }
1554 assert!(parse.month_mdy_hms("not-date-time").is_none());
1555 }
1556
1557 #[test]
1558 fn month_mdy_hms_z() {
1559 let parse = Parse::new(&Utc, Utc::now().time());
1560
1561 let test_cases = [
1562 (
1563 "May 02, 2021 15:51:31 UTC",
1564 Utc.ymd(2021, 5, 2).and_hms(15, 51, 31),
1565 ),
1566 (
1567 "May 02, 2021 15:51 UTC",
1568 Utc.ymd(2021, 5, 2).and_hms(15, 51, 0),
1569 ),
1570 (
1571 "May 26, 2021, 12:49 AM PDT",
1572 Utc.ymd(2021, 5, 26).and_hms(7, 49, 0),
1573 ),
1574 (
1575 "September 17, 2012 at 10:09am PST",
1576 Utc.ymd(2012, 9, 17).and_hms(18, 9, 0),
1577 ),
1578 ];
1579
1580 for &(input, want) in test_cases.iter() {
1581 assert_eq!(
1582 parse.month_mdy_hms_z(input).unwrap().unwrap(),
1583 want,
1584 "month_mdy_hms_z/{}",
1585 input
1586 )
1587 }
1588 assert!(parse.month_mdy_hms_z("not-date-time").is_none());
1589 assert!(parse.month_mdy_hms_z("May 27, 02:45:27 XX PST").is_none()); assert!(parse.month_mdy_hms_z("May 27 1234 something PST").is_none()); }
1594
1595 #[test]
1596 fn month_mdy() {
1597 let parse = Parse::new(&Utc, Utc::now().time());
1598
1599 let test_cases = [
1600 (
1601 "May 25, 2021",
1602 Utc.ymd(2021, 5, 25).and_time(Utc::now().time()),
1603 ),
1604 (
1605 "oct 7, 1970",
1606 Utc.ymd(1970, 10, 7).and_time(Utc::now().time()),
1607 ),
1608 (
1609 "oct 7, 70",
1610 Utc.ymd(1970, 10, 7).and_time(Utc::now().time()),
1611 ),
1612 (
1613 "oct. 7, 1970",
1614 Utc.ymd(1970, 10, 7).and_time(Utc::now().time()),
1615 ),
1616 (
1617 "oct. 7, 70",
1618 Utc.ymd(1970, 10, 7).and_time(Utc::now().time()),
1619 ),
1620 (
1621 "October 7, 1970",
1622 Utc.ymd(1970, 10, 7).and_time(Utc::now().time()),
1623 ),
1624 ];
1625
1626 for &(input, want) in test_cases.iter() {
1627 assert_eq!(
1628 parse
1629 .month_mdy(input)
1630 .unwrap()
1631 .unwrap()
1632 .trunc_subsecs(0)
1633 .with_second(0)
1634 .unwrap(),
1635 want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
1636 "month_mdy/{}",
1637 input
1638 )
1639 }
1640 assert!(parse.month_mdy("not-date-time").is_none());
1641 }
1642
1643 #[test]
1644 fn month_dmy_hms() {
1645 let parse = Parse::new(&Utc, Utc::now().time());
1646
1647 let test_cases = [
1648 (
1649 "12 Feb 2006, 19:17",
1650 Utc.ymd(2006, 2, 12).and_hms(19, 17, 0),
1651 ),
1652 ("12 Feb 2006 19:17", Utc.ymd(2006, 2, 12).and_hms(19, 17, 0)),
1653 (
1654 "14 May 2019 19:11:40.164",
1655 Utc.ymd(2019, 5, 14).and_hms_milli(19, 11, 40, 164),
1656 ),
1657 ];
1658
1659 for &(input, want) in test_cases.iter() {
1660 assert_eq!(
1661 parse.month_dmy_hms(input).unwrap().unwrap(),
1662 want,
1663 "month_dmy_hms/{}",
1664 input
1665 )
1666 }
1667 assert!(parse.month_dmy_hms("not-date-time").is_none());
1668 }
1669
1670 #[test]
1671 fn month_dmy() {
1672 let parse = Parse::new(&Utc, Utc::now().time());
1673
1674 let test_cases = [
1675 ("7 oct 70", Utc.ymd(1970, 10, 7).and_time(Utc::now().time())),
1676 (
1677 "7 oct 1970",
1678 Utc.ymd(1970, 10, 7).and_time(Utc::now().time()),
1679 ),
1680 (
1681 "03 February 2013",
1682 Utc.ymd(2013, 2, 3).and_time(Utc::now().time()),
1683 ),
1684 (
1685 "1 July 2013",
1686 Utc.ymd(2013, 7, 1).and_time(Utc::now().time()),
1687 ),
1688 ];
1689
1690 for &(input, want) in test_cases.iter() {
1691 assert_eq!(
1692 parse
1693 .month_dmy(input)
1694 .unwrap()
1695 .unwrap()
1696 .trunc_subsecs(0)
1697 .with_second(0)
1698 .unwrap(),
1699 want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
1700 "month_dmy/{}",
1701 input
1702 )
1703 }
1704 assert!(parse.month_dmy("not-date-time").is_none());
1705 }
1706
1707 #[test]
1710 fn month_dmy_year_fast_path() {
1711 let parse = Parse::new(&Utc, Utc::now().time());
1712
1713 let four_digit = parse.month_dmy("14 May 2019").unwrap().unwrap();
1715 assert_eq!(four_digit.year(), 2019);
1716 assert_eq!(four_digit.month(), 5);
1717 assert_eq!(four_digit.day(), 14);
1718
1719 let two_digit = parse.month_dmy("14 May 19").unwrap().unwrap();
1722 assert_eq!(two_digit.year(), 2019);
1723 assert_eq!(two_digit.month(), 5);
1724 assert_eq!(two_digit.day(), 14);
1725 }
1726
1727 #[test]
1728 fn slash_mdy_hms() {
1729 let parse = Parse::new(&Utc, Utc::now().time());
1730
1731 let test_cases = vec![
1732 ("4/8/2014 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)),
1733 ("04/08/2014 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)),
1734 ("4/8/14 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)),
1735 ("04/2/2014 03:00:51", Utc.ymd(2014, 4, 2).and_hms(3, 0, 51)),
1736 ("8/8/1965 12:00:00 AM", Utc.ymd(1965, 8, 8).and_hms(0, 0, 0)),
1737 (
1738 "8/8/1965 01:00:01 PM",
1739 Utc.ymd(1965, 8, 8).and_hms(13, 0, 1),
1740 ),
1741 ("8/8/1965 01:00 PM", Utc.ymd(1965, 8, 8).and_hms(13, 0, 0)),
1742 ("8/8/1965 1:00 PM", Utc.ymd(1965, 8, 8).and_hms(13, 0, 0)),
1743 ("8/8/1965 12:00 AM", Utc.ymd(1965, 8, 8).and_hms(0, 0, 0)),
1744 ("4/02/2014 03:00:51", Utc.ymd(2014, 4, 2).and_hms(3, 0, 51)),
1745 (
1746 "03/19/2012 10:11:59",
1747 Utc.ymd(2012, 3, 19).and_hms(10, 11, 59),
1748 ),
1749 (
1750 "03/19/2012 10:11:59.3186369",
1751 Utc.ymd(2012, 3, 19).and_hms_nano(10, 11, 59, 318636900),
1752 ),
1753 ];
1754
1755 for &(input, want) in test_cases.iter() {
1756 assert_eq!(
1757 parse.slash_mdy_hms(input).unwrap().unwrap(),
1758 want,
1759 "slash_mdy_hms/{}",
1760 input
1761 )
1762 }
1763 assert!(parse.slash_mdy_hms("not-date-time").is_none());
1764 }
1765
1766 #[test]
1767 fn slash_mdy() {
1768 let parse = Parse::new(&Utc, Utc::now().time());
1769
1770 let test_cases = [
1771 (
1772 "3/31/2014",
1773 Utc.ymd(2014, 3, 31).and_time(Utc::now().time()),
1774 ),
1775 (
1776 "03/31/2014",
1777 Utc.ymd(2014, 3, 31).and_time(Utc::now().time()),
1778 ),
1779 ("08/21/71", Utc.ymd(1971, 8, 21).and_time(Utc::now().time())),
1780 ("8/1/71", Utc.ymd(1971, 8, 1).and_time(Utc::now().time())),
1781 ];
1782
1783 for &(input, want) in test_cases.iter() {
1784 assert_eq!(
1785 parse
1786 .slash_mdy(input)
1787 .unwrap()
1788 .unwrap()
1789 .trunc_subsecs(0)
1790 .with_second(0)
1791 .unwrap(),
1792 want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
1793 "slash_mdy/{}",
1794 input
1795 )
1796 }
1797 assert!(parse.slash_mdy("not-date-time").is_none());
1798 }
1799
1800 #[test]
1801 fn slash_dmy() {
1802 let mut parse = Parse::new(&Utc, Utc::now().time());
1803
1804 let test_cases = [
1805 (
1806 "31/3/2014",
1807 Utc.ymd(2014, 3, 31).and_time(Utc::now().time()),
1808 ),
1809 (
1810 "13/11/2014",
1811 Utc.ymd(2014, 11, 13).and_time(Utc::now().time()),
1812 ),
1813 ("21/08/71", Utc.ymd(1971, 8, 21).and_time(Utc::now().time())),
1814 ("1/8/71", Utc.ymd(1971, 8, 1).and_time(Utc::now().time())),
1815 ];
1816
1817 for &(input, want) in test_cases.iter() {
1818 assert_eq!(
1819 parse
1820 .prefer_dmy(true)
1821 .slash_dmy(input)
1822 .unwrap()
1823 .unwrap()
1824 .trunc_subsecs(0)
1825 .with_second(0)
1826 .unwrap(),
1827 want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
1828 "slash_dmy/{}",
1829 input
1830 )
1831 }
1832 assert!(parse.slash_dmy("not-date-time").is_none());
1833 }
1834
1835 #[test]
1836 fn slash_ymd_hms() {
1837 let parse = Parse::new(&Utc, Utc::now().time());
1838
1839 let test_cases = [
1840 ("2014/4/8 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)),
1841 ("2014/04/08 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)),
1842 ("2014/04/2 03:00:51", Utc.ymd(2014, 4, 2).and_hms(3, 0, 51)),
1843 ("2014/4/02 03:00:51", Utc.ymd(2014, 4, 2).and_hms(3, 0, 51)),
1844 (
1845 "2012/03/19 10:11:59",
1846 Utc.ymd(2012, 3, 19).and_hms(10, 11, 59),
1847 ),
1848 (
1849 "2012/03/19 10:11:59.3186369",
1850 Utc.ymd(2012, 3, 19).and_hms_nano(10, 11, 59, 318636900),
1851 ),
1852 ];
1853
1854 for &(input, want) in test_cases.iter() {
1855 assert_eq!(
1856 parse.slash_ymd_hms(input).unwrap().unwrap(),
1857 want,
1858 "slash_ymd_hms/{}",
1859 input
1860 )
1861 }
1862 assert!(parse.slash_ymd_hms("not-date-time").is_none());
1863 }
1864
1865 #[test]
1866 fn slash_ymd() {
1867 let parse = Parse::new(&Utc, Utc::now().time());
1868
1869 let test_cases = [
1870 (
1871 "2014/3/31",
1872 Utc.ymd(2014, 3, 31).and_time(Utc::now().time()),
1873 ),
1874 (
1875 "2014/03/31",
1876 Utc.ymd(2014, 3, 31).and_time(Utc::now().time()),
1877 ),
1878 ];
1879
1880 for &(input, want) in test_cases.iter() {
1881 assert_eq!(
1882 parse
1883 .slash_ymd(input)
1884 .unwrap()
1885 .unwrap()
1886 .trunc_subsecs(0)
1887 .with_second(0)
1888 .unwrap(),
1889 want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
1890 "slash_ymd/{}",
1891 input
1892 )
1893 }
1894 assert!(parse.slash_ymd("not-date-time").is_none());
1895 }
1896
1897 #[test]
1898 fn time_shape_classification() {
1899 use TimeShape::{Hm, Hms, HmsF, HmsFP, ImP, ImsP};
1900
1901 let cases = [
1902 ("2021-04-30 21:14", Hm),
1903 ("2021-04-30T21:14", Hm),
1904 ("2021-04-30 21:14:10", Hms),
1905 ("2021-04-30 21:14:10.052282", HmsF),
1906 ("8/8/1965 12:00 AM", ImP),
1907 ("8/8/1965 12:00am", ImP),
1908 ("8/8/1965 01:00:01 PM", ImsP),
1909 ("September 17 2012 10:09am", ImP),
1910 ("03/19/2012 10:11:59.318 PM", HmsFP),
1911 ("03/19/2012 10:11.123", Hm),
1914 ];
1915 for (input, want) in cases {
1916 assert!(
1917 time_shape(input) == want,
1918 "time_shape misclassified {input}"
1919 );
1920 }
1921 }
1922
1923 #[test]
1932 fn every_fmt_items_literal_is_valid() {
1933 let src = include_str!("datetime.rs");
1934 let call = Regex::new(r#"fmt_items!\("([^"]*)"\)"#).unwrap();
1936
1937 let mut checked = 0_usize;
1938 for caps in call.captures_iter(src) {
1939 let fmt = &caps[1];
1940 assert!(
1941 chrono::format::StrftimeItems::new(fmt).parse().is_ok(),
1942 "invalid strftime literal: {fmt}"
1943 );
1944 checked += 1;
1945 }
1946 assert!(
1948 checked >= 50,
1949 "expected to check every fmt_items! literal, only found {checked}"
1950 );
1951 }
1952
1953 #[test]
1961 fn fractional_seconds_with_ampm() {
1962 let parse = Parse::new(&Utc, Utc::now().time());
1963
1964 let cases = [
1965 (
1966 "03/19/2012 10:11:59.318 PM",
1967 Utc.ymd(2012, 3, 19).and_hms_milli(22, 11, 59, 318),
1968 ),
1969 (
1970 "3/19/2012 1:11:59.318 am",
1971 Utc.ymd(2012, 3, 19).and_hms_milli(1, 11, 59, 318),
1972 ),
1973 (
1974 "03/19/12 10:11:59.318 PM",
1975 Utc.ymd(2012, 3, 19).and_hms_milli(22, 11, 59, 318),
1976 ),
1977 (
1978 "2012/03/19 10:11:59.318 PM",
1979 Utc.ymd(2012, 3, 19).and_hms_milli(22, 11, 59, 318),
1980 ),
1981 (
1982 "2012-03-19 10:11:59.318 PM",
1983 Utc.ymd(2012, 3, 19).and_hms_milli(22, 11, 59, 318),
1984 ),
1985 (
1986 "2012-03-19T10:11:59.318 PM",
1987 Utc.ymd(2012, 3, 19).and_hms_milli(22, 11, 59, 318),
1988 ),
1989 (
1990 "2012-03-19 10:11:59.3186369 PM",
1991 Utc.ymd(2012, 3, 19).and_hms_nano(22, 11, 59, 318636900),
1992 ),
1993 (
1995 "03/19/2012 10:11:59 PM",
1996 Utc.ymd(2012, 3, 19).and_hms(22, 11, 59),
1997 ),
1998 (
1999 "2012-03-19 10:11:59 PM",
2000 Utc.ymd(2012, 3, 19).and_hms(22, 11, 59),
2001 ),
2002 ];
2003
2004 for (input, want) in cases {
2005 assert_eq!(parse.parse(input).unwrap(), want, "parse/{input}");
2006 }
2007
2008 let dmy = Parse::new_with_preference(&Utc, Utc::now().time(), true);
2012 let dmy_cases = [
2013 (
2014 "19/03/2012 10:11:59.318 PM",
2015 Utc.ymd(2012, 3, 19).and_hms_milli(22, 11, 59, 318),
2016 ),
2017 (
2018 "19/03/12 10:11:59.318 PM",
2019 Utc.ymd(2012, 3, 19).and_hms_milli(22, 11, 59, 318),
2020 ),
2021 (
2022 "9/3/2012 1:11:59.318 am",
2023 Utc.ymd(2012, 3, 9).and_hms_milli(1, 11, 59, 318),
2024 ),
2025 (
2027 "19/03/2012 10:11:59 PM",
2028 Utc.ymd(2012, 3, 19).and_hms(22, 11, 59),
2029 ),
2030 ];
2031
2032 for (input, want) in dmy_cases {
2033 assert_eq!(dmy.parse(input).unwrap(), want, "prefer_dmy/{input}");
2034 }
2035 }
2036
2037 #[test]
2046 fn unsupported_shapes_still_fail() {
2047 let parse = Parse::new(&Utc, Utc::now().time());
2048
2049 for input in [
2050 "May 8, 2009 5:57:51.123 PM",
2053 "14 May 2019 07:11:40 PM",
2059 "14 May 2019 07:11:40.164 PM",
2060 "03/19/2012 10:11.123",
2065 "2021-04-30 21:14.052282",
2066 "03/19/2012 10:11.123 PM",
2067 "03/19/2012 22:11:59.318 PM",
2070 ] {
2071 assert!(
2072 parse.parse(input).is_err(),
2073 "{input} is expected to remain unsupported"
2074 );
2075 }
2076 }
2077}