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