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)]
25#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
26pub struct Date {
27 days_since_epoch: i32,
30}
31
32impl Date {
34 #[inline]
36 pub fn is_leap_year(year: i32) -> bool {
37 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
38 }
39
40 #[inline]
42 pub fn days_in_month(year: i32, month: u32) -> u32 {
43 match month {
44 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
45 4 | 6 | 9 | 11 => 30,
46 2 => {
47 if Self::is_leap_year(year) {
48 29
49 } else {
50 28
51 }
52 }
53 _ => 0,
54 }
55 }
56
57 fn ymd_to_days_since_epoch(year: i32, month: u32, day: u32) -> Option<i32> {
59 if !(1..=12).contains(&month) || day < 1 || day > Self::days_in_month(year, month) {
61 return None;
62 }
63
64 let (y, m) = if month <= 2 {
67 (year - 1, month as i32 + 9) } else {
69 (year, month as i32 - 3) };
71
72 let era = if y >= 0 {
73 y
74 } else {
75 y - 399
76 } / 400;
77 let yoe = y - era * 400; let doy = (153 * m + 2) / 5 + day as i32 - 1; let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; let days = era * 146097 + doe - 719468;
81
82 Some(days)
83 }
84
85 fn days_since_epoch_to_ymd(days: i32) -> (i32, u32, u32) {
87 let days_since_ce = days + 719468;
89
90 let era = if days_since_ce >= 0 {
91 days_since_ce
92 } else {
93 days_since_ce - 146096
94 } / 146097;
95 let doe = days_since_ce - era * 146097; let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; let y = yoe + era * 400;
98 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let d = doy - (153 * mp + 2) / 5 + 1; let m = if mp < 10 {
102 mp + 3
103 } else {
104 mp - 9
105 }; let year = if m <= 2 {
107 y + 1
108 } else {
109 y
110 };
111
112 (year, m as u32, d as u32)
113 }
114}
115
116impl Date {
117 fn overflow_err(message: impl Into<String>) -> TypeError {
118 TypeError::Temporal {
119 kind: TemporalKind::DateOverflow {
120 message: message.into(),
121 },
122 message: "date overflow".to_string(),
123 fragment: Fragment::None,
124 }
125 }
126
127 pub fn new(year: i32, month: u32, day: u32) -> Option<Self> {
128 Self::ymd_to_days_since_epoch(year, month, day).map(|days_since_epoch| Self {
129 days_since_epoch,
130 })
131 }
132
133 pub fn from_ymd(year: i32, month: u32, day: u32) -> Result<Self, Box<TypeError>> {
134 Self::new(year, month, day).ok_or_else(|| {
135 Box::new(Self::overflow_err(format!("invalid date: {}-{:02}-{:02}", year, month, day)))
136 })
137 }
138
139 pub fn year(&self) -> i32 {
140 Self::days_since_epoch_to_ymd(self.days_since_epoch).0
141 }
142
143 pub fn month(&self) -> u32 {
144 Self::days_since_epoch_to_ymd(self.days_since_epoch).1
145 }
146
147 pub fn day(&self) -> u32 {
148 Self::days_since_epoch_to_ymd(self.days_since_epoch).2
149 }
150
151 pub fn to_days_since_epoch(&self) -> i32 {
153 self.days_since_epoch
154 }
155
156 pub fn from_days_since_epoch(days: i32) -> Option<Self> {
158 if !(-365_250_000..=365_250_000).contains(&days) {
161 return None;
162 }
163 Some(Self {
164 days_since_epoch: days,
165 })
166 }
167}
168
169impl Display for Date {
170 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
171 let (year, month, day) = Self::days_since_epoch_to_ymd(self.days_since_epoch);
172 if year < 0 {
173 write!(f, "-{:04}-{:02}-{:02}", -year, month, day)
174 } else {
175 write!(f, "{:04}-{:02}-{:02}", year, month, day)
176 }
177 }
178}
179
180impl Serialize for Date {
182 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
183 where
184 S: Serializer,
185 {
186 serializer.serialize_str(&self.to_string())
187 }
188}
189
190struct DateVisitor;
191
192impl<'de> Visitor<'de> for DateVisitor {
193 type Value = Date;
194
195 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
196 formatter.write_str("a date in ISO 8601 format (YYYY-MM-DD)")
197 }
198
199 fn visit_str<E>(self, value: &str) -> Result<Date, E>
200 where
201 E: de::Error,
202 {
203 let parts: Vec<&str> = value.split('-').collect();
205
206 if parts.len() != 3 {
207 return Err(E::custom(format!("invalid date format: {}", value)));
208 }
209
210 let (year_str, month_str, day_str) = if parts[0].is_empty() && parts.len() == 4 {
212 (format!("-{}", parts[1]), parts[2], parts[3])
215 } else {
216 (parts[0].to_string(), parts[1], parts[2])
217 };
218
219 let year = year_str.parse::<i32>().map_err(|_| E::custom(format!("invalid year: {}", year_str)))?;
220 let month = month_str.parse::<u32>().map_err(|_| E::custom(format!("invalid month: {}", month_str)))?;
221 let day = day_str.parse::<u32>().map_err(|_| E::custom(format!("invalid day: {}", day_str)))?;
222
223 Date::new(year, month, day)
224 .ok_or_else(|| E::custom(format!("invalid date: {}-{:02}-{:02}", year, month, day)))
225 }
226}
227
228impl<'de> Deserialize<'de> for Date {
229 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
230 where
231 D: Deserializer<'de>,
232 {
233 deserializer.deserialize_str(DateVisitor)
234 }
235}
236
237#[cfg(test)]
238pub mod tests {
239 use std::fmt::Debug;
240
241 use serde_json::{from_str, to_string};
242
243 use super::*;
244 use crate::error::{TemporalKind, TypeError};
245
246 #[test]
247 fn test_date_display_standard_dates() {
248 let date = Date::new(2024, 3, 15).unwrap();
250 assert_eq!(format!("{}", date), "2024-03-15");
251
252 let date = Date::new(2000, 1, 1).unwrap();
253 assert_eq!(format!("{}", date), "2000-01-01");
254
255 let date = Date::new(1999, 12, 31).unwrap();
256 assert_eq!(format!("{}", date), "1999-12-31");
257 }
258
259 #[test]
260 fn test_date_display_edge_cases() {
261 let date = Date::new(1970, 1, 1).unwrap();
263 assert_eq!(format!("{}", date), "1970-01-01");
264
265 let date = Date::new(2024, 2, 29).unwrap();
267 assert_eq!(format!("{}", date), "2024-02-29");
268
269 let date = Date::new(2024, 1, 9).unwrap();
271 assert_eq!(format!("{}", date), "2024-01-09");
272
273 let date = Date::new(2024, 9, 1).unwrap();
274 assert_eq!(format!("{}", date), "2024-09-01");
275 }
276
277 #[test]
278 fn test_date_display_boundary_dates() {
279 let date = Date::new(1, 1, 1).unwrap();
281 assert_eq!(format!("{}", date), "0001-01-01");
282
283 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();
289 assert_eq!(format!("{}", date), "1900-01-01");
290
291 let date = Date::new(2000, 1, 1).unwrap();
292 assert_eq!(format!("{}", date), "2000-01-01");
293
294 let date = Date::new(2100, 1, 1).unwrap();
295 assert_eq!(format!("{}", date), "2100-01-01");
296 }
297
298 #[test]
299 fn test_date_display_negative_years() {
300 let date = Date::new(0, 1, 1).unwrap();
302 assert_eq!(format!("{}", date), "0000-01-01");
303
304 let date = Date::new(-1, 1, 1).unwrap();
306 assert_eq!(format!("{}", date), "-0001-01-01");
307
308 let date = Date::new(-100, 12, 31).unwrap();
309 assert_eq!(format!("{}", date), "-0100-12-31");
310 }
311
312 #[test]
313 fn test_date_display_default() {
314 let date = Date::default();
315 assert_eq!(format!("{}", date), "1970-01-01");
316 }
317
318 #[test]
319 fn test_date_display_all_months() {
320 let months = [
321 (1, "01"),
322 (2, "02"),
323 (3, "03"),
324 (4, "04"),
325 (5, "05"),
326 (6, "06"),
327 (7, "07"),
328 (8, "08"),
329 (9, "09"),
330 (10, "10"),
331 (11, "11"),
332 (12, "12"),
333 ];
334
335 for (month, expected) in months {
336 let date = Date::new(2024, month, 15).unwrap();
337 assert_eq!(format!("{}", date), format!("2024-{}-15", expected));
338 }
339 }
340
341 #[test]
342 fn test_date_display_days_in_month() {
343 let test_cases = [
345 (2024, 1, 1, "2024-01-01"),
346 (2024, 1, 31, "2024-01-31"),
347 (2024, 2, 1, "2024-02-01"),
348 (2024, 2, 29, "2024-02-29"), (2024, 4, 1, "2024-04-01"),
350 (2024, 4, 30, "2024-04-30"),
351 (2024, 12, 1, "2024-12-01"),
352 (2024, 12, 31, "2024-12-31"),
353 ];
354
355 for (year, month, day, expected) in test_cases {
356 let date = Date::new(year, month, day).unwrap();
357 assert_eq!(format!("{}", date), expected);
358 }
359 }
360
361 #[test]
362 fn test_date_roundtrip() {
363 let test_dates = [
365 (1900, 1, 1),
366 (1970, 1, 1),
367 (2000, 2, 29), (2024, 12, 31),
369 (2100, 6, 15),
370 ];
371
372 for (year, month, day) in test_dates {
373 let date = Date::new(year, month, day).unwrap();
374 let days = date.to_days_since_epoch();
375 let recovered = Date::from_days_since_epoch(days).unwrap();
376
377 assert_eq!(date.year(), recovered.year());
378 assert_eq!(date.month(), recovered.month());
379 assert_eq!(date.day(), recovered.day());
380 }
381 }
382
383 #[test]
384 fn test_leap_year_detection() {
385 assert!(Date::is_leap_year(2000)); assert!(Date::is_leap_year(2024)); assert!(!Date::is_leap_year(1900)); assert!(!Date::is_leap_year(2023)); }
390
391 #[test]
392 fn test_invalid_dates() {
393 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()); }
400
401 #[test]
402 fn test_serde_roundtrip() {
403 let date = Date::new(2024, 3, 15).unwrap();
404 let json = to_string(&date).unwrap();
405 assert_eq!(json, "\"2024-03-15\"");
406
407 let recovered: Date = from_str(&json).unwrap();
408 assert_eq!(date, recovered);
409 }
410
411 fn assert_date_overflow<T: Debug>(result: Result<T, Box<TypeError>>) {
412 let err = result.expect_err("expected DateOverflow error");
413 match *err {
414 TypeError::Temporal {
415 kind: TemporalKind::DateOverflow {
416 ..
417 },
418 ..
419 } => {}
420 other => panic!("expected DateOverflow, got: {:?}", other),
421 }
422 }
423
424 #[test]
425 fn test_from_ymd_invalid_month() {
426 assert_date_overflow(Date::from_ymd(2024, 0, 1));
427 assert_date_overflow(Date::from_ymd(2024, 13, 1));
428 }
429
430 #[test]
431 fn test_from_ymd_invalid_day() {
432 assert_date_overflow(Date::from_ymd(2024, 1, 0));
433 assert_date_overflow(Date::from_ymd(2024, 1, 32));
434 }
435
436 #[test]
437 fn test_from_ymd_non_leap_year() {
438 assert_date_overflow(Date::from_ymd(2023, 2, 29));
439 }
440}