1use std::{fmt, str::FromStr};
5
6use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
7
8use crate::ParseError;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub struct Date {
13 year: u16,
14 month: u8,
15 day: u8,
16}
17
18impl Date {
19 pub fn new(year: u16, month: u8, day: u8) -> Option<Self> {
21 let valid = (1..=9999).contains(&year) && (1..=12).contains(&month) && day >= 1 && day <= days_in(year, month);
22 valid.then_some(Date { year, month, day })
23 }
24
25 pub fn year(self) -> u16 {
27 self.year
28 }
29
30 pub fn month(self) -> u8 {
32 self.month
33 }
34
35 pub fn day(self) -> u8 {
37 self.day
38 }
39}
40
41fn days_in(year: u16, month: u8) -> u8 {
42 match month {
43 4 | 6 | 9 | 11 => 30,
44 2 if year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400)) => 29,
45 2 => 28,
46 _ => 31,
47 }
48}
49
50impl FromStr for Date {
51 type Err = ParseError;
52
53 fn from_str(s: &str) -> Result<Self, Self::Err> {
54 let err = || ParseError::Date(s.to_owned());
55 let b = s.as_bytes();
56 if b.len() != 10 || b[4] != b'-' || b[7] != b'-' {
57 return Err(err());
58 }
59 let num = |part: &str| part.bytes().all(|c| c.is_ascii_digit()).then(|| part.parse::<u16>().ok()).flatten();
60 let (year, month, day) = (num(&s[0..4]), num(&s[5..7]), num(&s[8..10]));
61 match (year, month, day) {
62 (Some(y), Some(m), Some(d)) => Date::new(y, m as u8, d as u8).ok_or_else(err),
63 _ => Err(err()),
64 }
65 }
66}
67
68impl fmt::Display for Date {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
71 }
72}
73
74impl Serialize for Date {
75 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
76 serializer.collect_str(self)
77 }
78}
79
80impl<'de> Deserialize<'de> for Date {
81 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
82 String::deserialize(deserializer)?.parse().map_err(de::Error::custom)
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn reads_and_writes_iso_dates() {
92 assert_eq!("2026-10-24".parse::<Date>().unwrap().to_string(), "2026-10-24");
93 assert!("2024-02-29".parse::<Date>().is_ok());
94 assert!("2000-02-29".parse::<Date>().is_ok());
95 }
96
97 #[test]
98 fn rejects_days_that_do_not_exist() {
99 for s in ["2026-02-29", "1900-02-29", "2026-04-31", "2026-13-01", "0000-01-01", "2026-1-01", "26-10-24"] {
100 assert!(s.parse::<Date>().is_err(), "{s:?}");
101 }
102 }
103
104 #[test]
105 fn orders_chronologically() {
106 assert!("2026-01-31".parse::<Date>().unwrap() < "2026-02-01".parse().unwrap());
107 }
108}