1use std::fmt;
39
40pub const ATTEMPTED_FORMATS: [&str; 4] = [
54 "RFC 2822 (e.g. `Wed, 01 Jul 2026 07:07:07 +0000`)",
55 "long form (e.g. `July 1, 2026` or `1 July 2026`)",
56 "ISO 8601 date (e.g. `2026-07-01`)",
57 "ISO 8601 datetime (e.g. `2026-07-01T07:07:07Z`)",
58];
59
60const MONTH_ABBR: [&str; 12] = [
62 "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct",
63 "Nov", "Dec",
64];
65
66const MONTH_FULL: [&str; 12] = [
68 "january",
69 "february",
70 "march",
71 "april",
72 "may",
73 "june",
74 "july",
75 "august",
76 "september",
77 "october",
78 "november",
79 "december",
80];
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
100pub enum DateFormat {
101 Rfc2822,
103 LongForm,
105 IsoDate,
107 IsoDateTime,
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
129pub struct FlexibleDate {
130 pub year: i32,
132 pub month: u8,
134 pub day: u8,
136 pub hour: u8,
138 pub minute: u8,
140 pub second: u8,
142 pub offset_minutes: i32,
144 pub format: DateFormat,
146}
147
148impl FlexibleDate {
149 #[must_use]
165 pub fn to_rfc2822(&self) -> String {
166 let month = MONTH_ABBR
167 .get(usize::from(self.month.saturating_sub(1)))
168 .copied()
169 .unwrap_or(MONTH_ABBR[0]);
170 format!(
171 "{}, {:02} {} {:04} {:02}:{:02}:{:02} {}",
172 self.weekday_abbr(),
173 self.day,
174 month,
175 self.year,
176 self.hour,
177 self.minute,
178 self.second,
179 self.offset_string("")
180 )
181 }
182
183 #[must_use]
204 pub fn to_rfc3339(&self) -> String {
205 format!(
206 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}{}",
207 self.year,
208 self.month,
209 self.day,
210 self.hour,
211 self.minute,
212 self.second,
213 self.offset_string(":")
214 )
215 }
216
217 #[must_use]
233 pub fn to_w3c_date(&self) -> String {
234 self.to_rfc3339()
235 }
236
237 #[must_use]
249 pub fn to_iso_date(&self) -> String {
250 format!("{:04}-{:02}-{:02}", self.year, self.month, self.day)
251 }
252
253 fn weekday_abbr(&self) -> &'static str {
255 const OFFSETS: [i64; 12] = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4];
256 const NAMES: [&str; 7] =
257 ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
258 let mut y = i64::from(self.year);
259 if self.month < 3 {
260 y -= 1;
261 }
262 let month_idx = usize::from(self.month.saturating_sub(1)) % 12;
263 let idx = (y + y.div_euclid(4) - y.div_euclid(100)
264 + y.div_euclid(400)
265 + OFFSETS[month_idx]
266 + i64::from(self.day))
267 .rem_euclid(7) as usize;
268 NAMES[idx]
269 }
270
271 fn offset_string(&self, sep: &str) -> String {
274 let sign = if self.offset_minutes < 0 { '-' } else { '+' };
275 let abs = self.offset_minutes.unsigned_abs();
276 format!("{sign}{:02}{sep}{:02}", abs / 60, abs % 60)
277 }
278}
279
280#[derive(Debug, Clone, PartialEq, Eq)]
297pub struct DateParseError {
298 input: String,
300}
301
302impl DateParseError {
303 fn new(input: &str) -> Self {
305 let mut owned: String = input.chars().take(64).collect();
306 if owned.len() < input.len() {
307 owned.push('…');
308 }
309 Self { input: owned }
310 }
311
312 #[must_use]
323 pub const fn attempted_formats(&self) -> &'static [&'static str] {
324 &ATTEMPTED_FORMATS
325 }
326
327 #[must_use]
338 pub fn input(&self) -> &str {
339 &self.input
340 }
341}
342
343impl fmt::Display for DateParseError {
344 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
345 write!(
346 f,
347 "could not parse date {:?}; attempted formats: {}",
348 self.input,
349 ATTEMPTED_FORMATS.join(", ")
350 )
351 }
352}
353
354impl std::error::Error for DateParseError {}
355
356#[must_use]
370pub const fn is_leap_year(year: i32) -> bool {
371 year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
372}
373
374#[must_use]
388pub const fn days_in_month(year: i32, month: u8) -> u8 {
389 match month {
390 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
391 4 | 6 | 9 | 11 => 30,
392 2 => {
393 if is_leap_year(year) {
394 29
395 } else {
396 28
397 }
398 }
399 _ => 0,
400 }
401}
402
403pub fn parse_flexible_date(
427 input: &str,
428) -> Result<FlexibleDate, DateParseError> {
429 let trimmed = input.trim();
430 if trimmed.is_empty() {
431 return Err(DateParseError::new(input));
432 }
433 parse_rfc2822(trimmed)
434 .or_else(|| parse_long_form(trimmed))
435 .or_else(|| parse_iso8601(trimmed))
436 .ok_or_else(|| DateParseError::new(input))
437}
438
439#[allow(clippy::too_many_arguments)]
441fn build_date(
442 year: i32,
443 month: u8,
444 day: u8,
445 hour: u8,
446 minute: u8,
447 second: u8,
448 offset_minutes: i32,
449 format: DateFormat,
450) -> Option<FlexibleDate> {
451 if !(1..=9999).contains(&year) {
452 return None;
453 }
454 if month == 0 || month > 12 {
455 return None;
456 }
457 if day == 0 || day > days_in_month(year, month) {
458 return None;
459 }
460 if hour > 23 || minute > 59 || second > 59 {
461 return None;
462 }
463 Some(FlexibleDate {
464 year,
465 month,
466 day,
467 hour,
468 minute,
469 second,
470 offset_minutes,
471 format,
472 })
473}
474
475fn month_from_name(name: &str) -> Option<u8> {
477 let lower = name.to_ascii_lowercase();
478 MONTH_FULL
479 .iter()
480 .position(|full| {
481 *full == lower || (lower.len() == 3 && full.starts_with(&lower))
482 })
483 .map(|idx| idx as u8 + 1)
484}
485
486fn parse_hms(token: &str) -> Option<(u8, u8, u8)> {
489 let mut parts = token.split(':');
490 let hour: u8 = parts.next().unwrap_or_default().parse().ok()?;
493 let minute: u8 = parts.next()?.parse().ok()?;
494 let second: u8 = match parts.next() {
495 Some(sec) => sec.parse().ok()?,
496 None => 0,
497 };
498 if parts.next().is_some() {
499 return None;
500 }
501 Some((hour, minute, second))
502}
503
504fn parse_zone(token: &str) -> Option<i32> {
507 match token.to_ascii_uppercase().as_str() {
508 "Z" | "UT" | "GMT" | "UTC" => return Some(0),
509 "EST" => return Some(-5 * 60),
511 "EDT" => return Some(-4 * 60),
512 "CST" => return Some(-6 * 60),
513 "CDT" => return Some(-5 * 60),
514 "MST" => return Some(-7 * 60),
515 "MDT" => return Some(-6 * 60),
516 "PST" => return Some(-8 * 60),
517 "PDT" => return Some(-7 * 60),
518 _ => {}
519 }
520 let bytes = token.as_bytes();
521 let sign = match bytes.first()? {
522 b'+' => 1,
523 b'-' => -1,
524 _ => return None,
525 };
526 let digits = &bytes[1..];
527 let (hh, mm) = match digits {
528 [h1, h2, m1, m2] => (ascii_pair(*h1, *h2)?, ascii_pair(*m1, *m2)?),
530 [h1, h2, b':', m1, m2] => {
532 (ascii_pair(*h1, *h2)?, ascii_pair(*m1, *m2)?)
533 }
534 _ => return None,
535 };
536 if hh > 23 || mm > 59 {
537 return None;
538 }
539 Some(sign * (i32::from(hh) * 60 + i32::from(mm)))
540}
541
542const fn ascii_pair(first: u8, second: u8) -> Option<u8> {
544 if first.is_ascii_digit() && second.is_ascii_digit() {
545 Some((first - b'0') * 10 + (second - b'0'))
546 } else {
547 None
548 }
549}
550
551fn ascii_number(bytes: &[u8]) -> Option<u32> {
553 if bytes.is_empty() {
554 return None;
555 }
556 bytes.iter().try_fold(0u32, |acc, b| {
557 if b.is_ascii_digit() {
558 acc.checked_mul(10)?.checked_add(u32::from(b - b'0'))
559 } else {
560 None
561 }
562 })
563}
564
565fn parse_rfc2822(input: &str) -> Option<FlexibleDate> {
571 let rest = match input.split_once(',') {
574 Some((weekday, tail))
575 if !weekday.is_empty()
576 && weekday.len() <= 9
577 && weekday.chars().all(|c| c.is_ascii_alphabetic()) =>
578 {
579 tail.trim_start()
580 }
581 _ => input,
582 };
583 let tokens: Vec<&str> = rest.split_whitespace().collect();
584 if !(4..=5).contains(&tokens.len()) {
585 return None;
586 }
587 let day: u8 = tokens[0].parse().ok()?;
588 let month = month_from_name(tokens[1])?;
589 if tokens[2].len() != 4 {
590 return None;
591 }
592 let year = ascii_number(tokens[2].as_bytes())? as i32;
593 let (hour, minute, second) = parse_hms(tokens[3])?;
594 let offset = match tokens.get(4) {
595 Some(zone) => parse_zone(zone)?,
596 None => 0,
597 };
598 build_date(
599 year,
600 month,
601 day,
602 hour,
603 minute,
604 second,
605 offset,
606 DateFormat::Rfc2822,
607 )
608}
609
610fn parse_long_form(input: &str) -> Option<FlexibleDate> {
612 let cleaned = input.replace(',', " ");
613 let tokens: Vec<&str> = cleaned.split_whitespace().collect();
614 if tokens.len() != 3 {
615 return None;
616 }
617 let (month, day_token) = month_from_name(tokens[0])
620 .map(|m| (m, tokens[1]))
621 .or_else(|| month_from_name(tokens[1]).map(|m| (m, tokens[0])))?;
622 if day_token.len() > 2 {
623 return None;
624 }
625 let day: u8 = day_token.parse().ok()?;
626 if tokens[2].len() != 4 {
627 return None;
628 }
629 let year = ascii_number(tokens[2].as_bytes())? as i32;
630 build_date(year, month, day, 0, 0, 0, 0, DateFormat::LongForm)
631}
632
633fn parse_iso8601(input: &str) -> Option<FlexibleDate> {
636 let bytes = input.as_bytes();
637 if bytes.len() < 10 || bytes[4] != b'-' || bytes[7] != b'-' {
638 return None;
639 }
640 let year = ascii_number(&bytes[0..4])? as i32;
641 let month = ascii_number(&bytes[5..7])? as u8;
642 let day = ascii_number(&bytes[8..10])? as u8;
643 if bytes.len() == 10 {
644 return build_date(year, month, day, 0, 0, 0, 0, DateFormat::IsoDate);
645 }
646 if !matches!(bytes[10], b'T' | b't' | b' ') {
647 return None;
648 }
649 let rest = input.get(11..).unwrap_or("");
653 let (time_part, zone_part) = match rest.find(['Z', 'z', '+', '-']) {
656 Some(pos) => rest.split_at(pos),
657 None => (rest, ""),
658 };
659 let clock = match time_part.split_once('.') {
661 Some((clock, frac)) => {
662 if frac.is_empty() || !frac.bytes().all(|b| b.is_ascii_digit()) {
663 return None;
664 }
665 clock
666 }
667 None => time_part,
668 };
669 let (hour, minute, second) = parse_hms(clock)?;
670 let offset = if zone_part.is_empty() {
671 0
672 } else {
673 parse_zone(zone_part)?
674 };
675 build_date(
676 year,
677 month,
678 day,
679 hour,
680 minute,
681 second,
682 offset,
683 DateFormat::IsoDateTime,
684 )
685}
686
687#[cfg(test)]
688mod tests {
689 use super::*;
690
691 #[test]
696 fn rfc2822_spec_example() {
697 let dt = parse_flexible_date("Wed, 01 Jul 2026 07:07:07 +0000")
698 .expect("spec A4 example parses");
699 assert_eq!(dt.format, DateFormat::Rfc2822);
700 assert_eq!(dt.to_rfc3339(), "2026-07-01T07:07:07+00:00");
701 assert_eq!(dt.to_rfc2822(), "Wed, 01 Jul 2026 07:07:07 +0000");
702 assert_eq!(dt.to_iso_date(), "2026-07-01");
703 }
704
705 #[test]
706 fn rfc2822_wrong_weekday_is_tolerated() {
707 let dt = parse_flexible_date("Thu, 11 Apr 2026 06:06:06 +0000")
710 .expect("wrong weekday still parses");
711 assert_eq!(dt.to_rfc3339(), "2026-04-11T06:06:06+00:00");
712 assert_eq!(dt.to_rfc2822(), "Sat, 11 Apr 2026 06:06:06 +0000");
714 }
715
716 #[test]
717 fn rfc2822_no_weekday_no_zone() {
718 let dt = parse_flexible_date("11 Apr 2026 06:06:06")
719 .expect("weekday and zone are optional");
720 assert_eq!(dt.offset_minutes, 0);
721 assert_eq!(dt.to_rfc3339(), "2026-04-11T06:06:06+00:00");
722 }
723
724 #[test]
725 fn rfc2822_optional_seconds() {
726 let dt = parse_flexible_date("11 Apr 2026 06:06 +0000")
727 .expect("RFC 2822 seconds are optional");
728 assert_eq!(dt.second, 0);
729 }
730
731 #[test]
732 fn rfc2822_positive_hhmm_offset() {
733 let dt = parse_flexible_date("Fri, 25 Dec 2026 18:30:45 +0530")
734 .expect("+hhmm offset parses");
735 assert_eq!(dt.offset_minutes, 330);
736 assert_eq!(dt.to_rfc3339(), "2026-12-25T18:30:45+05:30");
737 assert_eq!(dt.to_rfc2822(), "Fri, 25 Dec 2026 18:30:45 +0530");
738 }
739
740 #[test]
741 fn rfc2822_negative_hhmm_offset() {
742 let dt = parse_flexible_date("Sat, 04 Jul 2026 09:15:00 -0700")
743 .expect("-hhmm offset parses");
744 assert_eq!(dt.offset_minutes, -420);
745 assert_eq!(dt.to_rfc3339(), "2026-07-04T09:15:00-07:00");
746 }
747
748 #[test]
749 fn rfc2822_named_zones() {
750 let gmt = parse_flexible_date("11 Apr 2026 06:06:06 GMT").unwrap();
751 assert_eq!(gmt.offset_minutes, 0);
752 let est = parse_flexible_date("11 Apr 2026 06:06:06 EST").unwrap();
753 assert_eq!(est.offset_minutes, -300);
754 assert!(parse_flexible_date("11 Apr 2026 06:06:06 XYZ").is_err());
755 }
756
757 #[test]
758 fn rfc2822_full_month_name_with_time() {
759 let dt = parse_flexible_date("1 July 2026 06:06:06 +0000")
760 .expect("full month name with time parses");
761 assert_eq!(dt.month, 7);
762 assert_eq!(dt.format, DateFormat::Rfc2822);
763 }
764
765 #[test]
770 fn long_form_month_first_single_digit_day() {
771 let dt = parse_flexible_date("July 1, 2026")
772 .expect("spec A4 long form parses");
773 assert_eq!(dt.format, DateFormat::LongForm);
774 assert_eq!((dt.year, dt.month, dt.day), (2026, 7, 1));
775 assert_eq!(dt.to_rfc2822(), "Wed, 01 Jul 2026 00:00:00 +0000");
776 assert_eq!(dt.to_w3c_date(), "2026-07-01T00:00:00+00:00");
777 }
778
779 #[test]
780 fn long_form_day_first() {
781 let dt = parse_flexible_date("1 July 2026")
782 .expect("day-first long form parses");
783 assert_eq!((dt.year, dt.month, dt.day), (2026, 7, 1));
784 }
785
786 #[test]
787 fn long_form_case_insensitive_and_no_comma() {
788 let dt = parse_flexible_date("december 25 2026").unwrap();
789 assert_eq!((dt.month, dt.day), (12, 25));
790 }
791
792 #[test]
793 fn long_form_rejects_bad_month_and_day() {
794 assert!(parse_flexible_date("Juvember 1, 2026").is_err());
795 assert!(parse_flexible_date("July 32, 2026").is_err());
796 assert!(parse_flexible_date("July 0, 2026").is_err());
797 }
798
799 #[test]
804 fn iso_date_only() {
805 let dt = parse_flexible_date("2026-07-01").expect("ISO date parses");
806 assert_eq!(dt.format, DateFormat::IsoDate);
807 assert_eq!(dt.to_rfc3339(), "2026-07-01T00:00:00+00:00");
808 assert_eq!(dt.to_iso_date(), "2026-07-01");
809 }
810
811 #[test]
812 fn iso_datetime_zulu() {
813 let dt = parse_flexible_date("2026-07-01T07:07:07Z")
814 .expect("Z-suffixed datetime parses");
815 assert_eq!(dt.format, DateFormat::IsoDateTime);
816 assert_eq!(dt.to_rfc3339(), "2026-07-01T07:07:07+00:00");
817 }
818
819 #[test]
820 fn iso_datetime_with_colon_offset() {
821 let dt = parse_flexible_date("2026-07-01T07:07:07+05:30").unwrap();
822 assert_eq!(dt.offset_minutes, 330);
823 let dt = parse_flexible_date("2026-07-01T07:07:07-07:00").unwrap();
824 assert_eq!(dt.offset_minutes, -420);
825 }
826
827 #[test]
828 fn iso_datetime_with_compact_offset() {
829 let dt = parse_flexible_date("2026-07-01T07:07:07+0530").unwrap();
830 assert_eq!(dt.offset_minutes, 330);
831 }
832
833 #[test]
834 fn iso_datetime_fractional_seconds_ignored() {
835 let dt = parse_flexible_date("2026-07-01T07:07:07.123Z").unwrap();
836 assert_eq!(dt.second, 7);
837 assert!(parse_flexible_date("2026-07-01T07:07:07.abcZ").is_err());
838 }
839
840 #[test]
841 fn iso_datetime_without_offset_defaults_utc() {
842 let dt = parse_flexible_date("2026-07-01T07:07:07").unwrap();
843 assert_eq!(dt.offset_minutes, 0);
844 }
845
846 #[test]
847 fn iso_rejects_invalid_components() {
848 assert!(parse_flexible_date("2026-13-01").is_err());
849 assert!(parse_flexible_date("2026-00-01").is_err());
850 assert!(parse_flexible_date("2026-04-31").is_err());
851 assert!(parse_flexible_date("2026-07-01T24:00:00").is_err());
852 assert!(parse_flexible_date("2026-07-01T07:60:00").is_err());
853 assert!(parse_flexible_date("2026-07-01T07:07:07+2500").is_err());
854 }
855
856 #[test]
861 fn leap_day_validation() {
862 assert!(parse_flexible_date("2024-02-29").is_ok());
863 assert!(parse_flexible_date("2023-02-29").is_err());
864 assert!(parse_flexible_date("2000-02-29").is_ok());
866 assert!(parse_flexible_date("2100-02-29").is_err());
867 assert!(parse_flexible_date("Thu, 29 Feb 2024 12:00:00 +0000").is_ok());
868 assert!(parse_flexible_date("February 29, 2024").is_ok());
869 assert!(parse_flexible_date("February 29, 2023").is_err());
870 }
871
872 #[test]
873 fn days_in_month_table() {
874 assert_eq!(days_in_month(2026, 1), 31);
875 assert_eq!(days_in_month(2026, 4), 30);
876 assert_eq!(days_in_month(2024, 2), 29);
877 assert_eq!(days_in_month(2025, 2), 28);
878 assert_eq!(days_in_month(2026, 13), 0);
879 assert!(is_leap_year(2000));
880 assert!(!is_leap_year(1900));
881 }
882
883 #[test]
888 fn garbage_input_is_err() {
889 for garbage in [
890 "",
891 " ",
892 "not a date",
893 "yesterday",
894 "13/01/2026",
895 "2026",
896 "--",
897 ] {
898 assert!(
899 parse_flexible_date(garbage).is_err(),
900 "{garbage:?} should not parse"
901 );
902 }
903 }
904
905 #[test]
906 fn error_names_all_attempted_formats() {
907 let err = parse_flexible_date("not a date").unwrap_err();
908 assert_eq!(err.input(), "not a date");
909 assert_eq!(err.attempted_formats().len(), 4);
910 let msg = err.to_string();
911 assert!(msg.contains("RFC 2822"), "message names RFC 2822: {msg}");
912 assert!(msg.contains("long form"), "message names long form: {msg}");
913 assert!(
914 msg.contains("ISO 8601 date"),
915 "message names ISO date: {msg}"
916 );
917 assert!(
918 msg.contains("ISO 8601 datetime"),
919 "message names ISO datetime: {msg}"
920 );
921 }
922
923 #[test]
924 fn error_truncates_long_input() {
925 let long = "x".repeat(200);
926 let err = parse_flexible_date(&long).unwrap_err();
927 assert!(err.input().chars().count() <= 65);
928 assert!(err.input().ends_with('…'));
929 }
930
931 #[test]
936 fn weekday_computation_across_calendar() {
937 let cases = [
939 ("2026-07-01", "Wed"),
940 ("2024-02-29", "Thu"),
941 ("2000-01-01", "Sat"),
942 ("1990-01-01", "Mon"),
943 ("2100-12-31", "Fri"),
944 ];
945 for (iso, weekday) in cases {
946 let dt = parse_flexible_date(iso).unwrap();
947 let rendered = dt.to_rfc2822();
948 assert!(
949 rendered.starts_with(weekday),
950 "{iso} should be {weekday}, got {rendered}"
951 );
952 }
953 }
954
955 #[test]
956 fn rfc2822_round_trips_through_itself() {
957 let dt = parse_flexible_date("2026-07-01T07:07:07+05:30").unwrap();
958 let reparsed = parse_flexible_date(&dt.to_rfc2822()).unwrap();
959 assert_eq!(dt.to_rfc3339(), reparsed.to_rfc3339());
960 }
961
962 #[test]
967 fn build_date_rejects_out_of_range_year() {
968 assert!(parse_flexible_date("0000-01-01").is_err());
970 }
971
972 #[test]
977 fn parse_hms_rejects_malformed_tokens() {
978 assert_eq!(parse_hms("xx:30"), None);
980 assert_eq!(parse_hms("12"), None);
982 assert_eq!(parse_hms("12:xx"), None);
984 assert_eq!(parse_hms("12:30:xx"), None);
986 assert_eq!(parse_hms("12:30:45:59"), None);
988 assert_eq!(parse_hms(""), None);
990 }
991
992 #[test]
997 fn parse_zone_maps_every_rfc2822_named_zone() {
998 let cases = [
999 ("EST", -5 * 60),
1000 ("EDT", -4 * 60),
1001 ("CST", -6 * 60),
1002 ("CDT", -5 * 60),
1003 ("MST", -7 * 60),
1004 ("MDT", -6 * 60),
1005 ("PST", -8 * 60),
1006 ("PDT", -7 * 60),
1007 ];
1008 for (name, minutes) in cases {
1009 assert_eq!(parse_zone(name), Some(minutes), "{name}");
1010 }
1011 }
1012
1013 #[test]
1014 fn parse_zone_rejects_malformed_offsets() {
1015 assert_eq!(parse_zone(""), None);
1017 assert_eq!(parse_zone("0500"), None);
1019 assert_eq!(parse_zone("+aa30"), None);
1021 assert_eq!(parse_zone("+12a0"), None);
1023 assert_eq!(parse_zone("+aa:30"), None);
1025 assert_eq!(parse_zone("+12:a0"), None);
1027 assert_eq!(parse_zone("+123"), None);
1029 assert_eq!(parse_zone("+2460"), None);
1031 }
1032
1033 #[test]
1038 fn ascii_number_rejects_empty_overflow_and_non_digits() {
1039 assert_eq!(ascii_number(b""), None);
1040 assert_eq!(ascii_number(b"99999999999"), None);
1042 assert_eq!(ascii_number(b"12a4"), None);
1043 assert_eq!(ascii_number(b"2026"), Some(2026));
1044 }
1045
1046 #[test]
1051 fn rfc2822_rejects_each_malformed_token() {
1052 assert!(parse_flexible_date("aa Jul 2026 12:00").is_err());
1054 assert!(parse_flexible_date("01 Foo 2026 12:00").is_err());
1056 assert!(parse_flexible_date("01 Jul 26 12:00").is_err());
1058 assert!(parse_flexible_date("01 Jul 2o26 12:00").is_err());
1060 assert!(parse_flexible_date("01 Jul 2026 xx:00").is_err());
1062 }
1063
1064 #[test]
1069 fn long_form_rejects_each_malformed_token() {
1070 assert!(parse_flexible_date("July 123 2026").is_err());
1072 assert!(parse_flexible_date("July aa 2026").is_err());
1074 assert!(parse_flexible_date("July 1 26").is_err());
1076 assert!(parse_flexible_date("July 1 2o26").is_err());
1078 }
1079
1080 #[test]
1085 fn iso8601_rejects_each_malformed_component() {
1086 assert!(parse_flexible_date("2o26-07-01").is_err());
1088 assert!(parse_flexible_date("2026-o7-01").is_err());
1089 assert!(parse_flexible_date("2026-07-o1").is_err());
1090 assert!(parse_flexible_date("2026-07-01x12:00").is_err());
1092 assert!(parse_flexible_date("2026-07-01T1a:00").is_err());
1094 }
1095}