#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct CalendarDate {
pub year: i32,
pub month: u8,
pub day: u8,
}
impl CalendarDate {
#[must_use]
pub const fn new(year: i32, month: u8, day: u8) -> Self {
Self { year, month, day }
}
#[must_use]
pub const fn is_plausible(&self) -> bool {
self.month >= 1 && self.month <= 12 && self.day >= 1 && self.day <= 31
}
#[must_use]
pub fn parse_iso(s: &str) -> Option<Self> {
let b = s.as_bytes();
if b.len() != 10 || b[4] != b'-' || b[7] != b'-' {
return None;
}
let field = |from: usize, to: usize| -> Option<u32> {
let mut acc: u32 = 0;
for &c in &b[from..to] {
if !c.is_ascii_digit() {
return None;
}
acc = acc * 10 + u32::from(c - b'0');
}
Some(acc)
};
let date = Self::new(
i32::try_from(field(0, 4)?).ok()?,
u8::try_from(field(5, 7)?).ok()?,
u8::try_from(field(8, 10)?).ok()?,
);
date.is_plausible().then_some(date)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn orders_chronologically_across_field_boundaries() {
let earlier = CalendarDate::new(2031, 8, 17);
let boundary = CalendarDate::new(2031, 8, 18);
assert!(earlier < boundary);
assert!(CalendarDate::new(2030, 12, 31) < CalendarDate::new(2031, 1, 1));
assert!(CalendarDate::new(2031, 7, 31) < CalendarDate::new(2031, 8, 1));
}
#[test]
fn equal_dates_compare_equal() {
assert_eq!(
CalendarDate::new(2036, 8, 18),
CalendarDate::new(2036, 8, 18)
);
}
#[test]
fn parses_a_well_formed_iso_date() {
assert_eq!(
CalendarDate::parse_iso("2031-08-18"),
Some(CalendarDate::new(2031, 8, 18))
);
assert_eq!(
CalendarDate::parse_iso("0001-01-01"),
Some(CalendarDate::new(1, 1, 1))
);
}
#[test]
fn rejects_anything_that_is_not_strict_iso() {
for bad in [
"",
"2031-8-18", "2031/08/18", "31-08-2031", "2031-08-18T", "2031-08-1", "2031-08-188", "20x1-08-18", "2031-13-01", "2031-00-01",
"2031-08-00",
"2031-08-32",
] {
assert_eq!(CalendarDate::parse_iso(bad), None, "should reject {bad:?}");
}
}
#[test]
fn plausibility_rejects_out_of_range_components() {
assert!(CalendarDate::new(2031, 8, 18).is_plausible());
assert!(!CalendarDate::new(2031, 13, 1).is_plausible());
assert!(!CalendarDate::new(2031, 0, 1).is_plausible());
assert!(!CalendarDate::new(2031, 8, 0).is_plausible());
assert!(!CalendarDate::new(2031, 8, 32).is_plausible());
assert!(CalendarDate::new(2031, 2, 31).is_plausible());
}
}