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_str(&self.to_string())
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 in ISO 8601 format (YYYY-MM-DD)")
173 }
174
175 fn visit_str<E>(self, value: &str) -> Result<Date, E>
176 where
177 E: de::Error,
178 {
179 let parts: Vec<&str> = value.split('-').collect();
180
181 if parts.len() != 3 {
182 return Err(E::custom(format!("invalid date format: {}", value)));
183 }
184
185 let (year_str, month_str, day_str) = if parts[0].is_empty() && parts.len() == 4 {
186 (format!("-{}", parts[1]), parts[2], parts[3])
187 } else {
188 (parts[0].to_string(), parts[1], parts[2])
189 };
190
191 let year = year_str.parse::<i32>().map_err(|_| E::custom(format!("invalid year: {}", year_str)))?;
192 let month = month_str.parse::<u32>().map_err(|_| E::custom(format!("invalid month: {}", month_str)))?;
193 let day = day_str.parse::<u32>().map_err(|_| E::custom(format!("invalid day: {}", day_str)))?;
194
195 Date::new(year, month, day)
196 .ok_or_else(|| E::custom(format!("invalid date: {}-{:02}-{:02}", year, month, day)))
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_str(DateVisitor)
206 }
207}
208
209#[cfg(test)]
210pub mod tests {
211 use std::fmt::Debug;
212
213 use serde_json::{from_str, to_string};
214
215 use super::*;
216 use crate::error::{TemporalKind, TypeError};
217
218 #[test]
219 fn test_date_display_standard_dates() {
220 let date = Date::new(2024, 3, 15).unwrap();
222 assert_eq!(format!("{}", date), "2024-03-15");
223
224 let date = Date::new(2000, 1, 1).unwrap();
225 assert_eq!(format!("{}", date), "2000-01-01");
226
227 let date = Date::new(1999, 12, 31).unwrap();
228 assert_eq!(format!("{}", date), "1999-12-31");
229 }
230
231 #[test]
232 fn test_date_display_edge_cases() {
233 let date = Date::new(1970, 1, 1).unwrap();
235 assert_eq!(format!("{}", date), "1970-01-01");
236
237 let date = Date::new(2024, 2, 29).unwrap();
239 assert_eq!(format!("{}", date), "2024-02-29");
240
241 let date = Date::new(2024, 1, 9).unwrap();
243 assert_eq!(format!("{}", date), "2024-01-09");
244
245 let date = Date::new(2024, 9, 1).unwrap();
246 assert_eq!(format!("{}", date), "2024-09-01");
247 }
248
249 #[test]
250 fn test_date_display_boundary_dates() {
251 let date = Date::new(1, 1, 1).unwrap();
253 assert_eq!(format!("{}", date), "0001-01-01");
254
255 let date = Date::new(9999, 12, 31).unwrap();
257 assert_eq!(format!("{}", date), "9999-12-31");
258
259 let date = Date::new(1900, 1, 1).unwrap();
261 assert_eq!(format!("{}", date), "1900-01-01");
262
263 let date = Date::new(2000, 1, 1).unwrap();
264 assert_eq!(format!("{}", date), "2000-01-01");
265
266 let date = Date::new(2100, 1, 1).unwrap();
267 assert_eq!(format!("{}", date), "2100-01-01");
268 }
269
270 #[test]
271 fn test_date_display_negative_years() {
272 let date = Date::new(0, 1, 1).unwrap();
274 assert_eq!(format!("{}", date), "0000-01-01");
275
276 let date = Date::new(-1, 1, 1).unwrap();
278 assert_eq!(format!("{}", date), "-0001-01-01");
279
280 let date = Date::new(-100, 12, 31).unwrap();
281 assert_eq!(format!("{}", date), "-0100-12-31");
282 }
283
284 #[test]
285 fn test_date_display_default() {
286 let date = Date::default();
287 assert_eq!(format!("{}", date), "1970-01-01");
288 }
289
290 #[test]
291 fn test_date_display_all_months() {
292 let months = [
293 (1, "01"),
294 (2, "02"),
295 (3, "03"),
296 (4, "04"),
297 (5, "05"),
298 (6, "06"),
299 (7, "07"),
300 (8, "08"),
301 (9, "09"),
302 (10, "10"),
303 (11, "11"),
304 (12, "12"),
305 ];
306
307 for (month, expected) in months {
308 let date = Date::new(2024, month, 15).unwrap();
309 assert_eq!(format!("{}", date), format!("2024-{}-15", expected));
310 }
311 }
312
313 #[test]
314 fn test_date_display_days_in_month() {
315 let test_cases = [
317 (2024, 1, 1, "2024-01-01"),
318 (2024, 1, 31, "2024-01-31"),
319 (2024, 2, 1, "2024-02-01"),
320 (2024, 2, 29, "2024-02-29"), (2024, 4, 1, "2024-04-01"),
322 (2024, 4, 30, "2024-04-30"),
323 (2024, 12, 1, "2024-12-01"),
324 (2024, 12, 31, "2024-12-31"),
325 ];
326
327 for (year, month, day, expected) in test_cases {
328 let date = Date::new(year, month, day).unwrap();
329 assert_eq!(format!("{}", date), expected);
330 }
331 }
332
333 #[test]
334 fn test_date_roundtrip() {
335 let test_dates = [
337 (1900, 1, 1),
338 (1970, 1, 1),
339 (2000, 2, 29), (2024, 12, 31),
341 (2100, 6, 15),
342 ];
343
344 for (year, month, day) in test_dates {
345 let date = Date::new(year, month, day).unwrap();
346 let days = date.to_days_since_epoch();
347 let recovered = Date::from_days_since_epoch(days).unwrap();
348
349 assert_eq!(date.year(), recovered.year());
350 assert_eq!(date.month(), recovered.month());
351 assert_eq!(date.day(), recovered.day());
352 }
353 }
354
355 #[test]
356 fn test_leap_year_detection() {
357 assert!(Date::is_leap_year(2000)); assert!(Date::is_leap_year(2024)); assert!(!Date::is_leap_year(1900)); assert!(!Date::is_leap_year(2023)); }
362
363 #[test]
364 fn test_invalid_dates() {
365 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()); }
372
373 #[test]
374 fn test_serde_roundtrip() {
375 let date = Date::new(2024, 3, 15).unwrap();
376 let json = to_string(&date).unwrap();
377 assert_eq!(json, "\"2024-03-15\"");
378
379 let recovered: Date = from_str(&json).unwrap();
380 assert_eq!(date, recovered);
381 }
382
383 fn assert_date_overflow<T: Debug>(result: Result<T, Box<TypeError>>) {
384 let err = result.expect_err("expected DateOverflow error");
385 match *err {
386 TypeError::Temporal {
387 kind: TemporalKind::DateOverflow {
388 ..
389 },
390 ..
391 } => {}
392 other => panic!("expected DateOverflow, got: {:?}", other),
393 }
394 }
395
396 #[test]
397 fn test_from_ymd_invalid_month() {
398 assert_date_overflow(Date::from_ymd(2024, 0, 1));
399 assert_date_overflow(Date::from_ymd(2024, 13, 1));
400 }
401
402 #[test]
403 fn test_from_ymd_invalid_day() {
404 assert_date_overflow(Date::from_ymd(2024, 1, 0));
405 assert_date_overflow(Date::from_ymd(2024, 1, 32));
406 }
407
408 #[test]
409 fn test_from_ymd_non_leap_year() {
410 assert_date_overflow(Date::from_ymd(2023, 2, 29));
411 }
412}