use std::sync::LazyLock;
use time::format_description::{self, FormatItem};
use time::{Date, PrimitiveDateTime};
static DATE_FMT: LazyLock<Vec<FormatItem<'static>>> =
LazyLock::new(|| format_description::parse("[year]-[month]-[day]").expect("valid format"));
static DATETIME_FMTS: LazyLock<[Vec<FormatItem<'static>>; 2]> = LazyLock::new(|| {
[
format_description::parse("[year]-[month]-[day]T[hour]:[minute]:[second]")
.expect("valid format"),
format_description::parse("[year]-[month]-[day]T[hour]:[minute]").expect("valid format"),
]
});
pub(crate) fn is_valid_date(s: &str) -> bool {
parse_date(s).is_some()
}
pub(crate) fn is_valid_datetime(s: &str) -> bool {
parse_datetime(s).is_some()
}
pub fn parse_date(s: &str) -> Option<(i32, u8, u8)> {
Date::parse(s, &*DATE_FMT)
.ok()
.map(|d| (d.year(), u8::from(d.month()), d.day()))
}
pub fn parse_datetime(s: &str) -> Option<(i32, u8, u8, u8, u8, u8)> {
for fmt in DATETIME_FMTS.iter() {
if let Ok(dt) = PrimitiveDateTime::parse(s, fmt) {
return Some((
dt.year(),
u8::from(dt.month()),
dt.day(),
dt.hour(),
dt.minute(),
dt.second(),
));
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_date_accepts_bare_calendar_dates() {
assert_eq!(parse_date("2026-06-01"), Some((2026, 6, 1)));
assert_eq!(parse_date("2000-12-31"), Some((2000, 12, 31)));
}
#[test]
fn parse_date_rejects_time_components_and_malformed() {
for s in [
"",
"2026",
"2026-06",
"2026-6-1", "2026-13-01", "2026-02-30", "2026-06-01T12:00", "2026-06-01T12:00:00", "2026-06-01 12:00", "2026-06-01Z", "2026-06-01T12:00:00Z", "not-a-date",
] {
assert_eq!(parse_date(s), None, "expected rejected date: {s}");
}
}
#[test]
fn parse_datetime_accepts_offsetless_wall_clock() {
assert_eq!(
parse_datetime("2026-06-01T14:30:15"),
Some((2026, 6, 1, 14, 30, 15))
);
assert_eq!(
parse_datetime("2026-06-01T14:30"),
Some((2026, 6, 1, 14, 30, 0))
);
assert_eq!(
parse_datetime("2026-06-01T00:00:00"),
Some((2026, 6, 1, 0, 0, 0))
);
}
#[test]
fn parse_datetime_rejects_offsets_space_fraction_and_bare_date() {
for s in [
"",
"2026-06-01", "2026-06-01T14:30:00Z", "2026-06-01T14:30:00+00:00", "2026-06-01T14:30:00+05:30", "2026-06-01T14:30:00-05:00", "2026-06-01 14:30:00", "2026-06-01T14:30:00.5", "2026-06-01T14:30:00.123", "2026-06-01T14:30:00 ", "2026-06-01T14", "2026-06-01T14:3", "2026-13-01T14:30:00", "2026-02-30T14:30:00", ] {
assert_eq!(parse_datetime(s), None, "expected rejected datetime: {s}");
}
}
}