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