1use core::{
2 cmp::{Ord, Ordering, PartialOrd},
3 fmt::Display,
4};
5
6use thiserror::Error;
7
8use crate::{String, ToString, common::Date};
9
10#[derive(Debug, Error, PartialEq, Eq, Clone)]
12#[non_exhaustive]
13pub enum DateError {
14 #[error("{0}")]
15 InvalidYear(String),
16 #[error("{0}")]
17 InvalidMonth(String),
18 #[error("{0}")]
19 InvalidDay(String),
20 #[error("Date conversion error: {0}")]
21 ConversionError(String),
22}
23
24impl Display for Date {
25 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
26 match self.kind() {
27 DateKind::Full => write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day),
28 DateKind::YearAndMonth => write!(f, "{:04}-{:02}", self.year, self.month),
29 DateKind::YearOnly => write!(f, "{:04}", self.year),
30 DateKind::MonthAndDay => write!(f, "{:02}-{:02}", self.month, self.day),
31 }
32 }
33}
34
35#[derive(Debug, Clone, Eq, PartialEq, Copy)]
37pub enum DateKind {
38 Full,
40 YearOnly,
42 YearAndMonth,
44 MonthAndDay,
46}
47
48impl Date {
49 pub fn new(year: i32, month: i32, day: i32) -> Result<Self, DateError> {
53 validate_date(year, month, day)?;
54
55 Ok(Self { year, month, day })
56 }
57
58 #[must_use]
60 #[inline]
61 pub const fn kind(&self) -> DateKind {
62 if self.year != 0 && self.month == 0 && self.day == 0 {
63 DateKind::YearOnly
64 } else if self.year != 0 && self.month != 0 && self.day == 0 {
65 DateKind::YearAndMonth
66 } else if self.year == 0 && self.month != 0 && self.day != 0 {
67 DateKind::MonthAndDay
68 } else {
69 DateKind::Full
70 }
71 }
72
73 #[must_use]
75 pub fn is_valid(&self) -> bool {
76 validate_date(self.year, self.month, self.day).is_ok()
77 }
78
79 #[must_use]
80 #[inline]
81 pub const fn has_year(&self) -> bool {
82 self.year != 0
83 }
84
85 #[must_use]
87 #[inline]
88 pub const fn is_year_only(&self) -> bool {
89 self.year != 0 && (self.month == 0 && self.day == 0)
90 }
91
92 #[must_use]
94 #[inline]
95 pub const fn is_year_and_month(&self) -> bool {
96 self.year != 0 && self.month != 0 && self.day == 0
97 }
98
99 #[must_use]
101 #[inline]
102 pub const fn is_month_and_day(&self) -> bool {
103 self.year == 0 && self.month != 0 && self.day != 0
104 }
105}
106
107impl PartialOrd for Date {
108 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
109 if !(self.is_valid() && other.is_valid()) {
110 return None;
111 }
112
113 let self_kind = self.kind();
114 let other_kind = other.kind();
115
116 if self_kind != other_kind {
117 return None;
118 }
119
120 Some(
121 self.year
122 .cmp(&other.year)
123 .then_with(|| self.month.cmp(&other.month))
124 .then_with(|| self.day.cmp(&other.day)),
125 )
126 }
127}
128
129#[cfg(feature = "chrono")]
130mod chrono_impls {
131 use chrono::Utc;
132
133 use super::validate_date;
134 use crate::{Date, ToString, date::DateError, format};
135
136 impl Date {
137 pub fn to_naive_date(self) -> Result<::chrono::NaiveDate, DateError> {
139 self.try_into()
140 }
141
142 #[cfg(any(feature = "std", feature = "chrono-wasm"))]
143 #[must_use]
145 #[inline]
146 pub fn today() -> Self {
147 Utc::now().naive_utc().date().into()
148 }
149 }
150
151 impl TryFrom<crate::Date> for chrono::NaiveDate {
152 type Error = DateError;
153
154 fn try_from(date: Date) -> Result<Self, Self::Error> {
155 if date.year == 0 || date.month == 0 || date.day == 0 {
156 return Err(DateError::ConversionError(
157 "Cannot convert Date with year=0, month=0, or day=0 to NaiveDate".to_string(),
158 ));
159 }
160
161 validate_date(date.year, date.month, date.day)?;
162
163 Self::from_ymd_opt(
165 date.year,
166 date.month.cast_unsigned(),
167 date.day.cast_unsigned(),
168 )
169 .ok_or_else(|| {
170 DateError::ConversionError(format!(
171 "Invalid date components for NaiveDate: Y:{}, M:{}, D:{}",
172 date.year, date.month, date.day
173 ))
174 })
175 }
176 }
177
178 impl From<chrono::NaiveDate> for Date {
179 #[inline]
180 fn from(naive_date: chrono::NaiveDate) -> Self {
181 use chrono::Datelike;
182 Self {
184 year: naive_date.year(),
185 month: naive_date.month().cast_signed(),
186 day: naive_date.day().cast_signed(),
187 }
188 }
189 }
190}
191
192const fn is_leap_year(year: i32) -> bool {
193 (year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0))
194}
195
196pub(crate) const fn days_in_month(month: i32, year: i32) -> i32 {
197 match month {
198 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
199 4 | 6 | 9 | 11 => 30,
200 2 => {
201 if year == 0 || is_leap_year(year) {
204 29
205 } else {
206 28
207 }
208 }
209 _ => 0,
210 }
211}
212
213fn validate_date(year: i32, month: i32, day: i32) -> Result<(), DateError> {
214 if !(0..=9999).contains(&year) {
215 return Err(DateError::InvalidYear(
216 "Invalid year value (must be within 0 and 9999)".to_string(),
217 ));
218 }
219
220 if !(0..=12).contains(&month) {
221 return Err(DateError::InvalidMonth(
222 "Invalid month value (must be within 0 and 12)".to_string(),
223 ));
224 }
225
226 if !(0..=31).contains(&day) {
227 return Err(DateError::InvalidDay(
228 "Invalid day value (must be within 0 and 31)".to_string(),
229 ));
230 }
231
232 if year == 0 {
233 if month == 0 {
234 return Err(DateError::InvalidMonth(
235 "The month cannot be set to 0 if the year is also set to 0".to_string(),
236 ));
237 }
238 if day == 0 {
239 return Err(DateError::InvalidDay(
240 "The day cannot be set to 0 if the year is also set to 0".to_string(),
241 ));
242 }
243 } else if month == 0 {
244 if day != 0 {
245 return Err(DateError::InvalidMonth(
246 "The month cannot be 0 if the day is set".to_string(),
247 ));
248 }
249 return Ok(());
250 }
251
252 if day != 0 {
253 let max_days = days_in_month(month, year);
254 if day > max_days {
255 return Err(DateError::InvalidDay(alloc::format!(
256 "Invalid day {day} for month {month} (max is {max_days} for year {year})"
257 )));
258 }
259 }
260
261 Ok(())
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 fn date(y: i32, m: i32, d: i32) -> Result<Date, DateError> {
269 Date::new(y, m, d)
270 }
271
272 #[test]
273 fn test_date_kinds_creation() {
274 let full = date(2024, 1, 15).unwrap();
276 assert_eq!(full.kind(), DateKind::Full);
277 assert_eq!(full.to_string(), "2024-01-15");
278 assert!(full.is_valid());
279
280 let year = date(2024, 0, 0).unwrap();
282 assert_eq!(year.kind(), DateKind::YearOnly);
283 assert_eq!(year.to_string(), "2024");
284 assert!(year.is_year_only());
285
286 let ym = date(2025, 12, 0).unwrap();
288 assert_eq!(ym.kind(), DateKind::YearAndMonth);
289 assert_eq!(ym.to_string(), "2025-12");
290 assert!(ym.is_year_and_month());
291
292 let md = date(0, 5, 20).unwrap();
294 assert_eq!(md.kind(), DateKind::MonthAndDay);
295 assert_eq!(md.to_string(), "05-20");
296 assert!(md.is_month_and_day());
297 }
298
299 #[test]
300 fn test_validation_failures() {
301 assert!(matches!(date(-1, 1, 1), Err(DateError::InvalidYear(_))));
303 assert!(matches!(date(10000, 1, 1), Err(DateError::InvalidYear(_))));
304 assert!(matches!(date(2024, 13, 1), Err(DateError::InvalidMonth(_))));
305 assert!(matches!(date(2024, 1, 32), Err(DateError::InvalidDay(_))));
306
307 assert!(matches!(date(0, 0, 5), Err(DateError::InvalidMonth(_))));
309
310 assert!(matches!(date(0, 5, 0), Err(DateError::InvalidDay(_))));
312
313 assert!(matches!(date(2024, 0, 5), Err(DateError::InvalidMonth(_))));
315 }
316
317 #[test]
318 fn test_ordering() {
319 let d1 = date(2024, 5, 10).unwrap();
321 let d2 = date(2024, 5, 11).unwrap();
322 let d3 = date(2025, 1, 1).unwrap();
323
324 assert!(d1 < d2);
325 assert!(d2 < d3);
326 assert!(d1 < d3);
327
328 let y1 = date(2023, 0, 0).unwrap();
330 let y2 = date(2024, 0, 0).unwrap();
331 assert!(y1 < y2);
332
333 let full = date(2024, 1, 1).unwrap();
335 let year_only = date(2024, 0, 0).unwrap();
336 assert_eq!(full.partial_cmp(&year_only), None);
337
338 let md1 = date(0, 2, 1).unwrap();
340 let md2 = date(0, 2, 2).unwrap();
341 assert!(md1 < md2);
342 }
343
344 #[test]
345 fn test_calendar_validation() {
346 assert!(Date::new(2023, 1, 31).is_ok());
348 assert!(Date::new(2023, 4, 30).is_ok());
349 assert!(Date::new(2023, 4, 31).is_err()); assert!(Date::new(2023, 2, 28).is_ok());
353 assert!(Date::new(2023, 2, 29).is_err()); assert!(Date::new(2024, 2, 29).is_ok());
357 assert!(Date::new(2024, 2, 30).is_err());
358
359 assert!(Date::new(1900, 2, 29).is_err()); assert!(Date::new(2000, 2, 29).is_ok()); }
363
364 #[test]
365 fn test_special_zero_cases() {
366 assert!(Date::new(2024, 0, 0).is_ok());
368
369 assert!(Date::new(2024, 2, 0).is_ok());
371
372 assert!(Date::new(2024, 0, 5).is_err());
374
375 assert!(Date::new(0, 2, 29).is_ok());
378 assert!(Date::new(0, 2, 30).is_err());
379 }
380
381 #[cfg(feature = "chrono")]
382 mod chrono_tests {
383 use super::*;
384 use chrono::NaiveDate;
385
386 #[test]
387 fn test_to_naive_date() {
388 let d = date(2024, 2, 29).unwrap(); let naive = d.to_naive_date().unwrap();
390 assert_eq!(naive, NaiveDate::from_ymd_opt(2024, 2, 29).unwrap());
391 }
392
393 #[test]
394 fn test_from_naive_date() {
395 let naive = NaiveDate::from_ymd_opt(2023, 10, 25).unwrap();
396 let d: Date = naive.into();
397 assert_eq!(d.year, 2023);
398 assert_eq!(d.month, 10);
399 assert_eq!(d.day, 25);
400 assert_eq!(d.kind(), DateKind::Full);
401 }
402 }
403}