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
65#[inline]
73fn cannot_be_date(input: &str) -> bool {
74 input.bytes().any(|b| !DATE_BYTE[b as usize])
75}
76
77#[derive(Clone, Copy, PartialEq, Eq)]
85enum TimeShape {
86 Hm,
88 Hms,
90 HmsF,
92 ImP,
94 ImsP,
96 HmsFP,
103}
104
105#[inline]
118fn time_shape(input: &str) -> TimeShape {
119 let bytes = input.as_bytes();
120
121 let ampm = bytes.len() >= 2 && {
122 let [.., ap, m] = bytes else { unreachable!() };
123 m | 32 == b'm' && matches!(ap | 32, b'a' | b'p')
124 };
125
126 let mut colons = 0_u8;
127 let mut fraction = false;
128 for &b in bytes {
129 match b {
130 b':' => colons += 1,
131 b'.' => fraction = true,
132 _ => {}
133 }
134 }
135
136 match (colons >= 2, fraction, ampm) {
137 (true, true, true) => TimeShape::HmsFP,
138 (true, true, false) => TimeShape::HmsF,
139 (true, false, true) => TimeShape::ImsP,
140 (true, false, false) => TimeShape::Hms,
141 (false, _, true) => TimeShape::ImP,
142 (false, _, false) => TimeShape::Hm,
143 }
144}
145
146#[inline]
156fn slash_year_is_two_digits(bytes: &[u8]) -> bool {
157 let mut slashes = 0u8;
158 let mut year_len = 0usize;
159 for &b in bytes {
160 if b == b'/' {
161 slashes += 1;
162 } else if slashes == 2 {
163 if b.is_ascii_digit() {
164 year_len += 1;
165 } else {
166 break;
167 }
168 }
169 }
170 year_len == 2
171}
172
173pub struct Parse<'z, Tz2> {
175 tz: &'z Tz2,
176 default_time: NaiveTime,
177 prefer_dmy: bool,
178}
179
180impl<'z, Tz2> Parse<'z, Tz2>
181where
182 Tz2: TimeZone,
183{
184 pub const fn new(tz: &'z Tz2, default_time: NaiveTime) -> Self {
187 Self {
188 tz,
189 default_time,
190 prefer_dmy: false,
191 }
192 }
193
194 pub const fn prefer_dmy(&mut self, yes: bool) -> &Self {
195 self.prefer_dmy = yes;
196 self
197 }
198
199 pub const fn new_with_preference(
202 tz: &'z Tz2,
203 default_time: NaiveTime,
204 prefer_dmy: bool,
205 ) -> Self {
206 Self {
207 tz,
208 default_time,
209 prefer_dmy,
210 }
211 }
212
213 #[inline]
220 fn dt_from_items(&self, input: &str, items: &[Item<'static>]) -> ParseResult<DateTime<Tz2>> {
221 let mut parsed = Parsed::new();
222 parse_items(&mut parsed, input, items.iter())?;
223 parsed.to_datetime_with_timezone(self.tz)
224 }
225
226 #[inline]
229 fn naive_dt_from_items(input: &str, items: &[Item<'static>]) -> ParseResult<NaiveDateTime> {
230 let mut parsed = Parsed::new();
231 parse_items(&mut parsed, input, items.iter())?;
232 parsed.to_naive_datetime_with_offset(0)
233 }
234
235 #[inline]
238 fn naive_date_from_items(input: &str, items: &[Item<'static>]) -> ParseResult<NaiveDate> {
239 let mut parsed = Parsed::new();
240 parse_items(&mut parsed, input, items.iter())?;
241 parsed.to_naive_date()
242 }
243
244 #[inline]
265 pub fn parse(&self, input: &str) -> Result<DateTime<Utc>> {
266 if cannot_be_date(input) {
267 return Err(anyhow!("{} did not match any formats.", input));
268 }
269 self.slash_mdy_family(input)
270 .or_else(|| self.slash_ymd_family(input))
271 .or_else(|| self.ymd_family(input))
272 .or_else(|| self.month_ymd(input))
273 .or_else(|| self.month_mdy_family(input))
274 .or_else(|| self.month_dmy_family(input))
275 .or_else(|| self.unix_timestamp(input))
276 .or_else(|| self.rfc2822(input))
277 .unwrap_or_else(|| Err(anyhow!("{} did not match any formats.", input)))
278 }
279
280 #[inline]
281 fn ymd_family(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
282 let re: &Regex = regex! {
283 r"^\d{4}-\d{2}"
284
285 };
286
287 if !re.is_match(input) {
288 return None;
289 }
290 self.rfc3339(input)
291 .or_else(|| self.ymd_hms(input))
292 .or_else(|| self.ymd_hms_z(input))
293 .or_else(|| self.ymd(input))
294 .or_else(|| self.ymd_z(input))
295 }
296
297 #[inline]
298 fn month_mdy_family(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
299 let re: &Regex = regex! {
300 r"^[a-zA-Z]{3,9}\.?\s+\d{1,2}"
301 };
302
303 if !re.is_match(input) {
304 return None;
305 }
306 self.month_mdy_hms(input)
307 .or_else(|| self.month_mdy_hms_z(input))
308 .or_else(|| self.month_mdy(input))
309 }
310
311 #[inline]
312 fn month_dmy_family(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
313 let re: &Regex = regex! {r"^\d{1,2}\s+[a-zA-Z]{3,9}"
314 };
315
316 if !re.is_match(input) {
317 return None;
318 }
319 self.month_dmy_hms(input).or_else(|| self.month_dmy(input))
320 }
321
322 #[inline]
323 fn slash_mdy_family(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
324 let re: &Regex = regex! {r"^\d{1,2}/\d{1,2}"
325 };
326 if !re.is_match(input) {
327 return None;
328 }
329 if self.prefer_dmy {
330 self.slash_dmy_hms(input)
331 .or_else(|| self.slash_dmy(input))
332 .or_else(|| self.slash_mdy_hms(input))
333 .or_else(|| self.slash_mdy(input))
334 } else {
335 self.slash_mdy_hms(input)
336 .or_else(|| self.slash_mdy(input))
337 .or_else(|| self.slash_dmy_hms(input))
338 .or_else(|| self.slash_dmy(input))
339 }
340 }
341
342 #[inline]
343 fn slash_ymd_family(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
344 let re: &Regex = regex! {r"^[0-9]{4}/[0-9]{1,2}"};
345 if !re.is_match(input) {
346 return None;
347 }
348 self.slash_ymd_hms(input).or_else(|| self.slash_ymd(input))
349 }
350
351 #[inline]
356 fn unix_timestamp(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
357 let &b0 = input.as_bytes().first()?;
364 if !(b0.is_ascii_digit() || matches!(b0, b'+' | b'-' | b'.')) {
365 return None;
366 }
367
368 let ts_sec_val: f64 = if let Ok(val) = fast_float2::parse(input) {
369 val
370 } else {
371 return None;
372 };
373
374 if !ts_sec_val.is_finite() {
379 return None;
380 }
381
382 let ts_ns_val = ts_sec_val * 1_000_000_000_f64;
384
385 let result = Utc.timestamp_nanos(ts_ns_val as i64).with_timezone(&Utc);
386 Some(Ok(result))
387 }
388
389 #[inline]
393 fn rfc3339(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
394 DateTime::parse_from_rfc3339(input)
395 .ok()
396 .map(|parsed| parsed.with_timezone(&Utc))
397 .map(Ok)
398 }
399
400 #[inline]
403 fn rfc2822(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
404 if !input.as_bytes().contains(&b':') {
409 return None;
410 }
411 DateTime::parse_from_rfc2822(input)
412 .ok()
413 .map(|parsed| parsed.with_timezone(&Utc))
414 .map(Ok)
415 }
416
417 #[inline]
430 fn ymd_hms(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
431 let re: &Regex = regex! {
432 r"^\d{4}-\d{2}-\d{2}[T\s]+\d{2}:\d{2}(?::\d{2})?(?:\.\d{1,9})?\s*(?:am|pm|AM|PM)?$"
433
434 };
435 if !re.is_match(input) {
436 return None;
437 }
438
439 let items = match (input.as_bytes()[10] == b'T', time_shape(input)) {
443 (true, TimeShape::Hms) => fmt_items!("%Y-%m-%dT%H:%M:%S"),
444 (true, TimeShape::Hm) => fmt_items!("%Y-%m-%dT%H:%M"),
445 (true, TimeShape::HmsF) => fmt_items!("%Y-%m-%dT%H:%M:%S%.f"),
446 (true, TimeShape::ImsP | TimeShape::HmsFP) => {
447 fmt_items!("%Y-%m-%dT%I:%M:%S%.f %P")
448 }
449 (true, TimeShape::ImP) => fmt_items!("%Y-%m-%dT%I:%M %P"),
450 (false, TimeShape::Hms) => fmt_items!("%Y-%m-%d %H:%M:%S"),
451 (false, TimeShape::Hm) => fmt_items!("%Y-%m-%d %H:%M"),
452 (false, TimeShape::HmsF) => fmt_items!("%Y-%m-%d %H:%M:%S%.f"),
453 (false, TimeShape::ImsP | TimeShape::HmsFP) => {
454 fmt_items!("%Y-%m-%d %I:%M:%S%.f %P")
455 }
456 (false, TimeShape::ImP) => fmt_items!("%Y-%m-%d %I:%M %P"),
457 };
458
459 self.dt_from_items(input, items)
460 .ok()
461 .map(|parsed| parsed.with_timezone(&Utc))
462 .map(Ok)
463 }
464
465 #[inline]
475 fn ymd_hms_z(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
476 if input.len() < 17 || !input.as_bytes()[10].is_ascii_whitespace() {
478 return None;
479 }
480 let re: &Regex = regex! {
481 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})$"
482 };
483
484 if let Some(caps) = re.captures(input)
485 && let Some(matched_tz) = caps.name("tz")
486 {
487 let parse_from_str = Self::naive_dt_from_items;
488 return match timezone::parse(matched_tz.as_str().trim()) {
489 Ok(offset) => parse_from_str(input, fmt_items!("%Y-%m-%d %H:%M:%S %Z"))
490 .or_else(|_| parse_from_str(input, fmt_items!("%Y-%m-%d %H:%M %Z")))
491 .or_else(|_| parse_from_str(input, fmt_items!("%Y-%m-%d %H:%M:%S%.f %Z")))
492 .ok()
493 .and_then(|parsed| offset.from_local_datetime(&parsed).single())
494 .map(|datetime| datetime.with_timezone(&Utc))
495 .map(Ok),
496 Err(err) => Some(Err(err)),
497 };
498 }
499 None
500 }
501
502 #[inline]
505 fn ymd(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
506 let re: &Regex = regex! {r"^\d{4}-\d{2}-\d{2}$"
507 };
508
509 if !re.is_match(input) {
510 return None;
511 }
512 let now = Utc::now()
513 .date()
514 .and_time(self.default_time)?
515 .with_timezone(self.tz);
516 Self::naive_date_from_items(input, fmt_items!("%Y-%m-%d"))
517 .ok()
518 .map(|parsed| parsed.and_time(now.time()))
519 .and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
520 .map(|at_tz| at_tz.with_timezone(&Utc))
521 .map(Ok)
522 }
523
524 #[inline]
529 fn ymd_z(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
530 if input.len() <= 10 {
532 return None;
533 }
534 let re: &Regex = regex! {r"^\d{4}-\d{2}-\d{2}(?P<tz>\s*[+-:a-zA-Z0-9]{3,6})$"
535 };
536 if let Some(caps) = re.captures(input)
537 && let Some(matched_tz) = caps.name("tz")
538 {
539 return match timezone::parse(matched_tz.as_str().trim()) {
540 Ok(offset) => {
541 let now = Utc::now()
542 .date()
543 .and_time(self.default_time)?
544 .with_timezone(&offset);
545 Self::naive_date_from_items(input, fmt_items!("%Y-%m-%d %Z"))
546 .ok()
547 .map(|parsed| parsed.and_time(now.time()))
548 .and_then(|datetime| offset.from_local_datetime(&datetime).single())
549 .map(|at_tz| at_tz.with_timezone(&Utc))
550 .map(Ok)
551 }
552 Err(err) => Some(Err(err)),
553 };
554 }
555 None
556 }
557
558 #[inline]
561 fn month_ymd(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
562 let re: &Regex = regex! {r"^\d{4}-\w{3,9}-\d{2}$"
563 };
564 if !re.is_match(input) {
565 return None;
566 }
567
568 let now = Utc::now()
569 .date()
570 .and_time(self.default_time)?
571 .with_timezone(self.tz);
572 Self::naive_date_from_items(input, fmt_items!("%Y-%m-%d"))
573 .or_else(|_| Self::naive_date_from_items(input, fmt_items!("%Y-%b-%d")))
574 .ok()
575 .map(|parsed| parsed.and_time(now.time()))
576 .and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
577 .map(|at_tz| at_tz.with_timezone(&Utc))
578 .map(Ok)
579 }
580
581 #[inline]
586 fn month_mdy_hms(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
587 let re: &Regex = regex! {
588 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)?$"
589 };
590 if !re.is_match(input) {
591 return None;
592 }
593
594 let dt = input.replace([',', '.'], "");
599 let items = match time_shape(&dt) {
605 TimeShape::Hms => fmt_items!("%B %d %Y %H:%M:%S"),
606 TimeShape::Hm => fmt_items!("%B %d %Y %H:%M"),
607 TimeShape::ImsP => fmt_items!("%B %d %Y %I:%M:%S %P"),
608 TimeShape::ImP => fmt_items!("%B %d %Y %I:%M %P"),
609 TimeShape::HmsF | TimeShape::HmsFP => return None,
610 };
611 self.dt_from_items(&dt, items)
612 .ok()
613 .map(|at_tz| at_tz.with_timezone(&Utc))
614 .map(Ok)
615 }
616
617 #[inline]
623 fn month_mdy_hms_z(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
624 if input.len() < 20 {
627 return None;
628 }
629 let bytes = input.as_bytes();
630 let has_year = (0..bytes.len().saturating_sub(3)).any(|i| {
631 bytes[i..i + 4].iter().all(|b| b.is_ascii_digit())
632 && (i == 0 || !bytes[i - 1].is_ascii_digit())
633 && bytes.get(i + 4).is_none_or(|b| !b.is_ascii_digit())
634 });
635 if !has_year {
636 return None;
637 }
638 let re: &Regex = regex! {
639 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})$",
640 };
641 if let Some(caps) = re.captures(input)
642 && let Some(matched_tz) = caps.name("tz")
643 {
644 let parse_from_str = Self::naive_dt_from_items;
645 return match timezone::parse(matched_tz.as_str().trim()) {
646 Ok(offset) => {
647 let mut dt = input.replace(',', "");
648 if let Some(pos) = dt.find("at") {
649 dt.replace_range(pos..pos + 2, "");
650 }
651 parse_from_str(&dt, fmt_items!("%B %d %Y %H:%M:%S %Z"))
652 .or_else(|_| parse_from_str(&dt, fmt_items!("%B %d %Y %H:%M %Z")))
653 .or_else(|_| parse_from_str(&dt, fmt_items!("%B %d %Y %I:%M:%S %P %Z")))
654 .or_else(|_| parse_from_str(&dt, fmt_items!("%B %d %Y %I:%M %P %Z")))
655 .ok()
656 .and_then(|parsed| offset.from_local_datetime(&parsed).single())
657 .map(|datetime| datetime.with_timezone(&Utc))
658 .map(Ok)
659 }
660 Err(err) => Some(Err(err)),
661 };
662 }
663 None
664 }
665
666 #[inline]
674 fn month_mdy(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
675 let re: &Regex = regex! {r"^[a-zA-Z]{3,9}\.?\s+\d{1,2},\s+\d{2,4}$"
676 };
677 if !re.is_match(input) {
678 return None;
679 }
680
681 let now = Utc::now()
682 .date()
683 .and_time(self.default_time)?
684 .with_timezone(self.tz);
685 let dt = input.replace([',', '.'], "");
689 Self::naive_date_from_items(&dt, fmt_items!("%B %d %y"))
690 .or_else(|_| Self::naive_date_from_items(&dt, fmt_items!("%B %d %Y")))
691 .ok()
692 .map(|parsed| parsed.and_time(now.time()))
693 .and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
694 .map(|at_tz| at_tz.with_timezone(&Utc))
695 .map(Ok)
696 }
697
698 #[inline]
703 fn month_dmy_hms(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
704 if !input.as_bytes().contains(&b':') {
706 return None;
707 }
708 let re: &Regex = regex! {
709 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})?$"
710 };
711 if !re.is_match(input) {
712 return None;
713 }
714
715 let dt = input.replace(',', "");
716 let items = match time_shape(&dt) {
721 TimeShape::Hms => fmt_items!("%d %B %Y %H:%M:%S"),
722 TimeShape::Hm => fmt_items!("%d %B %Y %H:%M"),
723 TimeShape::HmsF => fmt_items!("%d %B %Y %H:%M:%S%.f"),
724 TimeShape::ImP | TimeShape::ImsP | TimeShape::HmsFP => return None,
725 };
726 self.dt_from_items(&dt, items)
727 .ok()
728 .map(|at_tz| at_tz.with_timezone(&Utc))
729 .map(Ok)
730 }
731
732 #[inline]
738 fn month_dmy(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
739 let re: &Regex = regex! {r"^\d{1,2}\s+[a-zA-Z]{3,9}\s+\d{2,4}$"
740 };
741 if !re.is_match(input) {
742 return None;
743 }
744
745 let now = Utc::now()
746 .date()
747 .and_time(self.default_time)?
748 .with_timezone(self.tz);
749 let bytes = input.as_bytes();
752 let len = bytes.len();
753 let four_digit_year = len >= 5
754 && bytes[len - 4..].iter().all(|b| b.is_ascii_digit())
755 && bytes[len - 5].is_ascii_whitespace();
756 let parsed = if four_digit_year {
757 Self::naive_date_from_items(input, fmt_items!("%d %B %Y"))
758 } else {
759 Self::naive_date_from_items(input, fmt_items!("%d %B %y"))
760 .or_else(|_| Self::naive_date_from_items(input, fmt_items!("%d %B %Y")))
761 };
762 parsed
763 .ok()
764 .map(|parsed| parsed.and_time(now.time()))
765 .and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
766 .map(|at_tz| at_tz.with_timezone(&Utc))
767 .map(Ok)
768 }
769
770 #[inline]
785 fn slash_mdy_hms(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
786 let re: &Regex = regex! {
787 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)?$"
788 };
789 if !re.is_match(input) {
790 return None;
791 }
792
793 let items = match (
798 slash_year_is_two_digits(input.as_bytes()),
799 time_shape(input),
800 ) {
801 (true, TimeShape::Hms) => fmt_items!("%m/%d/%y %H:%M:%S"),
802 (true, TimeShape::Hm) => fmt_items!("%m/%d/%y %H:%M"),
803 (true, TimeShape::HmsF) => fmt_items!("%m/%d/%y %H:%M:%S%.f"),
804 (true, TimeShape::ImsP | TimeShape::HmsFP) => {
805 fmt_items!("%m/%d/%y %I:%M:%S%.f %P")
806 }
807 (true, TimeShape::ImP) => fmt_items!("%m/%d/%y %I:%M %P"),
808 (false, TimeShape::Hms) => fmt_items!("%m/%d/%Y %H:%M:%S"),
809 (false, TimeShape::Hm) => fmt_items!("%m/%d/%Y %H:%M"),
810 (false, TimeShape::HmsF) => fmt_items!("%m/%d/%Y %H:%M:%S%.f"),
811 (false, TimeShape::ImsP | TimeShape::HmsFP) => {
812 fmt_items!("%m/%d/%Y %I:%M:%S%.f %P")
813 }
814 (false, TimeShape::ImP) => fmt_items!("%m/%d/%Y %I:%M %P"),
815 };
816 self.dt_from_items(input, items)
817 .ok()
818 .map(|at_tz| at_tz.with_timezone(&Utc))
819 .map(Ok)
820 }
821
822 #[inline]
837 fn slash_dmy_hms(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
838 let re: &Regex = regex! {
839 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)?$"
840 };
841 if !re.is_match(input) {
842 return None;
843 }
844
845 let items = match (
848 slash_year_is_two_digits(input.as_bytes()),
849 time_shape(input),
850 ) {
851 (true, TimeShape::Hms) => fmt_items!("%d/%m/%y %H:%M:%S"),
852 (true, TimeShape::Hm) => fmt_items!("%d/%m/%y %H:%M"),
853 (true, TimeShape::HmsF) => fmt_items!("%d/%m/%y %H:%M:%S%.f"),
854 (true, TimeShape::ImsP | TimeShape::HmsFP) => {
855 fmt_items!("%d/%m/%y %I:%M:%S%.f %P")
856 }
857 (true, TimeShape::ImP) => fmt_items!("%d/%m/%y %I:%M %P"),
858 (false, TimeShape::Hms) => fmt_items!("%d/%m/%Y %H:%M:%S"),
859 (false, TimeShape::Hm) => fmt_items!("%d/%m/%Y %H:%M"),
860 (false, TimeShape::HmsF) => fmt_items!("%d/%m/%Y %H:%M:%S%.f"),
861 (false, TimeShape::ImsP | TimeShape::HmsFP) => {
862 fmt_items!("%d/%m/%Y %I:%M:%S%.f %P")
863 }
864 (false, TimeShape::ImP) => fmt_items!("%d/%m/%Y %I:%M %P"),
865 };
866 self.dt_from_items(input, items)
867 .ok()
868 .map(|at_tz| at_tz.with_timezone(&Utc))
869 .map(Ok)
870 }
871
872 #[inline]
878 fn slash_mdy(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
879 let re: &Regex = regex! {r"^\d{1,2}/\d{1,2}/\d{2,4}$"
880 };
881 if !re.is_match(input) {
882 return None;
883 }
884
885 let now = Utc::now()
886 .date()
887 .and_time(self.default_time)?
888 .with_timezone(self.tz);
889 let fmt = if slash_year_is_two_digits(input.as_bytes()) {
890 fmt_items!("%m/%d/%y")
891 } else {
892 fmt_items!("%m/%d/%Y")
893 };
894 Self::naive_date_from_items(input, fmt)
895 .ok()
896 .map(|parsed| parsed.and_time(now.time()))
897 .and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
898 .map(|at_tz| at_tz.with_timezone(&Utc))
899 .map(Ok)
900 }
901
902 #[inline]
908 fn slash_dmy(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
909 let re: &Regex = regex! {r"^[0-9]{1,2}/[0-9]{1,2}/[0-9]{2,4}$"
910 };
911 if !re.is_match(input) {
912 return None;
913 }
914
915 let now = Utc::now()
916 .date()
917 .and_time(self.default_time)?
918 .with_timezone(self.tz);
919 let fmt = if slash_year_is_two_digits(input.as_bytes()) {
920 fmt_items!("%d/%m/%y")
921 } else {
922 fmt_items!("%d/%m/%Y")
923 };
924 Self::naive_date_from_items(input, fmt)
925 .ok()
926 .map(|parsed| parsed.and_time(now.time()))
927 .and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
928 .map(|at_tz| at_tz.with_timezone(&Utc))
929 .map(Ok)
930 }
931
932 #[inline]
941 fn slash_ymd_hms(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
942 let re: &Regex = regex! {
943 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)?$"
944 };
945 if !re.is_match(input) {
946 return None;
947 }
948
949 let items = match time_shape(input) {
950 TimeShape::Hms => fmt_items!("%Y/%m/%d %H:%M:%S"),
951 TimeShape::Hm => fmt_items!("%Y/%m/%d %H:%M"),
952 TimeShape::HmsF => fmt_items!("%Y/%m/%d %H:%M:%S%.f"),
953 TimeShape::ImsP | TimeShape::HmsFP => fmt_items!("%Y/%m/%d %I:%M:%S%.f %P"),
954 TimeShape::ImP => fmt_items!("%Y/%m/%d %I:%M %P"),
955 };
956 self.dt_from_items(input, items)
957 .ok()
958 .map(|at_tz| at_tz.with_timezone(&Utc))
959 .map(Ok)
960 }
961
962 #[inline]
966 fn slash_ymd(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
967 let re: &Regex = regex! {r"^[0-9]{4}/[0-9]{1,2}/[0-9]{1,2}$"
968 };
969 if !re.is_match(input) {
970 return None;
971 }
972
973 let now = Utc::now()
974 .date()
975 .and_time(self.default_time)?
976 .with_timezone(self.tz);
977 Self::naive_date_from_items(input, fmt_items!("%Y/%m/%d"))
978 .ok()
979 .map(|parsed| parsed.and_time(now.time()))
980 .and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
981 .map(|at_tz| at_tz.with_timezone(&Utc))
982 .map(Ok)
983 }
984}
985
986#[cfg(test)]
987mod tests {
988 use super::*;
989
990 #[test]
991 fn unix_timestamp() {
992 let parse = Parse::new(&Utc, Utc::now().time());
993
994 let test_cases = vec![
995 ("0", Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)),
996 ("0000000000", Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)),
997 ("0000000000000", Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)),
998 ("0000000000000000000", Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)),
999 ("-770172300", Utc.ymd(1945, 8, 5).and_hms(23, 15, 0)),
1000 (
1001 "1671673426.123456789",
1002 Utc.ymd(2022, 12, 22).and_hms_nano(1, 43, 46, 123456768),
1003 ),
1004 ("1511648546", Utc.ymd(2017, 11, 25).and_hms(22, 22, 26)),
1005 (
1006 "1620036248.420",
1007 Utc.ymd(2021, 5, 3).and_hms_milli(10, 4, 8, 420),
1008 ),
1009 (
1010 "1620036248.717915136",
1011 Utc.ymd(2021, 5, 3).and_hms_nano(10, 4, 8, 717915136),
1012 ),
1013 ];
1014
1015 for &(input, want) in test_cases.iter() {
1016 assert_eq!(
1017 parse.unix_timestamp(input).unwrap().unwrap(),
1018 want,
1019 "unix_timestamp/{}",
1020 input
1021 )
1022 }
1023 assert!(parse.unix_timestamp("15116").is_some());
1024 assert!(
1025 parse
1026 .unix_timestamp("16200248727179150001620024872717915000") .is_some()
1028 );
1029 assert!(parse.unix_timestamp("not-a-ts").is_none());
1030 for input in [
1033 "inf", "nan", "INF", "NaN", "infinity", "+inf", "-inf", "-nan",
1034 ] {
1035 assert!(
1036 parse.unix_timestamp(input).is_none(),
1037 "unix_timestamp must reject non-finite {input}"
1038 );
1039 }
1040 }
1041
1042 #[test]
1043 fn rfc3339() {
1044 let parse = Parse::new(&Utc, Utc::now().time());
1045
1046 let test_cases = [
1047 (
1048 "2021-05-01T01:17:02.604456Z",
1049 Utc.ymd(2021, 5, 1).and_hms_nano(1, 17, 2, 604456000),
1050 ),
1051 (
1052 "2017-11-25T22:34:50Z",
1053 Utc.ymd(2017, 11, 25).and_hms(22, 34, 50),
1054 ),
1055 ];
1056
1057 for &(input, want) in test_cases.iter() {
1058 assert_eq!(
1059 parse.rfc3339(input).unwrap().unwrap(),
1060 want,
1061 "rfc3339/{}",
1062 input
1063 )
1064 }
1065 assert!(parse.rfc3339("2017-11-25 22:34:50").is_none());
1066 assert!(parse.rfc3339("not-date-time").is_none());
1067 }
1068
1069 #[test]
1070 fn rfc2822() {
1071 let parse = Parse::new(&Utc, Utc::now().time());
1072
1073 let test_cases = [
1074 (
1075 "Wed, 02 Jun 2021 06:31:39 GMT",
1076 Utc.ymd(2021, 6, 2).and_hms(6, 31, 39),
1077 ),
1078 (
1079 "Wed, 02 Jun 2021 06:31:39 PDT",
1080 Utc.ymd(2021, 6, 2).and_hms(13, 31, 39),
1081 ),
1082 ];
1083
1084 for &(input, want) in test_cases.iter() {
1085 assert_eq!(
1086 parse.rfc2822(input).unwrap().unwrap(),
1087 want,
1088 "rfc2822/{}",
1089 input
1090 )
1091 }
1092 assert!(parse.rfc2822("02 Jun 2021 06:31:39").is_none());
1093 assert!(parse.rfc2822("not-date-time").is_none());
1094 }
1095
1096 #[test]
1097 fn ymd_hms() {
1098 let parse = Parse::new(&Utc, Utc::now().time());
1099
1100 let test_cases = [
1101 ("2021-04-30 21:14", Utc.ymd(2021, 4, 30).and_hms(21, 14, 0)),
1102 (
1103 "2021-04-30 21:14:10",
1104 Utc.ymd(2021, 4, 30).and_hms(21, 14, 10),
1105 ),
1106 (
1107 "2021-04-30 21:14:10.052282",
1108 Utc.ymd(2021, 4, 30).and_hms_micro(21, 14, 10, 52282),
1109 ),
1110 (
1111 "2014-04-26 05:24:37 PM",
1112 Utc.ymd(2014, 4, 26).and_hms(17, 24, 37),
1113 ),
1114 (
1115 "2014-04-26 17:24:37.123",
1116 Utc.ymd(2014, 4, 26).and_hms_milli(17, 24, 37, 123),
1117 ),
1118 (
1119 "2014-04-26 17:24:37.3186369",
1120 Utc.ymd(2014, 4, 26).and_hms_nano(17, 24, 37, 318636900),
1121 ),
1122 (
1123 "2012-08-03 18:31:59.257000000",
1124 Utc.ymd(2012, 8, 3).and_hms_nano(18, 31, 59, 257000000),
1125 ),
1126 ("2020-01-15T08:00", Utc.ymd(2020, 1, 15).and_hms(8, 0, 0)),
1129 ("2020-01-15T08:00:00", Utc.ymd(2020, 1, 15).and_hms(8, 0, 0)),
1130 (
1131 "2020-01-15T08:00:00.123",
1132 Utc.ymd(2020, 1, 15).and_hms_milli(8, 0, 0, 123),
1133 ),
1134 (
1135 "2020-01-15T08:00:00.123456",
1136 Utc.ymd(2020, 1, 15).and_hms_micro(8, 0, 0, 123456),
1137 ),
1138 (
1139 "2020-01-15T08:00:00.123456789",
1140 Utc.ymd(2020, 1, 15).and_hms_nano(8, 0, 0, 123456789),
1141 ),
1142 ];
1143
1144 for &(input, want) in test_cases.iter() {
1145 assert_eq!(
1146 parse.ymd_hms(input).unwrap().unwrap(),
1147 want,
1148 "ymd_hms/{}",
1149 input
1150 )
1151 }
1152 assert!(parse.ymd_hms("not-date-time").is_none());
1153
1154 let t_form = parse.ymd_hms("2020-01-15T08:00:00").unwrap().unwrap();
1156 let space_form = parse.ymd_hms("2020-01-15 08:00:00").unwrap().unwrap();
1157 assert_eq!(t_form, space_form, "T-separator vs space disagree");
1158 }
1159
1160 #[test]
1161 fn ymd_hms_z() {
1162 let parse = Parse::new(&Utc, Utc::now().time());
1163
1164 let test_cases = [
1165 (
1166 "2017-11-25 13:31:15 PST",
1167 Utc.ymd(2017, 11, 25).and_hms(21, 31, 15),
1168 ),
1169 (
1170 "2017-11-25 13:31 PST",
1171 Utc.ymd(2017, 11, 25).and_hms(21, 31, 0),
1172 ),
1173 (
1174 "2014-12-16 06:20:00 UTC",
1175 Utc.ymd(2014, 12, 16).and_hms(6, 20, 0),
1176 ),
1177 (
1178 "2014-12-16 06:20:00 GMT",
1179 Utc.ymd(2014, 12, 16).and_hms(6, 20, 0),
1180 ),
1181 (
1182 "2014-04-26 13:13:43 +0800",
1183 Utc.ymd(2014, 4, 26).and_hms(5, 13, 43),
1184 ),
1185 (
1186 "2014-04-26 13:13:44 +09:00",
1187 Utc.ymd(2014, 4, 26).and_hms(4, 13, 44),
1188 ),
1189 (
1190 "2012-08-03 18:31:59.257000000 +0000",
1191 Utc.ymd(2012, 8, 3).and_hms_nano(18, 31, 59, 257000000),
1192 ),
1193 (
1194 "2015-09-30 18:48:56.35272715 UTC",
1195 Utc.ymd(2015, 9, 30).and_hms_nano(18, 48, 56, 352727150),
1196 ),
1197 ];
1198
1199 for &(input, want) in test_cases.iter() {
1200 assert_eq!(
1201 parse.ymd_hms_z(input).unwrap().unwrap(),
1202 want,
1203 "ymd_hms_z/{}",
1204 input
1205 )
1206 }
1207 assert!(parse.ymd_hms_z("not-date-time").is_none());
1208 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()); }
1215
1216 #[test]
1217 fn ymd() {
1218 let parse = Parse::new(&Utc, Utc::now().time());
1219
1220 let test_cases = [(
1221 "2021-02-21",
1222 Utc.ymd(2021, 2, 21).and_time(Utc::now().time()),
1223 )];
1224
1225 for &(input, want) in test_cases.iter() {
1226 assert_eq!(
1227 parse
1228 .ymd(input)
1229 .unwrap()
1230 .unwrap()
1231 .trunc_subsecs(0)
1232 .with_second(0)
1233 .unwrap(),
1234 want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
1235 "ymd/{}",
1236 input
1237 )
1238 }
1239 assert!(parse.ymd("not-date-time").is_none());
1240 }
1241
1242 #[test]
1243 fn ymd_z() {
1244 let parse = Parse::new(&Utc, Utc::now().time());
1245 let now_at_pst = Utc::now().with_timezone(&FixedOffset::west(8 * 3600));
1246 let now_at_cst = Utc::now().with_timezone(&FixedOffset::east(8 * 3600));
1247
1248 let test_cases = [
1249 (
1250 "2021-02-21 PST",
1251 FixedOffset::west(8 * 3600)
1252 .ymd(2021, 2, 21)
1253 .and_time(now_at_pst.time())
1254 .map(|dt| dt.with_timezone(&Utc)),
1255 ),
1256 (
1257 "2021-02-21 UTC",
1258 FixedOffset::west(0)
1259 .ymd(2021, 2, 21)
1260 .and_time(Utc::now().time())
1261 .map(|dt| dt.with_timezone(&Utc)),
1262 ),
1263 (
1264 "2020-07-20+08:00",
1265 FixedOffset::east(8 * 3600)
1266 .ymd(2020, 7, 20)
1267 .and_time(now_at_cst.time())
1268 .map(|dt| dt.with_timezone(&Utc)),
1269 ),
1270 ];
1271
1272 for &(input, want) in test_cases.iter() {
1273 assert_eq!(
1274 parse
1275 .ymd_z(input)
1276 .unwrap()
1277 .unwrap()
1278 .trunc_subsecs(0)
1279 .with_second(0)
1280 .unwrap(),
1281 want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
1282 "ymd_z/{}",
1283 input
1284 )
1285 }
1286 assert!(parse.ymd_z("not-date-time").is_none());
1287 assert!(parse.ymd_z("2021-02-21").is_none()); assert!(parse.ymd_z("2021-02-21X").is_none()); }
1291
1292 #[test]
1293 fn month_ymd() {
1294 let parse = Parse::new(&Utc, Utc::now().time());
1295
1296 let test_cases = [(
1297 "2021-Feb-21",
1298 Utc.ymd(2021, 2, 21).and_time(Utc::now().time()),
1299 )];
1300
1301 for &(input, want) in test_cases.iter() {
1302 assert_eq!(
1303 parse
1304 .month_ymd(input)
1305 .unwrap()
1306 .unwrap()
1307 .trunc_subsecs(0)
1308 .with_second(0)
1309 .unwrap(),
1310 want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
1311 "month_ymd/{}",
1312 input
1313 )
1314 }
1315 assert!(parse.month_ymd("not-date-time").is_none());
1316 }
1317
1318 #[test]
1319 fn month_mdy_hms() {
1320 let parse = Parse::new(&Utc, Utc::now().time());
1321
1322 let test_cases = [
1323 (
1324 "May 8, 2009 5:57:51 PM",
1325 Utc.ymd(2009, 5, 8).and_hms(17, 57, 51),
1326 ),
1327 (
1328 "September 17, 2012 10:09am",
1329 Utc.ymd(2012, 9, 17).and_hms(10, 9, 0),
1330 ),
1331 (
1332 "September 17, 2012, 10:10:09",
1333 Utc.ymd(2012, 9, 17).and_hms(10, 10, 9),
1334 ),
1335 ];
1336
1337 for &(input, want) in test_cases.iter() {
1338 assert_eq!(
1339 parse.month_mdy_hms(input).unwrap().unwrap(),
1340 want,
1341 "month_mdy_hms/{}",
1342 input
1343 )
1344 }
1345 assert!(parse.month_mdy_hms("not-date-time").is_none());
1346 }
1347
1348 #[test]
1349 fn month_mdy_hms_z() {
1350 let parse = Parse::new(&Utc, Utc::now().time());
1351
1352 let test_cases = [
1353 (
1354 "May 02, 2021 15:51:31 UTC",
1355 Utc.ymd(2021, 5, 2).and_hms(15, 51, 31),
1356 ),
1357 (
1358 "May 02, 2021 15:51 UTC",
1359 Utc.ymd(2021, 5, 2).and_hms(15, 51, 0),
1360 ),
1361 (
1362 "May 26, 2021, 12:49 AM PDT",
1363 Utc.ymd(2021, 5, 26).and_hms(7, 49, 0),
1364 ),
1365 (
1366 "September 17, 2012 at 10:09am PST",
1367 Utc.ymd(2012, 9, 17).and_hms(18, 9, 0),
1368 ),
1369 ];
1370
1371 for &(input, want) in test_cases.iter() {
1372 assert_eq!(
1373 parse.month_mdy_hms_z(input).unwrap().unwrap(),
1374 want,
1375 "month_mdy_hms_z/{}",
1376 input
1377 )
1378 }
1379 assert!(parse.month_mdy_hms_z("not-date-time").is_none());
1380 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()); }
1385
1386 #[test]
1387 fn month_mdy() {
1388 let parse = Parse::new(&Utc, Utc::now().time());
1389
1390 let test_cases = [
1391 (
1392 "May 25, 2021",
1393 Utc.ymd(2021, 5, 25).and_time(Utc::now().time()),
1394 ),
1395 (
1396 "oct 7, 1970",
1397 Utc.ymd(1970, 10, 7).and_time(Utc::now().time()),
1398 ),
1399 (
1400 "oct 7, 70",
1401 Utc.ymd(1970, 10, 7).and_time(Utc::now().time()),
1402 ),
1403 (
1404 "oct. 7, 1970",
1405 Utc.ymd(1970, 10, 7).and_time(Utc::now().time()),
1406 ),
1407 (
1408 "oct. 7, 70",
1409 Utc.ymd(1970, 10, 7).and_time(Utc::now().time()),
1410 ),
1411 (
1412 "October 7, 1970",
1413 Utc.ymd(1970, 10, 7).and_time(Utc::now().time()),
1414 ),
1415 ];
1416
1417 for &(input, want) in test_cases.iter() {
1418 assert_eq!(
1419 parse
1420 .month_mdy(input)
1421 .unwrap()
1422 .unwrap()
1423 .trunc_subsecs(0)
1424 .with_second(0)
1425 .unwrap(),
1426 want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
1427 "month_mdy/{}",
1428 input
1429 )
1430 }
1431 assert!(parse.month_mdy("not-date-time").is_none());
1432 }
1433
1434 #[test]
1435 fn month_dmy_hms() {
1436 let parse = Parse::new(&Utc, Utc::now().time());
1437
1438 let test_cases = [
1439 (
1440 "12 Feb 2006, 19:17",
1441 Utc.ymd(2006, 2, 12).and_hms(19, 17, 0),
1442 ),
1443 ("12 Feb 2006 19:17", Utc.ymd(2006, 2, 12).and_hms(19, 17, 0)),
1444 (
1445 "14 May 2019 19:11:40.164",
1446 Utc.ymd(2019, 5, 14).and_hms_milli(19, 11, 40, 164),
1447 ),
1448 ];
1449
1450 for &(input, want) in test_cases.iter() {
1451 assert_eq!(
1452 parse.month_dmy_hms(input).unwrap().unwrap(),
1453 want,
1454 "month_dmy_hms/{}",
1455 input
1456 )
1457 }
1458 assert!(parse.month_dmy_hms("not-date-time").is_none());
1459 }
1460
1461 #[test]
1462 fn month_dmy() {
1463 let parse = Parse::new(&Utc, Utc::now().time());
1464
1465 let test_cases = [
1466 ("7 oct 70", Utc.ymd(1970, 10, 7).and_time(Utc::now().time())),
1467 (
1468 "7 oct 1970",
1469 Utc.ymd(1970, 10, 7).and_time(Utc::now().time()),
1470 ),
1471 (
1472 "03 February 2013",
1473 Utc.ymd(2013, 2, 3).and_time(Utc::now().time()),
1474 ),
1475 (
1476 "1 July 2013",
1477 Utc.ymd(2013, 7, 1).and_time(Utc::now().time()),
1478 ),
1479 ];
1480
1481 for &(input, want) in test_cases.iter() {
1482 assert_eq!(
1483 parse
1484 .month_dmy(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 "month_dmy/{}",
1492 input
1493 )
1494 }
1495 assert!(parse.month_dmy("not-date-time").is_none());
1496 }
1497
1498 #[test]
1501 fn month_dmy_year_fast_path() {
1502 let parse = Parse::new(&Utc, Utc::now().time());
1503
1504 let four_digit = parse.month_dmy("14 May 2019").unwrap().unwrap();
1506 assert_eq!(four_digit.year(), 2019);
1507 assert_eq!(four_digit.month(), 5);
1508 assert_eq!(four_digit.day(), 14);
1509
1510 let two_digit = parse.month_dmy("14 May 19").unwrap().unwrap();
1513 assert_eq!(two_digit.year(), 2019);
1514 assert_eq!(two_digit.month(), 5);
1515 assert_eq!(two_digit.day(), 14);
1516 }
1517
1518 #[test]
1519 fn slash_mdy_hms() {
1520 let parse = Parse::new(&Utc, Utc::now().time());
1521
1522 let test_cases = vec![
1523 ("4/8/2014 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)),
1524 ("04/08/2014 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)),
1525 ("4/8/14 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)),
1526 ("04/2/2014 03:00:51", Utc.ymd(2014, 4, 2).and_hms(3, 0, 51)),
1527 ("8/8/1965 12:00:00 AM", Utc.ymd(1965, 8, 8).and_hms(0, 0, 0)),
1528 (
1529 "8/8/1965 01:00:01 PM",
1530 Utc.ymd(1965, 8, 8).and_hms(13, 0, 1),
1531 ),
1532 ("8/8/1965 01:00 PM", Utc.ymd(1965, 8, 8).and_hms(13, 0, 0)),
1533 ("8/8/1965 1:00 PM", Utc.ymd(1965, 8, 8).and_hms(13, 0, 0)),
1534 ("8/8/1965 12:00 AM", Utc.ymd(1965, 8, 8).and_hms(0, 0, 0)),
1535 ("4/02/2014 03:00:51", Utc.ymd(2014, 4, 2).and_hms(3, 0, 51)),
1536 (
1537 "03/19/2012 10:11:59",
1538 Utc.ymd(2012, 3, 19).and_hms(10, 11, 59),
1539 ),
1540 (
1541 "03/19/2012 10:11:59.3186369",
1542 Utc.ymd(2012, 3, 19).and_hms_nano(10, 11, 59, 318636900),
1543 ),
1544 ];
1545
1546 for &(input, want) in test_cases.iter() {
1547 assert_eq!(
1548 parse.slash_mdy_hms(input).unwrap().unwrap(),
1549 want,
1550 "slash_mdy_hms/{}",
1551 input
1552 )
1553 }
1554 assert!(parse.slash_mdy_hms("not-date-time").is_none());
1555 }
1556
1557 #[test]
1558 fn slash_mdy() {
1559 let parse = Parse::new(&Utc, Utc::now().time());
1560
1561 let test_cases = [
1562 (
1563 "3/31/2014",
1564 Utc.ymd(2014, 3, 31).and_time(Utc::now().time()),
1565 ),
1566 (
1567 "03/31/2014",
1568 Utc.ymd(2014, 3, 31).and_time(Utc::now().time()),
1569 ),
1570 ("08/21/71", Utc.ymd(1971, 8, 21).and_time(Utc::now().time())),
1571 ("8/1/71", Utc.ymd(1971, 8, 1).and_time(Utc::now().time())),
1572 ];
1573
1574 for &(input, want) in test_cases.iter() {
1575 assert_eq!(
1576 parse
1577 .slash_mdy(input)
1578 .unwrap()
1579 .unwrap()
1580 .trunc_subsecs(0)
1581 .with_second(0)
1582 .unwrap(),
1583 want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
1584 "slash_mdy/{}",
1585 input
1586 )
1587 }
1588 assert!(parse.slash_mdy("not-date-time").is_none());
1589 }
1590
1591 #[test]
1592 fn slash_dmy() {
1593 let mut parse = Parse::new(&Utc, Utc::now().time());
1594
1595 let test_cases = [
1596 (
1597 "31/3/2014",
1598 Utc.ymd(2014, 3, 31).and_time(Utc::now().time()),
1599 ),
1600 (
1601 "13/11/2014",
1602 Utc.ymd(2014, 11, 13).and_time(Utc::now().time()),
1603 ),
1604 ("21/08/71", Utc.ymd(1971, 8, 21).and_time(Utc::now().time())),
1605 ("1/8/71", Utc.ymd(1971, 8, 1).and_time(Utc::now().time())),
1606 ];
1607
1608 for &(input, want) in test_cases.iter() {
1609 assert_eq!(
1610 parse
1611 .prefer_dmy(true)
1612 .slash_dmy(input)
1613 .unwrap()
1614 .unwrap()
1615 .trunc_subsecs(0)
1616 .with_second(0)
1617 .unwrap(),
1618 want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
1619 "slash_dmy/{}",
1620 input
1621 )
1622 }
1623 assert!(parse.slash_dmy("not-date-time").is_none());
1624 }
1625
1626 #[test]
1627 fn slash_ymd_hms() {
1628 let parse = Parse::new(&Utc, Utc::now().time());
1629
1630 let test_cases = [
1631 ("2014/4/8 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)),
1632 ("2014/04/08 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)),
1633 ("2014/04/2 03:00:51", Utc.ymd(2014, 4, 2).and_hms(3, 0, 51)),
1634 ("2014/4/02 03:00:51", Utc.ymd(2014, 4, 2).and_hms(3, 0, 51)),
1635 (
1636 "2012/03/19 10:11:59",
1637 Utc.ymd(2012, 3, 19).and_hms(10, 11, 59),
1638 ),
1639 (
1640 "2012/03/19 10:11:59.3186369",
1641 Utc.ymd(2012, 3, 19).and_hms_nano(10, 11, 59, 318636900),
1642 ),
1643 ];
1644
1645 for &(input, want) in test_cases.iter() {
1646 assert_eq!(
1647 parse.slash_ymd_hms(input).unwrap().unwrap(),
1648 want,
1649 "slash_ymd_hms/{}",
1650 input
1651 )
1652 }
1653 assert!(parse.slash_ymd_hms("not-date-time").is_none());
1654 }
1655
1656 #[test]
1657 fn slash_ymd() {
1658 let parse = Parse::new(&Utc, Utc::now().time());
1659
1660 let test_cases = [
1661 (
1662 "2014/3/31",
1663 Utc.ymd(2014, 3, 31).and_time(Utc::now().time()),
1664 ),
1665 (
1666 "2014/03/31",
1667 Utc.ymd(2014, 3, 31).and_time(Utc::now().time()),
1668 ),
1669 ];
1670
1671 for &(input, want) in test_cases.iter() {
1672 assert_eq!(
1673 parse
1674 .slash_ymd(input)
1675 .unwrap()
1676 .unwrap()
1677 .trunc_subsecs(0)
1678 .with_second(0)
1679 .unwrap(),
1680 want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
1681 "slash_ymd/{}",
1682 input
1683 )
1684 }
1685 assert!(parse.slash_ymd("not-date-time").is_none());
1686 }
1687
1688 #[test]
1689 fn time_shape_classification() {
1690 use TimeShape::{Hm, Hms, HmsF, HmsFP, ImP, ImsP};
1691
1692 let cases = [
1693 ("2021-04-30 21:14", Hm),
1694 ("2021-04-30T21:14", Hm),
1695 ("2021-04-30 21:14:10", Hms),
1696 ("2021-04-30 21:14:10.052282", HmsF),
1697 ("8/8/1965 12:00 AM", ImP),
1698 ("8/8/1965 12:00am", ImP),
1699 ("8/8/1965 01:00:01 PM", ImsP),
1700 ("September 17 2012 10:09am", ImP),
1701 ("03/19/2012 10:11:59.318 PM", HmsFP),
1702 ("03/19/2012 10:11.123", Hm),
1705 ];
1706 for (input, want) in cases {
1707 assert!(
1708 time_shape(input) == want,
1709 "time_shape misclassified {input}"
1710 );
1711 }
1712 }
1713
1714 #[test]
1723 fn every_fmt_items_literal_is_valid() {
1724 let src = include_str!("datetime.rs");
1725 let call = Regex::new(r#"fmt_items!\("([^"]*)"\)"#).unwrap();
1727
1728 let mut checked = 0_usize;
1729 for caps in call.captures_iter(src) {
1730 let fmt = &caps[1];
1731 assert!(
1732 chrono::format::StrftimeItems::new(fmt).parse().is_ok(),
1733 "invalid strftime literal: {fmt}"
1734 );
1735 checked += 1;
1736 }
1737 assert!(
1739 checked >= 50,
1740 "expected to check every fmt_items! literal, only found {checked}"
1741 );
1742 }
1743
1744 #[test]
1752 fn fractional_seconds_with_ampm() {
1753 let parse = Parse::new(&Utc, Utc::now().time());
1754
1755 let cases = [
1756 (
1757 "03/19/2012 10:11:59.318 PM",
1758 Utc.ymd(2012, 3, 19).and_hms_milli(22, 11, 59, 318),
1759 ),
1760 (
1761 "3/19/2012 1:11:59.318 am",
1762 Utc.ymd(2012, 3, 19).and_hms_milli(1, 11, 59, 318),
1763 ),
1764 (
1765 "03/19/12 10:11:59.318 PM",
1766 Utc.ymd(2012, 3, 19).and_hms_milli(22, 11, 59, 318),
1767 ),
1768 (
1769 "2012/03/19 10:11:59.318 PM",
1770 Utc.ymd(2012, 3, 19).and_hms_milli(22, 11, 59, 318),
1771 ),
1772 (
1773 "2012-03-19 10:11:59.318 PM",
1774 Utc.ymd(2012, 3, 19).and_hms_milli(22, 11, 59, 318),
1775 ),
1776 (
1777 "2012-03-19T10:11:59.318 PM",
1778 Utc.ymd(2012, 3, 19).and_hms_milli(22, 11, 59, 318),
1779 ),
1780 (
1781 "2012-03-19 10:11:59.3186369 PM",
1782 Utc.ymd(2012, 3, 19).and_hms_nano(22, 11, 59, 318636900),
1783 ),
1784 (
1786 "03/19/2012 10:11:59 PM",
1787 Utc.ymd(2012, 3, 19).and_hms(22, 11, 59),
1788 ),
1789 (
1790 "2012-03-19 10:11:59 PM",
1791 Utc.ymd(2012, 3, 19).and_hms(22, 11, 59),
1792 ),
1793 ];
1794
1795 for (input, want) in cases {
1796 assert_eq!(parse.parse(input).unwrap(), want, "parse/{input}");
1797 }
1798 }
1799
1800 #[test]
1809 fn unsupported_shapes_still_fail() {
1810 let parse = Parse::new(&Utc, Utc::now().time());
1811
1812 for input in [
1813 "May 8, 2009 5:57:51.123 PM",
1816 "14 May 2019 07:11:40 PM",
1819 "14 May 2019 07:11:40.164 PM",
1820 "03/19/2012 10:11.123",
1824 "2021-04-30 21:14.052282",
1825 "03/19/2012 10:11.123 PM",
1826 "03/19/2012 22:11:59.318 PM",
1829 ] {
1830 assert!(
1831 parse.parse(input).is_err(),
1832 "{input} is expected to remain unsupported"
1833 );
1834 }
1835 }
1836}