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 value::duration::Duration,
15};
16
17#[repr(transparent)]
18#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
19pub struct Date {
20 days_since_epoch: i32,
21}
22
23impl Date {
24 #[inline]
25 pub fn is_leap_year(year: i32) -> bool {
26 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
27 }
28
29 #[inline]
30 pub fn days_in_month(year: i32, month: u32) -> u32 {
31 match month {
32 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
33 4 | 6 | 9 | 11 => 30,
34 2 => {
35 if Self::is_leap_year(year) {
36 29
37 } else {
38 28
39 }
40 }
41 _ => 0,
42 }
43 }
44
45 fn ymd_to_days_since_epoch(year: i32, month: u32, day: u32) -> Option<i32> {
46 if !(1..=12).contains(&month) || day < 1 || day > Self::days_in_month(year, month) {
47 return None;
48 }
49
50 let (y, m) = if month <= 2 {
51 (year - 1, month as i32 + 9)
52 } else {
53 (year, month as i32 - 3)
54 };
55
56 let era = if y >= 0 {
57 y
58 } else {
59 y - 399
60 } / 400;
61 let yoe = y - era * 400;
62 let doy = (153 * m + 2) / 5 + day as i32 - 1;
63 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
64 let days = era * 146097 + doe - 719468;
65
66 Some(days)
67 }
68
69 fn days_since_epoch_to_ymd(days: i32) -> (i32, u32, u32) {
70 let days_since_ce = days + 719468;
71
72 let era = if days_since_ce >= 0 {
73 days_since_ce
74 } else {
75 days_since_ce - 146096
76 } / 146097;
77 let doe = days_since_ce - era * 146097;
78 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
79 let y = yoe + era * 400;
80 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
81 let mp = (5 * doy + 2) / 153;
82 let d = doy - (153 * mp + 2) / 5 + 1;
83 let m = if mp < 10 {
84 mp + 3
85 } else {
86 mp - 9
87 };
88 let year = if m <= 2 {
89 y + 1
90 } else {
91 y
92 };
93
94 (year, m as u32, d as u32)
95 }
96}
97
98impl Date {
99 fn overflow_err(message: impl Into<String>) -> TypeError {
100 TypeError::Temporal {
101 kind: TemporalKind::DateOverflow {
102 message: message.into(),
103 },
104 message: "date overflow".to_string(),
105 fragment: Fragment::None,
106 }
107 }
108
109 pub fn new(year: i32, month: u32, day: u32) -> Option<Self> {
110 Self::ymd_to_days_since_epoch(year, month, day).map(|days_since_epoch| Self {
111 days_since_epoch,
112 })
113 }
114
115 pub fn from_ymd(year: i32, month: u32, day: u32) -> Result<Self, Box<TypeError>> {
116 Self::new(year, month, day).ok_or_else(|| {
117 Box::new(Self::overflow_err(format!("invalid date: {}-{:02}-{:02}", year, month, day)))
118 })
119 }
120
121 pub fn year(&self) -> i32 {
122 Self::days_since_epoch_to_ymd(self.days_since_epoch).0
123 }
124
125 pub fn month(&self) -> u32 {
126 Self::days_since_epoch_to_ymd(self.days_since_epoch).1
127 }
128
129 pub fn day(&self) -> u32 {
130 Self::days_since_epoch_to_ymd(self.days_since_epoch).2
131 }
132
133 pub fn to_days_since_epoch(&self) -> i32 {
134 self.days_since_epoch
135 }
136
137 pub fn from_days_since_epoch(days: i32) -> Option<Self> {
138 if !(-365_250_000..=365_250_000).contains(&days) {
139 return None;
140 }
141 Some(Self {
142 days_since_epoch: days,
143 })
144 }
145
146 pub fn saturating_add(self, rhs: Duration) -> Date {
147 const NANOS_PER_DAY: i128 = 86_400_000_000_000;
148 let total = rhs.as_nanos().unwrap_or(if rhs.is_negative() {
149 i64::MIN
150 } else {
151 i64::MAX
152 });
153 let days = (self.days_since_epoch as i128 + total as i128 / NANOS_PER_DAY)
154 .clamp(-365_250_000, 365_250_000);
155 Self {
156 days_since_epoch: days as i32,
157 }
158 }
159
160 pub fn saturating_sub(self, rhs: Duration) -> Date {
161 const NANOS_PER_DAY: i128 = 86_400_000_000_000;
162 let total = rhs.as_nanos().unwrap_or(if rhs.is_negative() {
163 i64::MIN
164 } else {
165 i64::MAX
166 });
167 let days = (self.days_since_epoch as i128 - total as i128 / NANOS_PER_DAY)
168 .clamp(-365_250_000, 365_250_000);
169 Self {
170 days_since_epoch: days as i32,
171 }
172 }
173}
174
175impl Display for Date {
176 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
177 let (year, month, day) = Self::days_since_epoch_to_ymd(self.days_since_epoch);
178 if year < 0 {
179 write!(f, "-{:04}-{:02}-{:02}", -year, month, day)
180 } else {
181 write!(f, "{:04}-{:02}-{:02}", year, month, day)
182 }
183 }
184}
185
186impl Serialize for Date {
187 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
188 where
189 S: Serializer,
190 {
191 serializer.serialize_i32(self.days_since_epoch)
192 }
193}
194
195struct DateVisitor;
196
197impl<'de> Visitor<'de> for DateVisitor {
198 type Value = Date;
199
200 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
201 formatter.write_str("a date as days since the Unix epoch (i32)")
202 }
203
204 fn visit_i32<E>(self, value: i32) -> Result<Date, E>
205 where
206 E: de::Error,
207 {
208 Date::from_days_since_epoch(value)
209 .ok_or_else(|| E::custom(format!("date days out of range: {}", value)))
210 }
211
212 fn visit_i64<E>(self, value: i64) -> Result<Date, E>
213 where
214 E: de::Error,
215 {
216 let days = i32::try_from(value).map_err(|_| E::custom(format!("date days out of range: {}", value)))?;
217 self.visit_i32(days)
218 }
219
220 fn visit_u64<E>(self, value: u64) -> Result<Date, E>
221 where
222 E: de::Error,
223 {
224 let days = i32::try_from(value).map_err(|_| E::custom(format!("date days out of range: {}", value)))?;
225 self.visit_i32(days)
226 }
227}
228
229impl<'de> Deserialize<'de> for Date {
230 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
231 where
232 D: Deserializer<'de>,
233 {
234 deserializer.deserialize_i32(DateVisitor)
235 }
236}
237
238#[cfg(test)]
239pub mod tests {
240 use std::fmt::Debug;
241
242 use postcard::{from_bytes, to_allocvec};
243 use serde_json::{from_str, to_string};
244
245 use super::*;
246 use crate::{
247 error::{TemporalKind, TypeError},
248 value::duration::Duration,
249 };
250
251 #[test]
252 fn test_date_display_standard_dates() {
253 let date = Date::new(2024, 3, 15).unwrap();
255 assert_eq!(format!("{}", date), "2024-03-15");
256
257 let date = Date::new(2000, 1, 1).unwrap();
258 assert_eq!(format!("{}", date), "2000-01-01");
259
260 let date = Date::new(1999, 12, 31).unwrap();
261 assert_eq!(format!("{}", date), "1999-12-31");
262 }
263
264 #[test]
265 fn test_date_display_edge_cases() {
266 let date = Date::new(1970, 1, 1).unwrap();
268 assert_eq!(format!("{}", date), "1970-01-01");
269
270 let date = Date::new(2024, 2, 29).unwrap();
272 assert_eq!(format!("{}", date), "2024-02-29");
273
274 let date = Date::new(2024, 1, 9).unwrap();
276 assert_eq!(format!("{}", date), "2024-01-09");
277
278 let date = Date::new(2024, 9, 1).unwrap();
279 assert_eq!(format!("{}", date), "2024-09-01");
280 }
281
282 #[test]
283 fn test_date_display_boundary_dates() {
284 let date = Date::new(1, 1, 1).unwrap();
286 assert_eq!(format!("{}", date), "0001-01-01");
287
288 let date = Date::new(9999, 12, 31).unwrap();
290 assert_eq!(format!("{}", date), "9999-12-31");
291
292 let date = Date::new(1900, 1, 1).unwrap();
294 assert_eq!(format!("{}", date), "1900-01-01");
295
296 let date = Date::new(2000, 1, 1).unwrap();
297 assert_eq!(format!("{}", date), "2000-01-01");
298
299 let date = Date::new(2100, 1, 1).unwrap();
300 assert_eq!(format!("{}", date), "2100-01-01");
301 }
302
303 #[test]
304 fn test_date_display_negative_years() {
305 let date = Date::new(0, 1, 1).unwrap();
307 assert_eq!(format!("{}", date), "0000-01-01");
308
309 let date = Date::new(-1, 1, 1).unwrap();
311 assert_eq!(format!("{}", date), "-0001-01-01");
312
313 let date = Date::new(-100, 12, 31).unwrap();
314 assert_eq!(format!("{}", date), "-0100-12-31");
315 }
316
317 #[test]
318 fn test_date_display_default() {
319 let date = Date::default();
320 assert_eq!(format!("{}", date), "1970-01-01");
321 }
322
323 #[test]
324 fn test_date_display_all_months() {
325 let months = [
326 (1, "01"),
327 (2, "02"),
328 (3, "03"),
329 (4, "04"),
330 (5, "05"),
331 (6, "06"),
332 (7, "07"),
333 (8, "08"),
334 (9, "09"),
335 (10, "10"),
336 (11, "11"),
337 (12, "12"),
338 ];
339
340 for (month, expected) in months {
341 let date = Date::new(2024, month, 15).unwrap();
342 assert_eq!(format!("{}", date), format!("2024-{}-15", expected));
343 }
344 }
345
346 #[test]
347 fn test_date_display_days_in_month() {
348 let test_cases = [
350 (2024, 1, 1, "2024-01-01"),
351 (2024, 1, 31, "2024-01-31"),
352 (2024, 2, 1, "2024-02-01"),
353 (2024, 2, 29, "2024-02-29"), (2024, 4, 1, "2024-04-01"),
355 (2024, 4, 30, "2024-04-30"),
356 (2024, 12, 1, "2024-12-01"),
357 (2024, 12, 31, "2024-12-31"),
358 ];
359
360 for (year, month, day, expected) in test_cases {
361 let date = Date::new(year, month, day).unwrap();
362 assert_eq!(format!("{}", date), expected);
363 }
364 }
365
366 #[test]
367 fn test_date_roundtrip() {
368 let test_dates = [
370 (1900, 1, 1),
371 (1970, 1, 1),
372 (2000, 2, 29), (2024, 12, 31),
374 (2100, 6, 15),
375 ];
376
377 for (year, month, day) in test_dates {
378 let date = Date::new(year, month, day).unwrap();
379 let days = date.to_days_since_epoch();
380 let recovered = Date::from_days_since_epoch(days).unwrap();
381
382 assert_eq!(date.year(), recovered.year());
383 assert_eq!(date.month(), recovered.month());
384 assert_eq!(date.day(), recovered.day());
385 }
386 }
387
388 #[test]
389 fn test_leap_year_detection() {
390 assert!(Date::is_leap_year(2000)); assert!(Date::is_leap_year(2024)); assert!(!Date::is_leap_year(1900)); assert!(!Date::is_leap_year(2023)); }
395
396 #[test]
397 fn test_invalid_dates() {
398 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()); }
405
406 #[test]
407 fn test_serde_roundtrip() {
408 let date = Date::new(2024, 3, 15).unwrap();
409 let json = to_string(&date).unwrap();
410 assert_eq!(json, date.to_days_since_epoch().to_string());
412
413 let recovered: Date = from_str(&json).unwrap();
414 assert_eq!(date, recovered);
415 }
416
417 #[test]
418 fn test_serde_postcard_roundtrip_negative_years() {
419 for (y, m, d) in [(-100, 12, 31), (0, 1, 1), (1970, 1, 1), (2024, 3, 15), (9999, 12, 31)] {
421 let date = Date::new(y, m, d).unwrap();
422 let bytes = to_allocvec(&date).unwrap();
423 let recovered: Date = from_bytes(&bytes).unwrap();
424 assert_eq!(date, recovered);
425 assert_eq!(recovered.year(), y);
426 assert_eq!(recovered.month(), m);
427 assert_eq!(recovered.day(), d);
428 }
429 }
430
431 #[test]
432 fn test_deserialize_rejects_out_of_range_days() {
433 let json = 400_000_000i64.to_string();
435 assert!(from_str::<Date>(&json).is_err());
436 }
437
438 fn assert_date_overflow<T: Debug>(result: Result<T, Box<TypeError>>) {
439 let err = result.expect_err("expected DateOverflow error");
440 match *err {
441 TypeError::Temporal {
442 kind: TemporalKind::DateOverflow {
443 ..
444 },
445 ..
446 } => {}
447 other => panic!("expected DateOverflow, got: {:?}", other),
448 }
449 }
450
451 #[test]
452 fn test_from_ymd_invalid_month() {
453 assert_date_overflow(Date::from_ymd(2024, 0, 1));
454 assert_date_overflow(Date::from_ymd(2024, 13, 1));
455 }
456
457 #[test]
458 fn test_from_ymd_invalid_day() {
459 assert_date_overflow(Date::from_ymd(2024, 1, 0));
460 assert_date_overflow(Date::from_ymd(2024, 1, 32));
461 }
462
463 #[test]
464 fn test_from_ymd_non_leap_year() {
465 assert_date_overflow(Date::from_ymd(2023, 2, 29));
466 }
467
468 #[test]
469 fn saturating_add_sub_whole_days() {
470 let base = Date::from_ymd(2024, 1, 15).unwrap();
472
473 let forward = base.saturating_add(Duration::from_days(2).unwrap());
474 assert_eq!(forward, Date::from_ymd(2024, 1, 17).unwrap());
475
476 let backward = base.saturating_sub(Duration::from_days(2).unwrap());
477 assert_eq!(backward, Date::from_ymd(2024, 1, 13).unwrap());
478 }
479
480 #[test]
481 fn saturating_sub_day_truncates() {
482 let base = Date::from_ymd(2024, 1, 15).unwrap();
486
487 let half_day = base.saturating_add(Duration::from_seconds(12 * 3600).unwrap());
488 assert_eq!(half_day, base, "12h is sub-day and must not change the date");
489
490 let day_and_half = base.saturating_add(Duration::from_seconds(36 * 3600).unwrap());
491 assert_eq!(day_and_half, Date::from_ymd(2024, 1, 16).unwrap(), "36h truncates to 1 whole day");
492 }
493
494 #[test]
495 fn saturating_add_clamps_at_max() {
496 let max = Date::from_days_since_epoch(365_250_000).unwrap();
498 let clamped = max.saturating_add(Duration::from_days(10).unwrap());
499 assert_eq!(clamped.to_days_since_epoch(), 365_250_000);
500 }
501
502 #[test]
503 fn saturating_sub_clamps_at_min() {
504 let min = Date::from_days_since_epoch(-365_250_000).unwrap();
506 let clamped = min.saturating_sub(Duration::from_days(10).unwrap());
507 assert_eq!(clamped.to_days_since_epoch(), -365_250_000);
508 }
509}