1use std::fmt;
2use std::ops::{RangeFrom, RangeInclusive};
3
4use crate::week::{DAYS, Day};
5use crate::{Error, Result};
6
7const MONTHS: RangeInclusive<u8> = 1..=12;
8const YEARS: RangeFrom<u16> = 1..;
9
10#[expect(clippy::cast_possible_truncation)]
11const DAYS_PER_WEEK: u8 = DAYS.len() as u8;
12
13fn month_days(year: u16, month: u8) -> RangeInclusive<u8> {
14 let month = usize::from(month);
15 let is_leap_year =
16 year.is_multiple_of(4) && !year.is_multiple_of(100) || year.is_multiple_of(400);
17 1..=[
18 31, if is_leap_year { 29 } else { 28 }, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, ][month - 1]
31}
32
33fn days_before_year(year: u16) -> usize {
36 debug_assert!(YEARS.contains(&year));
37 let years = usize::from(year - 1);
38 years * 365 + years / 4 - years / 100 + years / 400 }
43
44fn days_before_month(year: u16, month: u8) -> usize {
45 debug_assert!(YEARS.contains(&year));
46 debug_assert!(MONTHS.contains(&month));
47 (1..month).map(|m| month_days(year, m).count()).sum()
48}
49
50#[derive(Clone, Copy, PartialEq, Eq)]
52pub struct Date {
53 year: u16,
54 month: u8,
55 day: u8,
56}
57
58impl Date {
59 pub const MAX: Date = Date {
62 year: u16::MAX,
63 month: 12,
64 day: 31,
65 };
66
67 fn count_days(self) -> usize {
70 let days = usize::from(self.day - 1);
71 days_before_year(self.year) + days_before_month(self.year, self.month) + days
72 }
73
74 #[must_use]
75 pub fn day_of_week(self) -> Day {
76 DAYS[(self.count_days() + 1) % DAYS.len()]
78 }
79
80 fn day_of_next_month(self, day: u8) -> Date {
81 let month = self.month + 1;
82 let (year, month) = if MONTHS.contains(&month) {
83 (self.year, month)
84 } else {
85 (self.year + 1, 1)
86 };
87 Date { year, month, day }
88 }
89
90 #[must_use]
97 pub fn plus_one_day(self) -> Date {
98 let day = self.day + 1;
99 if month_days(self.year, self.month).contains(&day) {
100 Date { day, ..self }
101 } else {
102 self.day_of_next_month(1)
103 }
104 }
105
106 #[must_use]
113 pub fn plus_one_week(self) -> Date {
114 let month_days = month_days(self.year, self.month);
115 let day = self.day + DAYS_PER_WEEK;
116 if month_days.contains(&day) {
117 Date { day, ..self }
118 } else {
119 self.day_of_next_month(day - month_days.end())
120 }
121 }
122
123 pub fn from_ymd(year: u16, month: u8, day: u8) -> Result<Date> {
144 (YEARS.contains(&year) && MONTHS.contains(&month) && month_days(year, month).contains(&day))
145 .then_some(Date { year, month, day })
146 .ok_or(Error::Date { year, month, day })
147 }
148
149 #[must_use]
150 pub fn day(self) -> u8 {
151 self.day
152 }
153
154 #[must_use]
155 pub fn month(self) -> u8 {
156 self.month
157 }
158
159 #[must_use]
160 pub fn year(self) -> u16 {
161 self.year
162 }
163
164 #[must_use]
166 pub fn is_leap_day(self) -> bool {
167 self.month == 2 && self.day == 29
168 }
169}
170
171impl fmt::Display for Date {
172 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173 let Date { year, month, day } = self;
174 write!(f, "{year:04}-{month:02}-{day:02}")
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 #[test]
183 fn fields_are_one_based() {
184 assert!(Date::from_ymd(1, 1, 1).is_ok());
185 assert!(Date::from_ymd(1, 1, 0).is_err());
186 assert!(Date::from_ymd(1, 0, 1).is_err());
187 assert!(Date::from_ymd(0, 1, 1).is_err());
188 }
189
190 #[test]
191 fn day_of_week_works() {
192 for (year, day) in [
193 (1, Day::Mon), (2, Day::Tue), (3, Day::Wed),
196 (4, Day::Thu),
197 (5, Day::Sat), ] {
199 let got = Date::from_ymd(year, 1, 1).map(Date::day_of_week);
200 assert_eq!(got, Ok(day), "{year}-01-01");
201 }
202 }
203}