1use crate::error::DateError;
4use std::fmt;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
8pub struct Date {
9 year: i32,
10 month: u8,
11 day: u8,
12}
13
14impl Date {
15 pub fn new(year: i32, month: u8, day: u8) -> Result<Self, DateError> {
17 if !(0..=9999).contains(&year) || !(1..=12).contains(&month) {
18 return Err(DateError::Invalid);
19 }
20 if day == 0 || day > days_in_month(year, month) {
21 return Err(DateError::Invalid);
22 }
23 Ok(Self { year, month, day })
24 }
25
26 pub fn parse(s: &str) -> Result<Self, DateError> {
28 let s = s.trim();
29 if s.len() < 10 || s.as_bytes().get(4) != Some(&b'-') || s.as_bytes().get(7) != Some(&b'-')
30 {
31 return Err(DateError::Invalid);
32 }
33 if s.len() > 10 {
34 return Err(DateError::Invalid);
36 }
37 let year: i32 = s[..4].parse().map_err(|_| DateError::Invalid)?;
38 let month: u8 = s[5..7].parse().map_err(|_| DateError::Invalid)?;
39 let day: u8 = s[8..10].parse().map_err(|_| DateError::Invalid)?;
40 Self::new(year, month, day)
41 }
42
43 pub fn year(self) -> i32 {
45 self.year
46 }
47 pub fn month(self) -> u8 {
49 self.month
50 }
51 pub fn day(self) -> u8 {
53 self.day
54 }
55}
56
57fn days_in_month(year: i32, month: u8) -> u8 {
58 match month {
59 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
60 4 | 6 | 9 | 11 => 30,
61 2 if leap(year) => 29,
62 2 => 28,
63 _ => 0,
64 }
65}
66
67fn leap(year: i32) -> bool {
68 year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
69}
70
71impl fmt::Display for Date {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 #[test]
82 fn parses_iso_day() {
83 let d = Date::parse("2026-06-30").unwrap();
84 assert_eq!(d.to_string(), "2026-06-30");
85 assert!(Date::parse("2026-02-30").is_err());
86 assert!(Date::parse("2026-06-01T00:00:00").is_err());
87 assert!(Date::parse("2026-01-15Z").is_err());
89 assert!(Date::parse("2026-01-15+00:00").is_err());
90 }
91}