utils/
date.rs

1//! Items for representing days, years and dates.
2
3use std::error::Error;
4use std::fmt::{self, Display, Formatter};
5use std::str::FromStr;
6use std::time::{Duration, SystemTime};
7
8/// Represents the [`Year`] and [`Day`] a puzzle was released.
9#[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; // 2015-12-01 05:00 UTC
17
18    #[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    /// The [`SystemTime`] when the puzzle was released/is scheduled to release.
53    ///
54    /// This can be compared to [`SystemTime::now()`] to check if a puzzle is released, or how long
55    /// remains until release.
56    #[must_use]
57    pub fn release_time(&self) -> SystemTime {
58        SystemTime::UNIX_EPOCH + Duration::from_secs(self.release_timestamp())
59    }
60
61    /// The [`Date`] of the next puzzle.
62    #[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        // Skip ahead whole years
76        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/// Represents a 4-digit year, 2015 or later.
115#[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    /// # Panics
130    ///
131    /// Panics at compile time (enforced by the use of const generics) if the year is out of range
132    ///
133    /// # Examples
134    ///
135    /// ```
136    /// # use utils::date::Year;
137    /// let year = Year::new_const::<2015>();
138    /// ```
139    ///
140    /// ```compile_fail
141    /// # use utils::date::Year;
142    /// let year = Year::new_const::<2000>();
143    /// ```
144    #[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    /// Returns the maximum day for the given year.
161    ///
162    /// # Examples
163    /// ```
164    /// # use utils::date::{Day, Year};
165    /// assert_eq!(Year::new_const::<2015>().max_day(), Day::new_const::<25>());
166    /// assert_eq!(Year::new_const::<2025>().max_day(), Day::new_const::<12>());
167    /// ```
168    #[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/// Represents a day between 1 and 25 (inclusive).
216#[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    /// # Panics
231    ///
232    /// Panics at compile time (enforced by the use of const generics) if the day is out of range
233    ///
234    /// # Examples
235    ///
236    /// ```
237    /// # use utils::date::Day;
238    /// let year = Day::new_const::<17>();
239    /// ```
240    ///
241    /// ```compile_fail
242    /// # use utils::date::Day;
243    /// let year = Day::new_const::<26>();
244    /// ```
245    #[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/// Error type returned when trying to construct an invalid [`Date`].
294#[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/// Error type returned when trying to convert an invalid value to a [`Year`].
306#[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/// Error type returned when trying to convert an invalid value to a [`Day`].
318#[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}