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 > 31 {
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#[derive(Debug)]
219pub struct DateValidationError {
220 field: InvalidDateField,
221 value: u32,
222}
223impl std::error::Error for DateValidationError {}
224#[derive(Debug)]
225enum InvalidDateField {
226 Year,
227 Month,
228 DayOfMonth { month: u32 },
229}
230impl Display for DateValidationError {
231 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
232 let field_name = match self.field {
233 InvalidDateField::Year => "year",
234 InvalidDateField::Month => "month",
235 InvalidDateField::DayOfMonth { .. } => "day of month",
236 };
237 write!(f, "Invalid {} `{}`", field_name, self.value)?;
238 match self.field {
239 InvalidDateField::Year | InvalidDateField::Month => {}
240 InvalidDateField::DayOfMonth { month } => {
241 write!(f, " for month {}", month)?;
242 }
243 }
244 Ok(())
245 }
246}
247
248#[cfg(test)]
249mod test {
250 use super::*;
251
252 #[test]
253 fn parse_valid() {
254 macro_rules! check_valid {
255 ($($text:literal => $expected:expr),+ $(,)?) => {
256 $(
257 assert_eq!(
258 $text.parse::<Date>().expect(concat!("Date `", $text, "` is invalid")),
259 $expected,
260 );
261 )*
262 }
263 }
264 check_valid! {
265 "2026-07-31" => Date::new(2026, 7, 31),
266 }
267 }
268
269 fn test_dates() -> Vec<(Date, Date)> {
271 vec![
272 (Date::new(2018, 12, 14), Date::new(2022, 8, 16)),
273 (Date::new(2024, 11, 14), Date::new(2024, 12, 7)),
274 (Date::new(2024, 11, 14), Date::new(2024, 11, 17)),
275 ]
276 }
277
278 #[test]
279 fn before_after() {
280 for (before, after) in test_dates() {
281 assert!(before.is_before(after), "{} & {}", before, after);
282 assert!(after.is_since(before), "{} & {}", before, after);
283 for &date in &[before, after] {
285 assert!(date.is_since(date), "{}", date);
286 assert!(!date.is_before(date), "{}", date);
287 }
288 }
289 }
290
291 #[test]
292 #[should_panic(expected = "Invalid year")]
293 fn invalid_year() {
294 let _ = Date::new(0, 7, 18);
295 }
296
297 #[test]
298 #[should_panic(expected = "Invalid month")]
299 fn invalid_month() {
300 let _ = Date::new(2014, 13, 18);
301 }
302
303 #[test]
304 #[should_panic(expected = "Invalid day of month")]
305 fn invalid_date() {
306 let _ = Date::new(2014, 7, 36);
307 }
308}