1use std::fmt::{self, Display, Formatter};
5
6use serde::{
7 Deserialize, Deserializer, Serialize, Serializer,
8 de::{self, Visitor},
9};
10
11use crate::{
12 error::{TemporalKind, TypeError},
13 fragment::Fragment,
14};
15
16#[repr(transparent)]
17#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
18pub struct Date {
19 days_since_epoch: i32,
20}
21
22impl Date {
23 #[inline]
24 pub fn is_leap_year(year: i32) -> bool {
25 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
26 }
27
28 #[inline]
29 pub fn days_in_month(year: i32, month: u32) -> u32 {
30 match month {
31 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
32 4 | 6 | 9 | 11 => 30,
33 2 => {
34 if Self::is_leap_year(year) {
35 29
36 } else {
37 28
38 }
39 }
40 _ => 0,
41 }
42 }
43
44 fn ymd_to_days_since_epoch(year: i32, month: u32, day: u32) -> Option<i32> {
45 if !(1..=12).contains(&month) || day < 1 || day > Self::days_in_month(year, month) {
46 return None;
47 }
48
49 let (y, m) = if month <= 2 {
50 (year - 1, month as i32 + 9)
51 } else {
52 (year, month as i32 - 3)
53 };
54
55 let era = if y >= 0 {
56 y
57 } else {
58 y - 399
59 } / 400;
60 let yoe = y - era * 400;
61 let doy = (153 * m + 2) / 5 + day as i32 - 1;
62 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
63 let days = era * 146097 + doe - 719468;
64
65 Some(days)
66 }
67
68 fn days_since_epoch_to_ymd(days: i32) -> (i32, u32, u32) {
69 let days_since_ce = days + 719468;
70
71 let era = if days_since_ce >= 0 {
72 days_since_ce
73 } else {
74 days_since_ce - 146096
75 } / 146097;
76 let doe = days_since_ce - era * 146097;
77 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
78 let y = yoe + era * 400;
79 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
80 let mp = (5 * doy + 2) / 153;
81 let d = doy - (153 * mp + 2) / 5 + 1;
82 let m = if mp < 10 {
83 mp + 3
84 } else {
85 mp - 9
86 };
87 let year = if m <= 2 {
88 y + 1
89 } else {
90 y
91 };
92
93 (year, m as u32, d as u32)
94 }
95}
96
97impl Date {
98 fn overflow_err(message: impl Into<String>) -> TypeError {
99 TypeError::Temporal {
100 kind: TemporalKind::DateOverflow {
101 message: message.into(),
102 },
103 message: "date overflow".to_string(),
104 fragment: Fragment::None,
105 }
106 }
107
108 pub fn new(year: i32, month: u32, day: u32) -> Option<Self> {
109 Self::ymd_to_days_since_epoch(year, month, day).map(|days_since_epoch| Self {
110 days_since_epoch,
111 })
112 }
113
114 pub fn from_ymd(year: i32, month: u32, day: u32) -> Result<Self, Box<TypeError>> {
115 Self::new(year, month, day).ok_or_else(|| {
116 Box::new(Self::overflow_err(format!("invalid date: {}-{:02}-{:02}", year, month, day)))
117 })
118 }
119
120 pub fn year(&self) -> i32 {
121 Self::days_since_epoch_to_ymd(self.days_since_epoch).0
122 }
123
124 pub fn month(&self) -> u32 {
125 Self::days_since_epoch_to_ymd(self.days_since_epoch).1
126 }
127
128 pub fn day(&self) -> u32 {
129 Self::days_since_epoch_to_ymd(self.days_since_epoch).2
130 }
131
132 pub fn to_days_since_epoch(&self) -> i32 {
133 self.days_since_epoch
134 }
135
136 pub fn from_days_since_epoch(days: i32) -> Option<Self> {
137 if !(-365_250_000..=365_250_000).contains(&days) {
138 return None;
139 }
140 Some(Self {
141 days_since_epoch: days,
142 })
143 }
144}
145
146impl Display for Date {
147 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
148 let (year, month, day) = Self::days_since_epoch_to_ymd(self.days_since_epoch);
149 if year < 0 {
150 write!(f, "-{:04}-{:02}-{:02}", -year, month, day)
151 } else {
152 write!(f, "{:04}-{:02}-{:02}", year, month, day)
153 }
154 }
155}
156
157impl Serialize for Date {
158 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
159 where
160 S: Serializer,
161 {
162 serializer.serialize_i32(self.days_since_epoch)
163 }
164}
165
166struct DateVisitor;
167
168impl<'de> Visitor<'de> for DateVisitor {
169 type Value = Date;
170
171 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
172 formatter.write_str("a date as days since the Unix epoch (i32)")
173 }
174
175 fn visit_i32<E>(self, value: i32) -> Result<Date, E>
176 where
177 E: de::Error,
178 {
179 Date::from_days_since_epoch(value)
180 .ok_or_else(|| E::custom(format!("date days out of range: {}", value)))
181 }
182
183 fn visit_i64<E>(self, value: i64) -> Result<Date, E>
184 where
185 E: de::Error,
186 {
187 let days = i32::try_from(value).map_err(|_| E::custom(format!("date days out of range: {}", value)))?;
188 self.visit_i32(days)
189 }
190
191 fn visit_u64<E>(self, value: u64) -> Result<Date, E>
192 where
193 E: de::Error,
194 {
195 let days = i32::try_from(value).map_err(|_| E::custom(format!("date days out of range: {}", value)))?;
196 self.visit_i32(days)
197 }
198}
199
200impl<'de> Deserialize<'de> for Date {
201 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
202 where
203 D: Deserializer<'de>,
204 {
205 deserializer.deserialize_i32(DateVisitor)
206 }
207}
208
209#[cfg(test)]
210pub mod tests {
211 use std::fmt::Debug;
212
213 use postcard::{from_bytes, to_allocvec};
214 use serde_json::{from_str, to_string};
215
216 use super::*;
217 use crate::error::{TemporalKind, TypeError};
218
219 #[test]
220 fn test_date_display_standard_dates() {
221 let date = Date::new(2024, 3, 15).unwrap();
223 assert_eq!(format!("{}", date), "2024-03-15");
224
225 let date = Date::new(2000, 1, 1).unwrap();
226 assert_eq!(format!("{}", date), "2000-01-01");
227
228 let date = Date::new(1999, 12, 31).unwrap();
229 assert_eq!(format!("{}", date), "1999-12-31");
230 }
231
232 #[test]
233 fn test_date_display_edge_cases() {
234 let date = Date::new(1970, 1, 1).unwrap();
236 assert_eq!(format!("{}", date), "1970-01-01");
237
238 let date = Date::new(2024, 2, 29).unwrap();
240 assert_eq!(format!("{}", date), "2024-02-29");
241
242 let date = Date::new(2024, 1, 9).unwrap();
244 assert_eq!(format!("{}", date), "2024-01-09");
245
246 let date = Date::new(2024, 9, 1).unwrap();
247 assert_eq!(format!("{}", date), "2024-09-01");
248 }
249
250 #[test]
251 fn test_date_display_boundary_dates() {
252 let date = Date::new(1, 1, 1).unwrap();
254 assert_eq!(format!("{}", date), "0001-01-01");
255
256 let date = Date::new(9999, 12, 31).unwrap();
258 assert_eq!(format!("{}", date), "9999-12-31");
259
260 let date = Date::new(1900, 1, 1).unwrap();
262 assert_eq!(format!("{}", date), "1900-01-01");
263
264 let date = Date::new(2000, 1, 1).unwrap();
265 assert_eq!(format!("{}", date), "2000-01-01");
266
267 let date = Date::new(2100, 1, 1).unwrap();
268 assert_eq!(format!("{}", date), "2100-01-01");
269 }
270
271 #[test]
272 fn test_date_display_negative_years() {
273 let date = Date::new(0, 1, 1).unwrap();
275 assert_eq!(format!("{}", date), "0000-01-01");
276
277 let date = Date::new(-1, 1, 1).unwrap();
279 assert_eq!(format!("{}", date), "-0001-01-01");
280
281 let date = Date::new(-100, 12, 31).unwrap();
282 assert_eq!(format!("{}", date), "-0100-12-31");
283 }
284
285 #[test]
286 fn test_date_display_default() {
287 let date = Date::default();
288 assert_eq!(format!("{}", date), "1970-01-01");
289 }
290
291 #[test]
292 fn test_date_display_all_months() {
293 let months = [
294 (1, "01"),
295 (2, "02"),
296 (3, "03"),
297 (4, "04"),
298 (5, "05"),
299 (6, "06"),
300 (7, "07"),
301 (8, "08"),
302 (9, "09"),
303 (10, "10"),
304 (11, "11"),
305 (12, "12"),
306 ];
307
308 for (month, expected) in months {
309 let date = Date::new(2024, month, 15).unwrap();
310 assert_eq!(format!("{}", date), format!("2024-{}-15", expected));
311 }
312 }
313
314 #[test]
315 fn test_date_display_days_in_month() {
316 let test_cases = [
318 (2024, 1, 1, "2024-01-01"),
319 (2024, 1, 31, "2024-01-31"),
320 (2024, 2, 1, "2024-02-01"),
321 (2024, 2, 29, "2024-02-29"), (2024, 4, 1, "2024-04-01"),
323 (2024, 4, 30, "2024-04-30"),
324 (2024, 12, 1, "2024-12-01"),
325 (2024, 12, 31, "2024-12-31"),
326 ];
327
328 for (year, month, day, expected) in test_cases {
329 let date = Date::new(year, month, day).unwrap();
330 assert_eq!(format!("{}", date), expected);
331 }
332 }
333
334 #[test]
335 fn test_date_roundtrip() {
336 let test_dates = [
338 (1900, 1, 1),
339 (1970, 1, 1),
340 (2000, 2, 29), (2024, 12, 31),
342 (2100, 6, 15),
343 ];
344
345 for (year, month, day) in test_dates {
346 let date = Date::new(year, month, day).unwrap();
347 let days = date.to_days_since_epoch();
348 let recovered = Date::from_days_since_epoch(days).unwrap();
349
350 assert_eq!(date.year(), recovered.year());
351 assert_eq!(date.month(), recovered.month());
352 assert_eq!(date.day(), recovered.day());
353 }
354 }
355
356 #[test]
357 fn test_leap_year_detection() {
358 assert!(Date::is_leap_year(2000)); assert!(Date::is_leap_year(2024)); assert!(!Date::is_leap_year(1900)); assert!(!Date::is_leap_year(2023)); }
363
364 #[test]
365 fn test_invalid_dates() {
366 assert!(Date::new(2024, 0, 1).is_none()); assert!(Date::new(2024, 13, 1).is_none()); assert!(Date::new(2024, 1, 0).is_none()); assert!(Date::new(2024, 1, 32).is_none()); assert!(Date::new(2023, 2, 29).is_none()); assert!(Date::new(2024, 4, 31).is_none()); }
373
374 #[test]
375 fn test_serde_roundtrip() {
376 let date = Date::new(2024, 3, 15).unwrap();
377 let json = to_string(&date).unwrap();
378 assert_eq!(json, date.to_days_since_epoch().to_string());
380
381 let recovered: Date = from_str(&json).unwrap();
382 assert_eq!(date, recovered);
383 }
384
385 #[test]
386 fn test_serde_postcard_roundtrip_negative_years() {
387 for (y, m, d) in [(-100, 12, 31), (0, 1, 1), (1970, 1, 1), (2024, 3, 15), (9999, 12, 31)] {
389 let date = Date::new(y, m, d).unwrap();
390 let bytes = to_allocvec(&date).unwrap();
391 let recovered: Date = from_bytes(&bytes).unwrap();
392 assert_eq!(date, recovered);
393 assert_eq!(recovered.year(), y);
394 assert_eq!(recovered.month(), m);
395 assert_eq!(recovered.day(), d);
396 }
397 }
398
399 #[test]
400 fn test_deserialize_rejects_out_of_range_days() {
401 let json = 400_000_000i64.to_string();
403 assert!(from_str::<Date>(&json).is_err());
404 }
405
406 fn assert_date_overflow<T: Debug>(result: Result<T, Box<TypeError>>) {
407 let err = result.expect_err("expected DateOverflow error");
408 match *err {
409 TypeError::Temporal {
410 kind: TemporalKind::DateOverflow {
411 ..
412 },
413 ..
414 } => {}
415 other => panic!("expected DateOverflow, got: {:?}", other),
416 }
417 }
418
419 #[test]
420 fn test_from_ymd_invalid_month() {
421 assert_date_overflow(Date::from_ymd(2024, 0, 1));
422 assert_date_overflow(Date::from_ymd(2024, 13, 1));
423 }
424
425 #[test]
426 fn test_from_ymd_invalid_day() {
427 assert_date_overflow(Date::from_ymd(2024, 1, 0));
428 assert_date_overflow(Date::from_ymd(2024, 1, 32));
429 }
430
431 #[test]
432 fn test_from_ymd_non_leap_year() {
433 assert_date_overflow(Date::from_ymd(2023, 2, 29));
434 }
435}