1use core::fmt::{self, Display};
8use std::str::FromStr;
9
10#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
17pub struct Date {
18 year: u32,
20 month: u8,
22 day: u8,
24}
25impl Date {
26 #[inline]
42 #[must_use]
43 pub fn new(year: u32, month: u32, day: u32) -> Self {
44 match Self::try_new(year, month, day) {
45 Ok(x) => x,
46 Err(e) => panic!("{}", e),
47 }
48 }
49
50 pub fn try_new(year: u32, month: u32, day: u32) -> Result<Self, DateValidationError> {
57 if year < 1 {
58 return Err(DateValidationError {
59 field: InvalidDateField::Year,
60 value: year,
61 });
62 }
63 if month < 1 || month > 12 {
64 return Err(DateValidationError {
65 field: InvalidDateField::Month,
66 value: month,
67 });
68 }
69 if day < 1 || day > max_days_of_month(month) {
70 return Err(DateValidationError {
71 field: InvalidDateField::DayOfMonth { month },
72 value: day,
73 });
74 }
75 Ok(Date {
76 month: month as u8,
77 day: day as u8,
78 year,
79 })
80 }
81
82 #[inline]
92 #[must_use]
93 pub fn is_since(&self, start: Date) -> bool {
94 *self >= start
95 }
96
97 #[inline]
109 #[must_use]
110 pub fn is_before(&self, end: Date) -> bool {
111 *self < end
112 }
113
114 #[inline]
116 #[must_use]
117 pub fn year(&self) -> u32 {
118 self.year
119 }
120
121 #[inline]
123 #[must_use]
124 pub fn month(&self) -> u32 {
125 self.month as u32
126 }
127
128 #[inline]
130 #[must_use]
131 pub fn day(&self) -> u32 {
132 self.day as u32
133 }
134}
135impl Display for Date {
137 fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
138 write!(
139 formatter,
140 "{:04}-{:02}-{:02}",
141 self.year, self.month, self.day,
142 )
143 }
144}
145impl FromStr for Date {
146 type Err = DateParseError;
147 fn from_str(full_text: &str) -> Result<Self, Self::Err> {
148 fn do_parse(full_text: &str) -> Result<Date, ParseErrorReason> {
149 let mut raw_parts = full_text.split('-');
150 let mut parts: [Option<u32>; 3] = [None; 3];
151 for part in &mut parts {
152 let raw_part = raw_parts.next().ok_or(ParseErrorReason::MalformedSyntax)?;
153 *part = Some(raw_part.parse().map_err(|cause| {
154 ParseErrorReason::NumberParseFailure {
155 text: raw_part.into(),
156 cause,
157 }
158 })?);
159 }
160 if raw_parts.next().is_some() {
161 return Err(ParseErrorReason::MalformedSyntax);
162 }
163 Date::try_new(parts[0].unwrap(), parts[1].unwrap(), parts[2].unwrap())
164 .map_err(ParseErrorReason::ValidationFailure)
165 }
166 match do_parse(full_text) {
167 Ok(res) => Ok(res),
168 Err(reason) => Err(DateParseError {
169 full_text: full_text.into(),
170 reason,
171 }),
172 }
173 }
174}
175#[derive(Debug)]
177pub struct DateParseError {
178 full_text: String,
179 reason: ParseErrorReason,
180}
181impl std::error::Error for DateParseError {
182 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
183 match self.reason {
184 ParseErrorReason::MalformedSyntax => None,
185 ParseErrorReason::NumberParseFailure { ref cause, .. } => Some(cause),
186 ParseErrorReason::ValidationFailure(ref cause) => Some(cause),
187 }
188 }
189}
190#[derive(Debug)]
191enum ParseErrorReason {
192 MalformedSyntax,
193 NumberParseFailure {
194 text: String,
195 cause: std::num::ParseIntError,
196 },
197 ValidationFailure(DateValidationError),
198}
199impl Display for DateParseError {
200 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
201 write!(f, "Failed to parse `{:?}` as a date: ", self.full_text)?;
202 match self.reason {
203 ParseErrorReason::MalformedSyntax => {
204 write!(f, "Not in ISO 8601 format (example: 2025-12-31)")
205 }
206 ParseErrorReason::NumberParseFailure {
207 ref text,
208 ref cause,
209 } => {
210 write!(f, "Failed to parse `{}` as number ({})", text, cause)
211 }
212 ParseErrorReason::ValidationFailure(ref cause) => Display::fmt(cause, f),
213 }
214 }
215}
216
217#[inline]
222fn max_days_of_month(x: u32) -> u32 {
223 match x {
224 1 => 31,
225 2 => 29,
226 _ => 30 + ((x + 1) % 2),
227 }
228}
229#[derive(Debug)]
231pub struct DateValidationError {
232 field: InvalidDateField,
233 value: u32,
234}
235impl std::error::Error for DateValidationError {}
236#[derive(Debug)]
237enum InvalidDateField {
238 Year,
239 Month,
240 DayOfMonth { month: u32 },
241}
242impl Display for DateValidationError {
243 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244 let field_name = match self.field {
245 InvalidDateField::Year => "year",
246 InvalidDateField::Month => "month",
247 InvalidDateField::DayOfMonth { .. } => "day of month",
248 };
249 write!(f, "Invalid {} `{}`", field_name, self.value)?;
250 match self.field {
251 InvalidDateField::Year | InvalidDateField::Month => {}
252 InvalidDateField::DayOfMonth { month } => {
253 write!(f, " for month {}", month)?;
254 }
255 }
256 Ok(())
257 }
258}
259
260#[cfg(test)]
261mod test {
262 use super::*;
263
264 fn test_dates() -> Vec<(Date, Date)> {
266 vec![
267 (Date::new(2018, 12, 14), Date::new(2022, 8, 16)),
268 (Date::new(2024, 11, 14), Date::new(2024, 12, 7)),
269 (Date::new(2024, 11, 14), Date::new(2024, 11, 17)),
270 ]
271 }
272
273 #[test]
274 fn days_of_month() {
275 assert_eq!(max_days_of_month(1), 31);
276 assert_eq!(max_days_of_month(12), 31);
277 assert_eq!(max_days_of_month(2), 29);
278 assert_eq!(max_days_of_month(10), 31);
279 }
280
281 #[test]
282 fn before_after() {
283 for (before, after) in test_dates() {
284 assert!(before.is_before(after), "{} & {}", before, after);
285 assert!(after.is_since(before), "{} & {}", before, after);
286 for &date in &[before, after] {
288 assert!(date.is_since(date), "{}", date);
289 assert!(!date.is_before(date), "{}", date);
290 }
291 }
292 }
293
294 #[test]
295 #[should_panic(expected = "Invalid year")]
296 fn invalid_year() {
297 let _ = Date::new(0, 7, 18);
298 }
299
300 #[test]
301 #[should_panic(expected = "Invalid month")]
302 fn invalid_month() {
303 let _ = Date::new(2014, 13, 18);
304 }
305
306 #[test]
307 #[should_panic(expected = "Invalid day of month")]
308 fn invalid_date() {
309 let _ = Date::new(2014, 7, 36);
310 }
311
312 #[test]
313 #[should_panic(expected = "Invalid day of month")]
314 fn contextually_invalid_date() {
315 let _ = Date::new(2014, 2, 30);
316 }
317}