1use std::error::Error;
4use std::fmt::{self, Display, Formatter};
5use std::str::FromStr;
6use std::time::{Duration, SystemTime};
7
8#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
10pub struct Date {
11 year: Year,
12 day: Day,
13}
14
15impl Date {
16 const FIRST_RELEASE_TIMESTAMP: u64 = 1_448_946_000; #[inline]
19 #[must_use]
20 pub const fn new(year: Year, day: Day) -> Option<Date> {
21 if day.0 <= year.max_day().0 {
22 Some(Self { year, day })
23 } else {
24 None
25 }
26 }
27
28 #[inline]
29 #[must_use]
30 pub const fn year(self) -> Year {
31 self.year
32 }
33
34 #[inline]
35 #[must_use]
36 pub const fn day(self) -> Day {
37 self.day
38 }
39
40 fn release_timestamp(self) -> u64 {
41 let mut days = u64::from(self.day.0) - 1;
42
43 for year in 2016..=self.year.0 {
44 let is_leap_year =
45 (year.is_multiple_of(4) && !year.is_multiple_of(100)) || year.is_multiple_of(400);
46 days += if is_leap_year { 366 } else { 365 };
47 }
48
49 Self::FIRST_RELEASE_TIMESTAMP + (days * 86400)
50 }
51
52 #[must_use]
57 pub fn release_time(&self) -> SystemTime {
58 SystemTime::UNIX_EPOCH + Duration::from_secs(self.release_timestamp())
59 }
60
61 #[must_use]
63 #[expect(clippy::cast_possible_truncation)]
64 pub fn next_puzzle() -> Option<Date> {
65 let now = SystemTime::now()
66 .duration_since(SystemTime::UNIX_EPOCH)
67 .unwrap()
68 .as_secs();
69
70 let mut date = Date {
71 year: Year(2015),
72 day: Day(1),
73 };
74
75 if now > Self::FIRST_RELEASE_TIMESTAMP {
77 let year = 2015 + ((now - Self::FIRST_RELEASE_TIMESTAMP) / 60 / 60 / 24 / 366);
78 if year > 9999 {
79 return None;
80 }
81 date.year = Year(year as u16);
82 }
83
84 while date.release_timestamp() < now {
85 if date.day.0 < date.year.max_day().0 {
86 date.day.0 += 1;
87 } else if date.year.0 < 9999 {
88 date.year.0 += 1;
89 date.day.0 = 1;
90 } else {
91 return None;
92 }
93 }
94
95 Some(date)
96 }
97}
98
99impl TryFrom<(Year, Day)> for Date {
100 type Error = InvalidDateError;
101
102 #[inline]
103 fn try_from((year, day): (Year, Day)) -> Result<Self, Self::Error> {
104 Self::new(year, day).ok_or(InvalidDateError(year))
105 }
106}
107
108impl Display for Date {
109 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
110 write!(f, "{} day {}", self.year.0, self.day.0)
111 }
112}
113
114#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
116pub struct Year(u16);
117
118impl Year {
119 #[inline]
120 #[must_use]
121 pub const fn new(year: u16) -> Option<Self> {
122 if year >= 2015 && year <= 9999 {
123 Some(Self(year))
124 } else {
125 None
126 }
127 }
128
129 #[inline]
145 #[must_use]
146 #[track_caller]
147 pub const fn new_const<const YEAR: u16>() -> Self {
148 const {
149 assert!(YEAR >= 2015 && YEAR <= 9999);
150 }
151 Self(YEAR)
152 }
153
154 #[inline]
155 #[must_use]
156 pub const fn to_u16(self) -> u16 {
157 self.0
158 }
159
160 #[inline]
169 #[must_use]
170 pub const fn max_day(self) -> Day {
171 if self.0 < 2025 {
172 Day::new_const::<25>()
173 } else {
174 Day::new_const::<12>()
175 }
176 }
177
178 #[inline]
179 pub fn days(self) -> impl Iterator<Item = Day> {
180 (1..=self.max_day().to_u8()).map(|d| Day::new(d).unwrap())
181 }
182}
183
184impl TryFrom<u16> for Year {
185 type Error = InvalidYearError;
186
187 #[inline]
188 fn try_from(value: u16) -> Result<Self, Self::Error> {
189 Self::new(value).ok_or(InvalidYearError)
190 }
191}
192
193impl FromStr for Year {
194 type Err = InvalidYearError;
195
196 fn from_str(s: &str) -> Result<Self, Self::Err> {
197 if let Ok(v) = s.parse::<u16>() {
198 v.try_into()
199 } else {
200 Err(InvalidYearError)
201 }
202 }
203}
204
205impl Display for Year {
206 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
207 if f.alternate() {
208 write!(f, "{}", self.0)
209 } else {
210 write!(f, "Year {}", self.0)
211 }
212 }
213}
214
215#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
217pub struct Day(u8);
218
219impl Day {
220 #[inline]
221 #[must_use]
222 pub const fn new(day: u8) -> Option<Self> {
223 if day >= 1 && day <= 25 {
224 Some(Self(day))
225 } else {
226 None
227 }
228 }
229
230 #[inline]
246 #[must_use]
247 #[track_caller]
248 pub const fn new_const<const DAY: u8>() -> Self {
249 const {
250 assert!(DAY >= 1 && DAY <= 25);
251 }
252 Self(DAY)
253 }
254
255 #[inline]
256 #[must_use]
257 pub const fn to_u8(self) -> u8 {
258 self.0
259 }
260}
261
262impl TryFrom<u8> for Day {
263 type Error = InvalidDayError;
264
265 #[inline]
266 fn try_from(value: u8) -> Result<Self, Self::Error> {
267 Self::new(value).ok_or(InvalidDayError)
268 }
269}
270
271impl FromStr for Day {
272 type Err = InvalidDayError;
273
274 fn from_str(s: &str) -> Result<Self, Self::Err> {
275 if let Ok(v) = s.parse::<u8>() {
276 v.try_into()
277 } else {
278 Err(InvalidDayError)
279 }
280 }
281}
282
283impl Display for Day {
284 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
285 if f.alternate() {
286 write!(f, "{:02}", self.0)
287 } else {
288 write!(f, "Day {:02}", self.0)
289 }
290 }
291}
292
293#[derive(Debug)]
295pub struct InvalidDateError(Year);
296
297impl Display for InvalidDateError {
298 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
299 write!(f, "invalid day for year {:#}", self.0)
300 }
301}
302
303impl Error for InvalidDateError {}
304
305#[derive(Debug)]
307pub struct InvalidYearError;
308
309impl Display for InvalidYearError {
310 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
311 f.write_str("invalid year")
312 }
313}
314
315impl Error for InvalidYearError {}
316
317#[derive(Debug)]
319pub struct InvalidDayError;
320
321impl Display for InvalidDayError {
322 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
323 f.write_str("invalid day")
324 }
325}
326
327impl Error for InvalidDayError {}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 #[test]
334 fn date_release_timestamps() {
335 assert_eq!(
336 Date {
337 year: Year(2015),
338 day: Day(1)
339 }
340 .release_timestamp(),
341 1_448_946_000
342 );
343 assert_eq!(
344 Date {
345 year: Year(2016),
346 day: Day(23)
347 }
348 .release_timestamp(),
349 1_482_469_200
350 );
351 assert_eq!(
352 Date {
353 year: Year(2017),
354 day: Day(11)
355 }
356 .release_timestamp(),
357 1_512_968_400
358 );
359 assert_eq!(
360 Date {
361 year: Year(2021),
362 day: Day(2)
363 }
364 .release_timestamp(),
365 1_638_421_200
366 );
367 assert_eq!(
368 Date {
369 year: Year(2023),
370 day: Day(7)
371 }
372 .release_timestamp(),
373 1_701_925_200
374 );
375 }
376}