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 = (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 #[must_use]
34 pub fn release_time(&self) -> SystemTime {
35 SystemTime::UNIX_EPOCH + Duration::from_secs(self.release_timestamp())
36 }
37
38 #[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 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#[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 #[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#[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 #[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#[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#[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}