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 pub year: Year,
12 pub day: Day,
13}
14
15impl Date {
16 const FIRST_RELEASE_TIMESTAMP: u64 = 1_448_946_000; 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 #[must_use]
35 pub fn release_time(&self) -> SystemTime {
36 SystemTime::UNIX_EPOCH + Duration::from_secs(self.release_timestamp())
37 }
38
39 #[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 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#[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 #[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#[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 #[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#[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#[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}