use core::fmt;
use core::str::FromStr;
use crate::error::ParseDateError;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(try_from = "String", into = "String"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Date {
year: i32,
month: u8,
day: u8,
}
impl Date {
pub fn new(year: i32, month: u8, day: u8) -> Result<Self, ParseDateError> {
if month == 0 || month > 12 || day == 0 || day > days_in_month(year, month) {
return Err(ParseDateError::NotACalendarDay { year, month, day });
}
Ok(Self { year, month, day })
}
pub fn parse(s: &str) -> Result<Self, ParseDateError> {
let malformed = || ParseDateError::Malformed {
input: s.to_owned(),
};
let b = s.as_bytes();
if b.len() != 10 || b[4] != b'-' || b[7] != b'-' {
return Err(malformed());
}
let digits = |r: core::ops::Range<usize>| -> Option<u32> {
let mut n = 0u32;
for i in r {
if !b[i].is_ascii_digit() {
return None;
}
n = n * 10 + u32::from(b[i] - b'0');
}
Some(n)
};
let (y, m, d) = (
digits(0..4).ok_or_else(malformed)?,
digits(5..7).ok_or_else(malformed)?,
digits(8..10).ok_or_else(malformed)?,
);
Self::new(y as i32, m as u8, d as u8)
}
#[must_use]
pub fn year(self) -> i32 {
self.year
}
#[must_use]
pub fn month(self) -> u8 {
self.month
}
#[must_use]
pub fn day(self) -> u8 {
self.day
}
}
const fn is_leap(year: i32) -> bool {
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}
const fn days_in_month(year: i32, month: u8) -> u8 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 if is_leap(year) => 29,
2 => 28,
_ => 0,
}
}
impl fmt::Display for Date {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.pad(&format!(
"{:04}-{:02}-{:02}",
self.year, self.month, self.day
))
}
}
impl FromStr for Date {
type Err = ParseDateError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
#[cfg(feature = "serde")]
impl TryFrom<String> for Date {
type Error = ParseDateError;
fn try_from(s: String) -> Result<Self, Self::Error> {
Self::parse(&s)
}
}
#[cfg(feature = "serde")]
impl From<Date> for String {
fn from(d: Date) -> Self {
d.to_string()
}
}
#[cfg(feature = "chrono")]
impl From<chrono::NaiveDate> for Date {
fn from(d: chrono::NaiveDate) -> Self {
use chrono::Datelike as _;
Self {
year: d.year(),
month: d.month() as u8,
day: d.day() as u8,
}
}
}
#[cfg(feature = "chrono")]
impl TryFrom<Date> for chrono::NaiveDate {
type Error = Date;
fn try_from(d: Date) -> Result<Self, Self::Error> {
chrono::NaiveDate::from_ymd_opt(d.year, u32::from(d.month), u32::from(d.day)).ok_or(d)
}
}
#[cfg(feature = "time")]
impl TryFrom<time::Date> for Date {
type Error = ParseDateError;
fn try_from(d: time::Date) -> Result<Self, Self::Error> {
Self::new(d.year(), u8::from(d.month()), d.day())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_and_renders_iso_8601() {
let d = Date::parse("2026-06-30").unwrap();
assert_eq!((d.year(), d.month(), d.day()), (2026, 6, 30));
assert_eq!(d.to_string(), "2026-06-30");
}
#[test]
fn rejects_non_calendar_days() {
for bad in [
"2026-02-30",
"2026-13-01",
"2026-00-01",
"2026-01-00",
"2026-04-31",
] {
assert!(Date::parse(bad).is_err(), "{bad} should not parse");
}
assert!(Date::parse("2024-02-29").is_ok(), "2024 is a leap year");
assert!(Date::parse("2026-02-29").is_err(), "2026 is not");
assert!(Date::parse("2000-02-29").is_ok(), "400-year rule");
assert!(Date::parse("1900-02-29").is_err(), "100-year rule");
}
#[test]
fn rejects_anything_that_is_not_a_bare_calendar_date() {
for bad in [
"2026-06-01T00:00:00",
"2026-06-01Z",
"2026-6-1",
"26-06-01",
"2026/06/01",
"",
"today",
] {
assert!(Date::parse(bad).is_err(), "{bad:?} should not parse");
}
}
#[test]
fn ordering_is_chronological() {
let d = |s| Date::parse(s).unwrap();
assert!(d("2026-01-31") < d("2026-02-01"));
assert!(d("2025-12-31") < d("2026-01-01"));
assert!(d("2026-06-30") >= d("2026-06-01"));
let mut v = [d("2026-03-01"), d("2025-12-31"), d("2026-01-15")];
v.sort();
assert_eq!(v[0], d("2025-12-31"));
assert_eq!(v[2], d("2026-03-01"));
}
#[cfg(feature = "serde")]
#[test]
fn serde_round_trips_and_validates() {
let d = Date::parse("2026-06-30").unwrap();
let json = serde_json::to_string(&d).unwrap();
assert_eq!(json, r#""2026-06-30""#);
assert_eq!(serde_json::from_str::<Date>(&json).unwrap(), d);
assert!(serde_json::from_str::<Date>(r#""2026-02-30""#).is_err());
}
}