use chrono::{DateTime, NaiveDate, NaiveDateTime, Utc};
const DATE_FORMATS: &[&str] = &[
"%Y-%m-%dT%H:%M:%S%.f%:z", "%Y-%m-%dT%H:%M:%S%:z", "%Y-%m-%dT%H:%M:%S%.fZ", "%Y-%m-%dT%H:%M:%SZ", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S", "%Y-%m-%d", "%Y-%m-%d %H:%M:%S%:z", "%Y/%m/%d %H:%M:%S", "%Y/%m/%d", "%d %b %Y %H:%M:%S", "%d %b %Y", "%d %B %Y %H:%M:%S", "%d %B %Y", "%B %d, %Y %H:%M:%S", "%B %d, %Y", "%b %d, %Y %H:%M:%S", "%b %d, %Y", "%m/%d/%Y %H:%M:%S", "%m/%d/%Y", "%m-%d-%Y", "%d.%m.%Y %H:%M:%S", "%d.%m.%Y", "%d/%m/%Y %H:%M:%S", "%d/%m/%Y", "%d-%b-%Y", "%d-%B-%Y", ];
fn parse_asctime(s: &str) -> Option<NaiveDateTime> {
let b = s.as_bytes();
if b.len() < 24 {
return None;
}
if !b[..3].iter().all(u8::is_ascii_alphabetic) || b[3] != b' ' {
return None;
}
let rest = &s[4..];
let normalized = if rest.len() > 4 && rest.as_bytes()[4] == b' ' && rest.as_bytes()[3] == b' ' {
let mut n = String::with_capacity(rest.len());
n.push_str(&rest[..3]); n.push(' ');
n.push_str(rest[4..].trim_start_matches(' '));
n
} else {
rest.to_string()
};
NaiveDateTime::parse_from_str(&normalized, "%b %e %H:%M:%S %Y")
.or_else(|_| NaiveDateTime::parse_from_str(&normalized, "%b %d %H:%M:%S %Y"))
.ok()
}
fn strip_weekday_prefix(s: &str) -> Option<&str> {
let b = s.as_bytes();
if b.len() > 5 && b[3] == b',' && b[4] == b' ' && b[..3].iter().all(u8::is_ascii_alphabetic) {
Some(&s[5..])
} else {
None
}
}
#[must_use]
pub fn parse_date(input: &str) -> Option<DateTime<Utc>> {
let input = input.trim();
if input.is_empty() {
return None;
}
if let Ok(dt) = DateTime::parse_from_rfc3339(input) {
return Some(dt.with_timezone(&Utc));
}
if let Ok(dt) = DateTime::parse_from_rfc2822(input) {
return Some(dt.with_timezone(&Utc));
}
if let Some(stripped) = strip_weekday_prefix(input)
&& let Ok(dt) = DateTime::parse_from_rfc2822(stripped)
{
return Some(dt.with_timezone(&Utc));
}
if let Ok(year) = input.parse::<i32>()
&& (1000..=9999).contains(&year)
{
return NaiveDate::from_ymd_opt(year, 1, 1)
.and_then(|d| d.and_hms_opt(0, 0, 0))
.map(|dt| dt.and_utc());
}
if input.len() == 7
&& input.chars().nth(4) == Some('-')
&& let (Ok(year), Ok(month)) = (input[..4].parse::<i32>(), input[5..7].parse::<u32>())
&& (1000..=9999).contains(&year)
&& (1..=12).contains(&month)
{
return NaiveDate::from_ymd_opt(year, month, 1)
.and_then(|d| d.and_hms_opt(0, 0, 0))
.map(|dt| dt.and_utc());
}
if let Some(dt) = parse_asctime(input) {
return Some(dt.and_utc());
}
for fmt in DATE_FORMATS {
if let Ok(dt) = NaiveDateTime::parse_from_str(input, fmt) {
return Some(dt.and_utc());
}
if let Ok(date) = NaiveDate::parse_from_str(input, fmt) {
return date.and_hms_opt(0, 0, 0).map(|dt| dt.and_utc());
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::{Datelike, Timelike};
#[test]
fn test_rfc3339_with_timezone() {
let dt = parse_date("2024-12-14T10:30:00+00:00");
assert!(dt.is_some());
let dt = dt.unwrap();
assert_eq!(dt.year(), 2024);
assert_eq!(dt.month(), 12);
assert_eq!(dt.day(), 14);
assert_eq!(dt.hour(), 10);
assert_eq!(dt.minute(), 30);
}
#[test]
fn test_rfc3339_z_suffix() {
let dt = parse_date("2024-12-14T10:30:00Z");
assert!(dt.is_some());
assert_eq!(dt.unwrap().year(), 2024);
}
#[test]
fn test_rfc3339_with_milliseconds() {
let dt = parse_date("2024-12-14T10:30:00.123Z");
assert!(dt.is_some());
}
#[test]
fn test_rfc2822_format() {
let dt = parse_date("Sat, 14 Dec 2024 10:30:00 +0000");
assert!(dt.is_some());
let dt = dt.unwrap();
assert_eq!(dt.year(), 2024);
assert_eq!(dt.month(), 12);
}
#[test]
fn test_rfc2822_gmt() {
let dt = parse_date("Sat, 14 Dec 2024 10:30:00 GMT");
assert!(dt.is_some());
}
#[test]
fn test_iso8601_date_only() {
let dt = parse_date("2024-12-14");
assert!(dt.is_some());
let dt = dt.unwrap();
assert_eq!(dt.year(), 2024);
assert_eq!(dt.month(), 12);
assert_eq!(dt.day(), 14);
assert_eq!(dt.hour(), 0); }
#[test]
fn test_us_format_long_month() {
let dt = parse_date("December 14, 2024");
assert!(dt.is_some());
}
#[test]
fn test_us_format_short_month() {
let dt = parse_date("Dec 14, 2024");
assert!(dt.is_some());
}
#[test]
fn test_invalid_date() {
let dt = parse_date("not a date");
assert!(dt.is_none());
}
#[test]
fn test_empty_string() {
let dt = parse_date("");
assert!(dt.is_none());
}
#[test]
fn test_whitespace_only() {
let dt = parse_date(" ");
assert!(dt.is_none());
}
#[test]
fn test_partial_date_invalid() {
let dt = parse_date("2024-13"); assert!(dt.is_none());
let dt = parse_date("abcd-12");
assert!(dt.is_none());
}
#[test]
fn test_us_date_slash_format() {
let dt = parse_date("12/14/2024");
assert!(dt.is_some());
}
#[test]
fn test_eu_date_dot_format() {
let dt = parse_date("14.12.2024");
assert!(dt.is_some());
}
#[test]
fn test_rfc822_without_day() {
let dt = parse_date("14 Dec 2024");
assert!(dt.is_some());
}
#[test]
fn test_rfc822_long_month() {
let dt = parse_date("14 December 2024");
assert!(dt.is_some());
}
#[test]
fn test_year_slash_format() {
let dt = parse_date("2024/12/14");
assert!(dt.is_some());
}
#[test]
fn test_dash_month_format() {
let dt = parse_date("14-Dec-2024");
assert!(dt.is_some());
}
#[test]
fn test_us_dash_format() {
let dt = parse_date("12-14-2024");
assert!(dt.is_some());
}
#[test]
fn test_eu_slash_with_time() {
let dt = parse_date("14/12/2024 10:30:45");
assert!(dt.is_some());
}
#[test]
fn test_multiple_formats_dont_panic() {
let dates = vec![
"2024-12-14T10:30:00Z",
"Sat, 14 Dec 2024 10:30:00 GMT",
"14 Dec 2024",
"December 14, 2024",
"12/14/2024",
"14.12.2024",
"2024/12/14",
"14-Dec-2024",
"not a date",
"",
"2024",
"12/2024",
];
for date_str in dates {
let _ = parse_date(date_str);
}
}
#[test]
fn test_rfc2822_wrong_weekday() {
let dt = parse_date("Mon, 15 Jan 2026 10:30:00 +0000").unwrap();
assert_eq!(dt.year(), 2026);
assert_eq!(dt.month(), 1);
assert_eq!(dt.day(), 15);
assert_eq!(dt.hour(), 10);
}
#[test]
fn test_rfc2822_wrong_weekday_new_year() {
let dt = parse_date("Wed, 01 Jan 2026 00:00:00 +0000").unwrap();
assert_eq!(dt.year(), 2026);
assert_eq!(dt.month(), 1);
assert_eq!(dt.day(), 1);
}
#[test]
fn test_rfc2822_correct_weekday() {
let dt = parse_date("Thu, 15 Jan 2026 10:30:00 +0000").unwrap();
assert_eq!(dt.year(), 2026);
assert_eq!(dt.month(), 1);
assert_eq!(dt.day(), 15);
}
#[test]
fn test_rfc2822_no_weekday() {
let dt = parse_date("15 Jan 2026 10:30:00 +0000").unwrap();
assert_eq!(dt.year(), 2026);
assert_eq!(dt.month(), 1);
assert_eq!(dt.day(), 15);
}
#[test]
fn test_edge_case_leap_year() {
let dt = parse_date("2024-02-29");
assert!(dt.is_some());
}
#[test]
fn test_edge_case_invalid_date() {
let dt = parse_date("2023-02-29");
assert!(dt.is_none());
}
#[test]
fn test_year_only_format() {
let dt = parse_date("2024").unwrap();
assert_eq!(dt.year(), 2024);
assert_eq!(dt.month(), 1);
assert_eq!(dt.day(), 1);
assert_eq!(dt.hour(), 0);
}
#[test]
fn test_year_month_format() {
let dt = parse_date("2024-12").unwrap();
assert_eq!(dt.year(), 2024);
assert_eq!(dt.month(), 12);
assert_eq!(dt.day(), 1);
assert_eq!(dt.hour(), 0);
}
#[test]
fn test_all_format_strings() {
let cases: &[(&str, i32, u32, u32)] = &[
("2024-12-14T10:30:45.123+00:00", 2024, 12, 14),
("2024-12-14T10:30:45+00:00", 2024, 12, 14),
("2024-12-14T10:30:45.123Z", 2024, 12, 14),
("2024-12-14T10:30:45Z", 2024, 12, 14),
("2024-12-14T10:30:45", 2024, 12, 14),
("2024-12-14 10:30:45", 2024, 12, 14),
("2024-12-14", 2024, 12, 14),
("2024-12-14 10:30:45+00:00", 2024, 12, 14),
("2024/12/14 10:30:45", 2024, 12, 14),
("2024/12/14", 2024, 12, 14),
("14 Dec 2024 10:30:45", 2024, 12, 14),
("14 Dec 2024", 2024, 12, 14),
("14 December 2024 10:30:45", 2024, 12, 14),
("14 December 2024", 2024, 12, 14),
("December 14, 2024 10:30:45", 2024, 12, 14),
("December 14, 2024", 2024, 12, 14),
("Dec 14, 2024 10:30:45", 2024, 12, 14),
("Dec 14, 2024", 2024, 12, 14),
("12/14/2024 10:30:45", 2024, 12, 14),
("12/14/2024", 2024, 12, 14),
("12-14-2024", 2024, 12, 14),
("14.12.2024 10:30:45", 2024, 12, 14),
("14.12.2024", 2024, 12, 14),
("14/12/2024 10:30:45", 2024, 12, 14),
("14/12/2024", 2024, 12, 14),
("14-Dec-2024", 2024, 12, 14),
("14-December-2024", 2024, 12, 14),
("2024", 2024, 1, 1),
("2024-12", 2024, 12, 1),
];
for &(input, year, month, day) in cases {
let dt = parse_date(input).unwrap_or_else(|| panic!("Failed to parse: {input}"));
assert_eq!(dt.year(), year, "Year mismatch for: {input}");
assert_eq!(dt.month(), month, "Month mismatch for: {input}");
assert_eq!(dt.day(), day, "Day mismatch for: {input}");
}
}
#[test]
fn test_asctime_single_digit_day_space_padded() {
let dt = parse_date("Mon Jan 6 12:30:00 2025").unwrap();
assert_eq!(dt.year(), 2025);
assert_eq!(dt.month(), 1);
assert_eq!(dt.day(), 6);
assert_eq!(dt.hour(), 12);
assert_eq!(dt.minute(), 30);
assert_eq!(dt.second(), 0);
}
#[test]
fn test_asctime_double_digit_day() {
let dt = parse_date("Mon Jan 16 12:30:00 2025").unwrap();
assert_eq!(dt.year(), 2025);
assert_eq!(dt.month(), 1);
assert_eq!(dt.day(), 16);
}
#[test]
fn test_asctime_various_months() {
let dt = parse_date("Fri Dec 31 23:59:59 2021").unwrap();
assert_eq!(dt.year(), 2021);
assert_eq!(dt.month(), 12);
assert_eq!(dt.day(), 31);
}
}