1#![forbid(unsafe_code)]
2const DAYS_BEFORE_MONTH: [u16; 12] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
24pub struct CalendarDate {
25 year: i32,
26 month: u8,
27 day: u8,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum DateError {
32 InvalidMonth,
33 InvalidDay,
34}
35
36fn is_leap_year(year: i32) -> bool {
37 year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
38}
39
40fn days_in_month(year: i32, month: u8) -> Option<u8> {
41 match month {
42 1 | 3 | 5 | 7 | 8 | 10 | 12 => Some(31),
43 4 | 6 | 9 | 11 => Some(30),
44 2 => Some(if is_leap_year(year) { 29 } else { 28 }),
45 _ => None,
46 }
47}
48
49fn day_of_year_unchecked(year: i32, month: u8, day: u8) -> u16 {
50 let mut ordinal = DAYS_BEFORE_MONTH[usize::from(month - 1)] + u16::from(day);
51
52 if is_leap_year(year) && month > 2 {
53 ordinal += 1;
54 }
55
56 ordinal
57}
58
59fn days_from_civil(year: i32, month: u8, day: u8) -> i64 {
60 let year = i64::from(year) - if month <= 2 { 1 } else { 0 };
61 let era = if year >= 0 { year } else { year - 399 } / 400;
62 let year_of_era = year - era * 400;
63 let month_index = i64::from(month) + if month > 2 { -3 } else { 9 };
64 let day_of_year = (153 * month_index + 2) / 5 + i64::from(day) - 1;
65 let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
66
67 era * 146_097 + day_of_era - 719_468
68}
69
70fn civil_from_days(days: i64) -> CalendarDate {
71 let days = days + 719_468;
72 let era = if days >= 0 { days } else { days - 146_096 } / 146_097;
73 let day_of_era = days - era * 146_097;
74 let year_of_era =
75 (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
76 let year = year_of_era + era * 400;
77 let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
78 let month_index = (5 * day_of_year + 2) / 153;
79 let day = day_of_year - (153 * month_index + 2) / 5 + 1;
80 let month = month_index + if month_index < 10 { 3 } else { -9 };
81 let year = year + if month <= 2 { 1 } else { 0 };
82
83 CalendarDate {
84 year: year as i32,
85 month: month as u8,
86 day: day as u8,
87 }
88}
89
90pub fn is_valid_date(year: i32, month: u8, day: u8) -> bool {
91 matches!(days_in_month(year, month), Some(days) if (1..=days).contains(&day))
92}
93
94impl CalendarDate {
95 pub fn new(year: i32, month: u8, day: u8) -> Result<Self, DateError> {
96 let max_day = days_in_month(year, month).ok_or(DateError::InvalidMonth)?;
97
98 if !(1..=max_day).contains(&day) {
99 return Err(DateError::InvalidDay);
100 }
101
102 Ok(Self { year, month, day })
103 }
104
105 #[must_use]
106 pub fn year(&self) -> i32 {
107 self.year
108 }
109
110 #[must_use]
111 pub fn month(&self) -> u8 {
112 self.month
113 }
114
115 #[must_use]
116 pub fn day(&self) -> u8 {
117 self.day
118 }
119
120 #[must_use]
121 pub fn is_valid(&self) -> bool {
122 is_valid_date(self.year, self.month, self.day)
123 }
124
125 #[must_use]
126 pub fn day_of_year(&self) -> u16 {
127 day_of_year_unchecked(self.year, self.month, self.day)
128 }
129}
130
131pub fn day_of_year(year: i32, month: u8, day: u8) -> Result<u16, DateError> {
132 Ok(CalendarDate::new(year, month, day)?.day_of_year())
133}
134
135#[must_use]
136pub fn days_between(start: CalendarDate, end: CalendarDate) -> i64 {
137 days_from_civil(end.year, end.month, end.day)
138 - days_from_civil(start.year, start.month, start.day)
139}
140
141#[must_use]
142pub fn add_days(date: CalendarDate, days: i64) -> CalendarDate {
143 civil_from_days(days_from_civil(date.year, date.month, date.day) + days)
144}
145
146#[cfg(test)]
147mod tests {
148 use super::{add_days, day_of_year, days_between, is_valid_date, CalendarDate, DateError};
149
150 #[test]
151 fn validates_dates_and_day_of_year() {
152 let leap_day = CalendarDate::new(2024, 2, 29).unwrap();
153
154 assert!(leap_day.is_valid());
155 assert!(is_valid_date(2024, 2, 29));
156 assert!(!is_valid_date(2023, 2, 29));
157 assert_eq!(leap_day.day_of_year(), 60);
158 assert_eq!(day_of_year(2024, 12, 31).unwrap(), 366);
159 }
160
161 #[test]
162 fn adds_days_and_measures_distance() {
163 let start = CalendarDate::new(2024, 2, 28).unwrap();
164 let end = add_days(start, 2);
165
166 assert_eq!(end, CalendarDate::new(2024, 3, 1).unwrap());
167 assert_eq!(days_between(start, end), 2);
168 assert_eq!(add_days(end, -2), start);
169 }
170
171 #[test]
172 fn rejects_invalid_dates() {
173 assert_eq!(CalendarDate::new(2024, 13, 1), Err(DateError::InvalidMonth));
174 assert_eq!(CalendarDate::new(2024, 2, 0), Err(DateError::InvalidDay));
175 assert_eq!(CalendarDate::new(2023, 2, 29), Err(DateError::InvalidDay));
176 assert_eq!(day_of_year(2024, 0, 1), Err(DateError::InvalidMonth));
177 }
178}