1use std::fmt;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub struct Year(u16);
13
14impl Year {
15 pub const FIRST: Self = Self(2015);
17 pub const FIRST_SHORT: Self = Self(2025);
20
21 #[must_use]
23 pub const fn new(year: u16) -> Option<Self> {
24 if year >= Self::FIRST.0 {
25 Some(Self(year))
26 } else {
27 None
28 }
29 }
30
31 #[must_use]
33 pub const fn last_day(self) -> Day {
34 if self.0 >= Self::FIRST_SHORT.0 {
35 Day::LAST_SHORT
36 } else {
37 Day::LAST_FULL
38 }
39 }
40
41 #[must_use]
43 pub const fn has_day(self, day: Day) -> bool {
44 day.0 <= self.last_day().0
45 }
46
47 #[must_use]
49 pub const fn get(self) -> u16 {
50 self.0
51 }
52}
53
54impl fmt::Display for Year {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 write!(f, "{}", self.0)
57 }
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
65pub struct Day(u8);
66
67impl Day {
68 pub const FIRST: Self = Self(1);
70 pub const LAST_FULL: Self = Self(25);
72 pub const LAST_SHORT: Self = Self(12);
74
75 #[must_use]
77 pub const fn new(day: u8) -> Option<Self> {
78 if day >= Self::FIRST.0 && day <= Self::LAST_FULL.0 {
79 Some(Self(day))
80 } else {
81 None
82 }
83 }
84
85 #[must_use]
91 pub const fn clamped(year: Year, day: u32) -> Self {
92 let last = year.last_day();
93
94 if day < Self::FIRST.0 as u32 {
95 Self::FIRST
96 } else if day > last.0 as u32 {
97 last
98 } else {
99 #[allow(clippy::cast_possible_truncation)]
100 Self(day as u8)
101 }
102 }
103
104 #[must_use]
106 pub const fn get(self) -> u8 {
107 self.0
108 }
109}
110
111impl fmt::Display for Day {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 write!(f, "{}", self.0)
114 }
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
119pub enum Part {
120 One,
122 Two,
124}
125
126impl Part {
127 #[must_use]
129 pub const fn number(self) -> u8 {
130 match self {
131 Self::One => 1,
132 Self::Two => 2,
133 }
134 }
135}
136
137impl fmt::Display for Part {
138 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139 write!(f, "part {}", self.number())
140 }
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
145pub struct Puzzle {
146 pub year: Year,
148 pub day: Day,
150}
151
152impl Puzzle {
153 #[must_use]
158 pub const fn new(year: Year, day: Day) -> Option<Self> {
159 if year.has_day(day) {
160 Some(Self { year, day })
161 } else {
162 None
163 }
164 }
165
166 #[must_use]
168 pub fn url(self) -> String {
169 format!("https://adventofcode.com/{}/day/{}", self.year, self.day)
170 }
171}
172
173impl fmt::Display for Puzzle {
174 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175 write!(f, "{} day {}", self.year, self.day)
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 fn year(year: u16) -> Year {
184 Year::new(year).expect("valid year")
185 }
186
187 fn day(day: u8) -> Day {
188 Day::new(day).expect("valid day")
189 }
190
191 #[test]
192 fn year_rejects_years_before_the_first_event() {
193 assert_eq!(Year::new(2014), None);
194 assert_eq!(Year::new(2015).map(Year::get), Some(2015));
195 assert_eq!(Year::new(2024).map(Year::get), Some(2024));
196 }
197
198 #[test]
199 fn day_accepts_only_puzzle_days() {
200 assert_eq!(Day::new(0), None);
201 assert_eq!(Day::new(26), None);
202 assert_eq!(Day::new(1).map(Day::get), Some(1));
203 assert_eq!(Day::new(25).map(Day::get), Some(25));
204 }
205
206 #[test]
207 fn events_from_2025_on_are_half_as_long() {
208 assert_eq!(year(2015).last_day().get(), 25);
209 assert_eq!(year(2024).last_day().get(), 25);
210 assert_eq!(year(2025).last_day().get(), 12);
211 assert_eq!(year(2030).last_day().get(), 12);
212
213 assert!(year(2024).has_day(day(25)));
214 assert!(year(2025).has_day(day(12)));
215 assert!(!year(2025).has_day(day(13)));
216 assert!(!year(2025).has_day(day(25)));
217 }
218
219 #[test]
220 fn day_clamps_late_december_dates() {
221 assert_eq!(Day::clamped(year(2024), 26).get(), 25);
222 assert_eq!(Day::clamped(year(2024), 31).get(), 25);
223 assert_eq!(Day::clamped(year(2024), 25).get(), 25);
224 assert_eq!(Day::clamped(year(2024), 7).get(), 7);
225 assert_eq!(Day::clamped(year(2024), 0).get(), 1);
226 }
227
228 #[test]
229 fn day_clamps_to_the_end_of_a_shortened_event() {
230 assert_eq!(Day::clamped(year(2025), 13).get(), 12);
231 assert_eq!(Day::clamped(year(2025), 25).get(), 12);
232 assert_eq!(Day::clamped(year(2025), 31).get(), 12);
233 assert_eq!(Day::clamped(year(2025), 7).get(), 7);
234 }
235
236 #[test]
237 fn puzzle_rejects_days_its_event_never_published() {
238 assert!(Puzzle::new(year(2024), day(25)).is_some());
239 assert!(Puzzle::new(year(2025), day(12)).is_some());
240 assert_eq!(Puzzle::new(year(2025), day(13)), None);
241 assert_eq!(Puzzle::new(year(2025), day(25)), None);
242 }
243
244 #[test]
245 fn puzzle_renders_the_canonical_url() {
246 let puzzle = Puzzle::new(year(2024), day(5)).expect("2024 has a day 5");
247
248 assert_eq!(puzzle.url(), "https://adventofcode.com/2024/day/5");
249 }
250
251 #[test]
252 fn parts_map_to_api_numbers() {
253 assert_eq!(Part::One.number(), 1);
254 assert_eq!(Part::Two.number(), 2);
255 assert_eq!(Part::Two.to_string(), "part 2");
256 }
257}