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();
254 assert_eq!(format!("{}", date), "2024-03-15");
255
256 let date = Date::new(2000, 1, 1).unwrap();
257 assert_eq!(format!("{}", date), "2000-01-01");
258
259 let date = Date::new(1999, 12, 31).unwrap();
260 assert_eq!(format!("{}", date), "1999-12-31");
261 }
262
263 #[test]
264 fn test_date_display_edge_cases() {
265 let date = Date::new(1970, 1, 1).unwrap();
266 assert_eq!(format!("{}", date), "1970-01-01");
267
268 let date = Date::new(2024, 2, 29).unwrap();
269 assert_eq!(format!("{}", date), "2024-02-29");
270
271 let date = Date::new(2024, 1, 9).unwrap();
273 assert_eq!(format!("{}", date), "2024-01-09");
274
275 let date = Date::new(2024, 9, 1).unwrap();
276 assert_eq!(format!("{}", date), "2024-09-01");
277 }
278
279 #[test]
280 fn test_date_display_boundary_dates() {
281 let date = Date::new(1, 1, 1).unwrap();
282 assert_eq!(format!("{}", date), "0001-01-01");
283
284 let date = Date::new(9999, 12, 31).unwrap();
285 assert_eq!(format!("{}", date), "9999-12-31");
286
287 let date = Date::new(1900, 1, 1).unwrap();
288 assert_eq!(format!("{}", date), "1900-01-01");
289
290 let date = Date::new(2000, 1, 1).unwrap();
291 assert_eq!(format!("{}", date), "2000-01-01");
292
293 let date = Date::new(2100, 1, 1).unwrap();
294 assert_eq!(format!("{}", date), "2100-01-01");
295 }
296
297 #[test]
298 fn test_date_display_negative_years() {
299 let date = Date::new(0, 1, 1).unwrap();
301 assert_eq!(format!("{}", date), "0000-01-01");
302
303 let date = Date::new(-1, 1, 1).unwrap();
304 assert_eq!(format!("{}", date), "-0001-01-01");
305
306 let date = Date::new(-100, 12, 31).unwrap();
307 assert_eq!(format!("{}", date), "-0100-12-31");
308 }
309
310 #[test]
311 fn test_date_display_default() {
312 let date = Date::default();
313 assert_eq!(format!("{}", date), "1970-01-01");
314 }
315
316 #[test]
317 fn test_date_display_all_months() {
318 let months = [
319 (1, "01"),
320 (2, "02"),
321 (3, "03"),
322 (4, "04"),
323 (5, "05"),
324 (6, "06"),
325 (7, "07"),
326 (8, "08"),
327 (9, "09"),
328 (10, "10"),
329 (11, "11"),
330 (12, "12"),
331 ];
332
333 for (month, expected) in months {
334 let date = Date::new(2024, month, 15).unwrap();
335 assert_eq!(format!("{}", date), format!("2024-{}-15", expected));
336 }
337 }
338
339 #[test]
340 fn test_date_display_days_in_month() {
341 let test_cases = [
342 (2024, 1, 1, "2024-01-01"),
343 (2024, 1, 31, "2024-01-31"),
344 (2024, 2, 1, "2024-02-01"),
345 (2024, 2, 29, "2024-02-29"), (2024, 4, 1, "2024-04-01"),
347 (2024, 4, 30, "2024-04-30"),
348 (2024, 12, 1, "2024-12-01"),
349 (2024, 12, 31, "2024-12-31"),
350 ];
351
352 for (year, month, day, expected) in test_cases {
353 let date = Date::new(year, month, day).unwrap();
354 assert_eq!(format!("{}", date), expected);
355 }
356 }
357
358 #[test]
359 fn test_date_roundtrip() {
360 let test_dates = [
361 (1900, 1, 1),
362 (1970, 1, 1),
363 (2000, 2, 29), (2024, 12, 31),
365 (2100, 6, 15),
366 ];
367
368 for (year, month, day) in test_dates {
369 let date = Date::new(year, month, day).unwrap();
370 let days = date.to_days_since_epoch();
371 let recovered = Date::from_days_since_epoch(days).unwrap();
372
373 assert_eq!(date.year(), recovered.year());
374 assert_eq!(date.month(), recovered.month());
375 assert_eq!(date.day(), recovered.day());
376 }
377 }
378
379 #[test]
380 fn test_leap_year_detection() {
381 assert!(Date::is_leap_year(2000)); assert!(Date::is_leap_year(2024)); assert!(!Date::is_leap_year(1900)); assert!(!Date::is_leap_year(2023)); }
386
387 #[test]
388 fn test_invalid_dates() {
389 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()); }
396
397 #[test]
398 fn test_serde_roundtrip() {
399 let date = Date::new(2024, 3, 15).unwrap();
400 let json = to_string(&date).unwrap();
401 assert_eq!(json, date.to_days_since_epoch().to_string());
403
404 let recovered: Date = from_str(&json).unwrap();
405 assert_eq!(date, recovered);
406 }
407
408 #[test]
409 fn test_serde_postcard_roundtrip_negative_years() {
410 for (y, m, d) in [(-100, 12, 31), (0, 1, 1), (1970, 1, 1), (2024, 3, 15), (9999, 12, 31)] {
412 let date = Date::new(y, m, d).unwrap();
413 let bytes = to_allocvec(&date).unwrap();
414 let recovered: Date = from_bytes(&bytes).unwrap();
415 assert_eq!(date, recovered);
416 assert_eq!(recovered.year(), y);
417 assert_eq!(recovered.month(), m);
418 assert_eq!(recovered.day(), d);
419 }
420 }
421
422 #[test]
423 fn test_deserialize_rejects_out_of_range_days() {
424 let json = 400_000_000i64.to_string();
426 assert!(from_str::<Date>(&json).is_err());
427 }
428
429 fn assert_date_overflow<T: Debug>(result: Result<T, Box<TypeError>>) {
430 let err = result.expect_err("expected DateOverflow error");
431 match *err {
432 TypeError::Temporal {
433 kind: TemporalKind::DateOverflow {
434 ..
435 },
436 ..
437 } => {}
438 other => panic!("expected DateOverflow, got: {:?}", other),
439 }
440 }
441
442 #[test]
443 fn test_from_ymd_invalid_month() {
444 assert_date_overflow(Date::from_ymd(2024, 0, 1));
445 assert_date_overflow(Date::from_ymd(2024, 13, 1));
446 }
447
448 #[test]
449 fn test_from_ymd_invalid_day() {
450 assert_date_overflow(Date::from_ymd(2024, 1, 0));
451 assert_date_overflow(Date::from_ymd(2024, 1, 32));
452 }
453
454 #[test]
455 fn test_from_ymd_non_leap_year() {
456 assert_date_overflow(Date::from_ymd(2023, 2, 29));
457 }
458
459 #[test]
460 fn saturating_add_sub_whole_days() {
461 let base = Date::from_ymd(2024, 1, 15).unwrap();
463
464 let forward = base.saturating_add(Duration::from_days(2).unwrap());
465 assert_eq!(forward, Date::from_ymd(2024, 1, 17).unwrap());
466
467 let backward = base.saturating_sub(Duration::from_days(2).unwrap());
468 assert_eq!(backward, Date::from_ymd(2024, 1, 13).unwrap());
469 }
470
471 #[test]
472 fn saturating_sub_day_truncates() {
473 let base = Date::from_ymd(2024, 1, 15).unwrap();
475
476 let half_day = base.saturating_add(Duration::from_seconds(12 * 3600).unwrap());
477 assert_eq!(half_day, base, "12h is sub-day and must not change the date");
478
479 let day_and_half = base.saturating_add(Duration::from_seconds(36 * 3600).unwrap());
480 assert_eq!(day_and_half, Date::from_ymd(2024, 1, 16).unwrap(), "36h truncates to 1 whole day");
481 }
482
483 #[test]
484 fn saturating_add_clamps_at_max() {
485 let max = Date::from_days_since_epoch(365_250_000).unwrap();
487 let clamped = max.saturating_add(Duration::from_days(10).unwrap());
488 assert_eq!(clamped.to_days_since_epoch(), 365_250_000);
489 }
490
491 #[test]
492 fn saturating_sub_clamps_at_min() {
493 let min = Date::from_days_since_epoch(-365_250_000).unwrap();
495 let clamped = min.saturating_sub(Duration::from_days(10).unwrap());
496 assert_eq!(clamped.to_days_since_epoch(), -365_250_000);
497 }
498}