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#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
21pub struct Date {
22 days_since_epoch: i32,
25}
26
27impl Date {
29 #[inline]
31 pub fn is_leap_year(year: i32) -> bool {
32 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
33 }
34
35 #[inline]
37 pub fn days_in_month(year: i32, month: u32) -> u32 {
38 match month {
39 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
40 4 | 6 | 9 | 11 => 30,
41 2 => {
42 if Self::is_leap_year(year) {
43 29
44 } else {
45 28
46 }
47 }
48 _ => 0,
49 }
50 }
51
52 fn ymd_to_days_since_epoch(year: i32, month: u32, day: u32) -> Option<i32> {
54 if !(1..=12).contains(&month) || day < 1 || day > Self::days_in_month(year, month) {
56 return None;
57 }
58
59 let (y, m) = if month <= 2 {
62 (year - 1, month as i32 + 9) } else {
64 (year, month as i32 - 3) };
66
67 let era = if y >= 0 {
68 y
69 } else {
70 y - 399
71 } / 400;
72 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;
76
77 Some(days)
78 }
79
80 fn days_since_epoch_to_ymd(days: i32) -> (i32, u32, u32) {
82 let days_since_ce = days + 719468;
84
85 let era = if days_since_ce >= 0 {
86 days_since_ce
87 } else {
88 days_since_ce - 146096
89 } / 146097;
90 let doe = days_since_ce - era * 146097; let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; let y = yoe + era * 400;
93 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 {
97 mp + 3
98 } else {
99 mp - 9
100 }; let year = if m <= 2 {
102 y + 1
103 } else {
104 y
105 };
106
107 (year, m as u32, d as u32)
108 }
109}
110
111impl Date {
112 fn overflow_err(message: impl Into<String>) -> TypeError {
113 TypeError::Temporal {
114 kind: TemporalKind::DateOverflow {
115 message: message.into(),
116 },
117 message: "date overflow".to_string(),
118 fragment: Fragment::None,
119 }
120 }
121
122 pub fn new(year: i32, month: u32, day: u32) -> Option<Self> {
123 Self::ymd_to_days_since_epoch(year, month, day).map(|days_since_epoch| Self {
124 days_since_epoch,
125 })
126 }
127
128 pub fn from_ymd(year: i32, month: u32, day: u32) -> Result<Self, Box<TypeError>> {
129 Self::new(year, month, day).ok_or_else(|| {
130 Box::new(Self::overflow_err(format!("invalid date: {}-{:02}-{:02}", year, month, day)))
131 })
132 }
133
134 pub fn year(&self) -> i32 {
135 Self::days_since_epoch_to_ymd(self.days_since_epoch).0
136 }
137
138 pub fn month(&self) -> u32 {
139 Self::days_since_epoch_to_ymd(self.days_since_epoch).1
140 }
141
142 pub fn day(&self) -> u32 {
143 Self::days_since_epoch_to_ymd(self.days_since_epoch).2
144 }
145
146 pub fn to_days_since_epoch(&self) -> i32 {
148 self.days_since_epoch
149 }
150
151 pub fn from_days_since_epoch(days: i32) -> Option<Self> {
153 if !(-365_250_000..=365_250_000).contains(&days) {
156 return None;
157 }
158 Some(Self {
159 days_since_epoch: days,
160 })
161 }
162}
163
164impl Display for Date {
165 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
166 let (year, month, day) = Self::days_since_epoch_to_ymd(self.days_since_epoch);
167 if year < 0 {
168 write!(f, "-{:04}-{:02}-{:02}", -year, month, day)
169 } else {
170 write!(f, "{:04}-{:02}-{:02}", year, month, day)
171 }
172 }
173}
174
175impl Serialize for Date {
177 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
178 where
179 S: Serializer,
180 {
181 serializer.serialize_str(&self.to_string())
182 }
183}
184
185struct DateVisitor;
186
187impl<'de> Visitor<'de> for DateVisitor {
188 type Value = Date;
189
190 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
191 formatter.write_str("a date in ISO 8601 format (YYYY-MM-DD)")
192 }
193
194 fn visit_str<E>(self, value: &str) -> Result<Date, E>
195 where
196 E: de::Error,
197 {
198 let parts: Vec<&str> = value.split('-').collect();
200
201 if parts.len() != 3 {
202 return Err(E::custom(format!("invalid date format: {}", value)));
203 }
204
205 let (year_str, month_str, day_str) = if parts[0].is_empty() && parts.len() == 4 {
207 (format!("-{}", parts[1]), parts[2], parts[3])
210 } else {
211 (parts[0].to_string(), parts[1], parts[2])
212 };
213
214 let year = year_str.parse::<i32>().map_err(|_| E::custom(format!("invalid year: {}", year_str)))?;
215 let month = month_str.parse::<u32>().map_err(|_| E::custom(format!("invalid month: {}", month_str)))?;
216 let day = day_str.parse::<u32>().map_err(|_| E::custom(format!("invalid day: {}", day_str)))?;
217
218 Date::new(year, month, day)
219 .ok_or_else(|| E::custom(format!("invalid date: {}-{:02}-{:02}", year, month, day)))
220 }
221}
222
223impl<'de> Deserialize<'de> for Date {
224 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
225 where
226 D: Deserializer<'de>,
227 {
228 deserializer.deserialize_str(DateVisitor)
229 }
230}
231
232#[cfg(test)]
233pub mod tests {
234 use std::fmt::Debug;
235
236 use serde_json::{from_str, to_string};
237
238 use super::*;
239 use crate::error::{TemporalKind, TypeError};
240
241 #[test]
242 fn test_date_display_standard_dates() {
243 let date = Date::new(2024, 3, 15).unwrap();
245 assert_eq!(format!("{}", date), "2024-03-15");
246
247 let date = Date::new(2000, 1, 1).unwrap();
248 assert_eq!(format!("{}", date), "2000-01-01");
249
250 let date = Date::new(1999, 12, 31).unwrap();
251 assert_eq!(format!("{}", date), "1999-12-31");
252 }
253
254 #[test]
255 fn test_date_display_edge_cases() {
256 let date = Date::new(1970, 1, 1).unwrap();
258 assert_eq!(format!("{}", date), "1970-01-01");
259
260 let date = Date::new(2024, 2, 29).unwrap();
262 assert_eq!(format!("{}", date), "2024-02-29");
263
264 let date = Date::new(2024, 1, 9).unwrap();
266 assert_eq!(format!("{}", date), "2024-01-09");
267
268 let date = Date::new(2024, 9, 1).unwrap();
269 assert_eq!(format!("{}", date), "2024-09-01");
270 }
271
272 #[test]
273 fn test_date_display_boundary_dates() {
274 let date = Date::new(1, 1, 1).unwrap();
276 assert_eq!(format!("{}", date), "0001-01-01");
277
278 let date = Date::new(9999, 12, 31).unwrap();
280 assert_eq!(format!("{}", date), "9999-12-31");
281
282 let date = Date::new(1900, 1, 1).unwrap();
284 assert_eq!(format!("{}", date), "1900-01-01");
285
286 let date = Date::new(2000, 1, 1).unwrap();
287 assert_eq!(format!("{}", date), "2000-01-01");
288
289 let date = Date::new(2100, 1, 1).unwrap();
290 assert_eq!(format!("{}", date), "2100-01-01");
291 }
292
293 #[test]
294 fn test_date_display_negative_years() {
295 let date = Date::new(0, 1, 1).unwrap();
297 assert_eq!(format!("{}", date), "0000-01-01");
298
299 let date = Date::new(-1, 1, 1).unwrap();
301 assert_eq!(format!("{}", date), "-0001-01-01");
302
303 let date = Date::new(-100, 12, 31).unwrap();
304 assert_eq!(format!("{}", date), "-0100-12-31");
305 }
306
307 #[test]
308 fn test_date_display_default() {
309 let date = Date::default();
310 assert_eq!(format!("{}", date), "1970-01-01");
311 }
312
313 #[test]
314 fn test_date_display_all_months() {
315 let months = [
316 (1, "01"),
317 (2, "02"),
318 (3, "03"),
319 (4, "04"),
320 (5, "05"),
321 (6, "06"),
322 (7, "07"),
323 (8, "08"),
324 (9, "09"),
325 (10, "10"),
326 (11, "11"),
327 (12, "12"),
328 ];
329
330 for (month, expected) in months {
331 let date = Date::new(2024, month, 15).unwrap();
332 assert_eq!(format!("{}", date), format!("2024-{}-15", expected));
333 }
334 }
335
336 #[test]
337 fn test_date_display_days_in_month() {
338 let test_cases = [
340 (2024, 1, 1, "2024-01-01"),
341 (2024, 1, 31, "2024-01-31"),
342 (2024, 2, 1, "2024-02-01"),
343 (2024, 2, 29, "2024-02-29"), (2024, 4, 1, "2024-04-01"),
345 (2024, 4, 30, "2024-04-30"),
346 (2024, 12, 1, "2024-12-01"),
347 (2024, 12, 31, "2024-12-31"),
348 ];
349
350 for (year, month, day, expected) in test_cases {
351 let date = Date::new(year, month, day).unwrap();
352 assert_eq!(format!("{}", date), expected);
353 }
354 }
355
356 #[test]
357 fn test_date_roundtrip() {
358 let test_dates = [
360 (1900, 1, 1),
361 (1970, 1, 1),
362 (2000, 2, 29), (2024, 12, 31),
364 (2100, 6, 15),
365 ];
366
367 for (year, month, day) in test_dates {
368 let date = Date::new(year, month, day).unwrap();
369 let days = date.to_days_since_epoch();
370 let recovered = Date::from_days_since_epoch(days).unwrap();
371
372 assert_eq!(date.year(), recovered.year());
373 assert_eq!(date.month(), recovered.month());
374 assert_eq!(date.day(), recovered.day());
375 }
376 }
377
378 #[test]
379 fn test_leap_year_detection() {
380 assert!(Date::is_leap_year(2000)); assert!(Date::is_leap_year(2024)); assert!(!Date::is_leap_year(1900)); assert!(!Date::is_leap_year(2023)); }
385
386 #[test]
387 fn test_invalid_dates() {
388 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()); }
395
396 #[test]
397 fn test_serde_roundtrip() {
398 let date = Date::new(2024, 3, 15).unwrap();
399 let json = to_string(&date).unwrap();
400 assert_eq!(json, "\"2024-03-15\"");
401
402 let recovered: Date = from_str(&json).unwrap();
403 assert_eq!(date, recovered);
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}