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