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    pub year: Year,
12    pub day: Day,
13}
14
15impl Date {
16    const FIRST_RELEASE_TIMESTAMP: u64 = 1_448_946_000; // 2015-12-01 05:00 UTC
17
18    fn release_timestamp(self) -> u64 {
19        let mut days = u64::from(self.day.0) - 1;
20
21        for year in 2016..=self.year.0 {
22            let is_leap_year =
23                (year.is_multiple_of(4) && !year.is_multiple_of(100)) || year.is_multiple_of(400);
24            days += if is_leap_year { 366 } else { 365 };
25        }
26
27        Self::FIRST_RELEASE_TIMESTAMP + (days * 86400)
28    }
29
30    /// The [`SystemTime`] when the puzzle was released/is scheduled to release.
31    ///
32    /// This can be compared to [`SystemTime::now()`] to check if a puzzle is released, or how long
33    /// remains until release.
34    #[must_use]
35    pub fn release_time(&self) -> SystemTime {
36        SystemTime::UNIX_EPOCH + Duration::from_secs(self.release_timestamp())
37    }
38
39    /// The [`Date`] of the next puzzle.
40    #[must_use]
41    #[expect(clippy::cast_possible_truncation)]
42    pub fn next_puzzle() -> Option<Date> {
43        let now = SystemTime::now()
44            .duration_since(SystemTime::UNIX_EPOCH)
45            .unwrap()
46            .as_secs();
47
48        let mut date = Date {
49            year: Year(2015),
50            day: Day(1),
51        };
52
53        // Skip ahead whole years
54        if now > Self::FIRST_RELEASE_TIMESTAMP {
55            let year = 2015 + ((now - Self::FIRST_RELEASE_TIMESTAMP) / 60 / 60 / 24 / 366);
56            if year > 9999 {
57                return None;
58            }
59            date.year = Year(year as u16);
60        }
61
62        while date.release_timestamp() < now {
63            if date.day.0 < 25 {
64                date.day.0 += 1;
65            } else if date.year.0 < 9999 {
66                date.year.0 += 1;
67                date.day.0 = 1;
68            } else {
69                return None;
70            }
71        }
72
73        Some(date)
74    }
75}
76
77impl Display for Date {
78    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
79        write!(f, "{} day {}", self.year.0, self.day.0)
80    }
81}
82
83/// Represents a 4-digit year, 2015 or later.
84#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
85pub struct Year(u16);
86
87impl Year {
88    #[must_use]
89    pub fn new(year: u16) -> Option<Self> {
90        if (2015..=9999).contains(&year) {
91            Some(Self(year))
92        } else {
93            None
94        }
95    }
96
97    /// # Panics
98    ///
99    /// Panics at compile time (enforced by the use of const generics) if the year is out of range
100    ///
101    /// # Examples
102    ///
103    /// ```
104    /// # use utils::date::Year;
105    /// let year = Year::new_const::<2015>();
106    /// ```
107    ///
108    /// ```should_panic
109    /// # use utils::date::Year;
110    /// let year = Year::new_const::<2000>();
111    /// ```
112    #[must_use]
113    pub const fn new_const<const YEAR: u16>() -> Self {
114        assert!(YEAR >= 2015 && YEAR <= 9999);
115        Self(YEAR)
116    }
117
118    #[must_use]
119    pub const fn to_u16(self) -> u16 {
120        self.0
121    }
122}
123
124impl TryFrom<u16> for Year {
125    type Error = InvalidYearError;
126
127    fn try_from(value: u16) -> Result<Self, Self::Error> {
128        Self::new(value).ok_or(InvalidYearError)
129    }
130}
131
132impl FromStr for Year {
133    type Err = InvalidYearError;
134
135    fn from_str(s: &str) -> Result<Self, Self::Err> {
136        if let Ok(v) = s.parse::<u16>() {
137            v.try_into()
138        } else {
139            Err(InvalidYearError)
140        }
141    }
142}
143
144impl Display for Year {
145    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
146        if f.alternate() {
147            write!(f, "{}", self.0)
148        } else {
149            write!(f, "Year {}", self.0)
150        }
151    }
152}
153
154/// Represents a day between 1 and 25 (inclusive).
155#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
156pub struct Day(u8);
157
158impl Day {
159    #[must_use]
160    pub fn new(day: u8) -> Option<Self> {
161        if (1..=25).contains(&day) {
162            Some(Self(day))
163        } else {
164            None
165        }
166    }
167
168    /// # Panics
169    ///
170    /// Panics at compile time (enforced by the use of const generics) if the day is out of range
171    ///
172    /// # Examples
173    ///
174    /// ```
175    /// # use utils::date::Day;
176    /// let year = Day::new_const::<17>();
177    /// ```
178    ///
179    /// ```should_panic
180    /// # use utils::date::Day;
181    /// let year = Day::new_const::<26>();
182    /// ```
183    #[must_use]
184    pub const fn new_const<const DAY: u8>() -> Self {
185        assert!(DAY >= 1 && DAY <= 25);
186        Self(DAY)
187    }
188
189    #[must_use]
190    pub const fn to_u8(self) -> u8 {
191        self.0
192    }
193}
194
195impl TryFrom<u8> for Day {
196    type Error = InvalidDayError;
197
198    fn try_from(value: u8) -> Result<Self, Self::Error> {
199        Self::new(value).ok_or(InvalidDayError)
200    }
201}
202
203impl FromStr for Day {
204    type Err = InvalidDayError;
205
206    fn from_str(s: &str) -> Result<Self, Self::Err> {
207        if let Ok(v) = s.parse::<u8>() {
208            v.try_into()
209        } else {
210            Err(InvalidDayError)
211        }
212    }
213}
214
215impl Display for Day {
216    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
217        if f.alternate() {
218            write!(f, "{:02}", self.0)
219        } else {
220            write!(f, "Day {:02}", self.0)
221        }
222    }
223}
224
225/// Error type returned when trying to convert an invalid value to a [`Year`].
226#[derive(Debug)]
227pub struct InvalidYearError;
228
229impl Display for InvalidYearError {
230    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
231        f.write_str("invalid year")
232    }
233}
234
235impl Error for InvalidYearError {}
236
237/// Error type returned when trying to convert an invalid value to a [`Day`].
238#[derive(Debug)]
239pub struct InvalidDayError;
240
241impl Display for InvalidDayError {
242    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
243        f.write_str("invalid day")
244    }
245}
246
247impl Error for InvalidDayError {}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252
253    #[test]
254    fn date_release_timestamps() {
255        assert_eq!(
256            Date {
257                year: Year(2015),
258                day: Day(1)
259            }
260            .release_timestamp(),
261            1_448_946_000
262        );
263        assert_eq!(
264            Date {
265                year: Year(2016),
266                day: Day(23)
267            }
268            .release_timestamp(),
269            1_482_469_200
270        );
271        assert_eq!(
272            Date {
273                year: Year(2017),
274                day: Day(11)
275            }
276            .release_timestamp(),
277            1_512_968_400
278        );
279        assert_eq!(
280            Date {
281                year: Year(2021),
282                day: Day(2)
283            }
284            .release_timestamp(),
285            1_638_421_200
286        );
287        assert_eq!(
288            Date {
289                year: Year(2023),
290                day: Day(7)
291            }
292            .release_timestamp(),
293            1_701_925_200
294        );
295    }
296}