use std::fmt;
use std::str::FromStr;
use jiff::civil::Date;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("{text:?} is not an effectiveTime (YYYYMMDD)")]
pub struct EffectiveTimeError {
pub text: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct EffectiveTime(Date);
impl EffectiveTime {
pub fn parse(text: &str) -> Result<Self, EffectiveTimeError> {
let error = || EffectiveTimeError {
text: text.to_owned(),
};
if text.len() != 8 || !text.bytes().all(|b| b.is_ascii_digit()) {
return Err(error());
}
let group = |range: std::ops::Range<usize>| -> Result<i16, EffectiveTimeError> {
text.get(range)
.and_then(|g| g.parse::<i16>().ok())
.ok_or_else(error)
};
let year = group(0..4)?;
let Ok(month) = i8::try_from(group(4..6)?) else {
return Err(error());
};
let Ok(day) = i8::try_from(group(6..8)?) else {
return Err(error());
};
let Ok(date) = Date::new(year, month, day) else {
return Err(error());
};
Ok(Self(date))
}
#[must_use]
pub const fn date(self) -> Date {
self.0
}
#[must_use]
pub fn compact(self) -> String {
format!(
"{:04}{:02}{:02}",
self.0.year(),
self.0.month(),
self.0.day()
)
}
}
impl fmt::Display for EffectiveTime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.compact())
}
}
impl FromStr for EffectiveTime {
type Err = EffectiveTimeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
#[cfg(test)]
mod tests {
use super::EffectiveTime;
#[test]
fn dates_parse_and_print() {
let time = EffectiveTime::parse("20260630").expect("valid");
assert_eq!(time.compact(), "20260630");
assert_eq!(time.date().year(), 2026);
assert!(EffectiveTime::parse("20260631").is_err());
assert!(EffectiveTime::parse("2026-06-30").is_err());
assert!(EffectiveTime::parse("").is_err());
assert!(
EffectiveTime::parse("20020131").expect("valid")
< EffectiveTime::parse("20260630").expect("valid")
);
}
}