use std::borrow::Borrow;
use crate::prelude::*;
#[derive(
Clone,
derive_more::Display,
Debug,
Serialize,
Deserialize,
PartialEq,
PartialOrd,
Eq,
Hash,
From,
TryUnwrap,
)]
#[serde(untagged)]
pub enum PeriodAnno {
YearAndMonth(YearAndMonth),
YearMonthAndFortnight(YearMonthAndFortnight),
}
impl std::str::FromStr for PeriodAnno {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Ok(period) = YearAndMonth::from_str(s) {
Ok(Self::YearAndMonth(period))
} else if let Ok(period) = YearMonthAndFortnight::from_str(s) {
Ok(Self::YearMonthAndFortnight(period))
} else {
Err(Error::InvalidPeriod {
bad_value: s.to_owned(),
})
}
}
}
impl HasSample for PeriodAnno {
fn sample() -> Self {
YearAndMonth::sample().into()
}
fn sample_other() -> Self {
YearMonthAndFortnight::sample_other().into()
}
}
impl TryFromPeriodAnno for PeriodAnno {
fn try_from_period_anno(period: PeriodAnno) -> Result<Self> {
Ok(period)
}
}
impl IsPeriod for PeriodAnno {
fn max_granularity(&self) -> Granularity {
match self {
Self::YearAndMonth(period) => period.max_granularity(),
Self::YearMonthAndFortnight(period) => period.max_granularity(),
}
}
fn elapsed_periods_since(&self, start: impl Borrow<Self>) -> Result<u16> {
match (self, start.borrow()) {
(Self::YearAndMonth(lhs), Self::YearAndMonth(rhs)) => lhs.elapsed_periods_since(rhs),
(Self::YearMonthAndFortnight(lhs), Self::YearMonthAndFortnight(rhs)) => {
lhs.elapsed_periods_since(rhs)
}
(Self::YearAndMonth(_), Self::YearMonthAndFortnight(_)) => {
Err(Error::PeriodIsNotYearAndMonth)
}
(Self::YearMonthAndFortnight(_), Self::YearAndMonth(_)) => {
Err(Error::PeriodIsNotYearMonthAndFortnight)
}
}
}
fn to_date_end_of_period(&self) -> Date {
match self {
Self::YearAndMonth(period) => period.to_date_end_of_period(),
Self::YearMonthAndFortnight(period) => period.to_date_end_of_period(),
}
}
fn year(&self) -> &Year {
match self {
Self::YearAndMonth(period) => period.year(),
Self::YearMonthAndFortnight(period) => period.year(),
}
}
fn month(&self) -> &Month {
match self {
Self::YearAndMonth(period) => period.month(),
Self::YearMonthAndFortnight(period) => period.month(),
}
}
}
#[cfg(test)]
mod tests {
use insta::{assert_ron_snapshot, assert_snapshot};
use super::*;
type Sut = PeriodAnno;
#[test]
fn equality() {
assert_eq!(Sut::sample(), Sut::sample());
assert_eq!(Sut::sample_other(), Sut::sample_other());
}
#[test]
fn inequality() {
assert_ne!(Sut::sample(), Sut::sample_other());
}
#[test]
fn test_elapsed_periods_since_year_and_month() {
let early = Sut::YearAndMonth(YearAndMonth::december(2024));
let late = Sut::YearAndMonth(YearAndMonth::february(2025));
assert_eq!(late.elapsed_periods_since(&early).unwrap(), 2);
}
#[test]
fn test_elapsed_periods_since_year_and_fortnight() {
let early = Sut::YearMonthAndFortnight(
YearMonthAndFortnight::builder()
.year(2024.into())
.month(Month::December)
.half(MonthHalf::Second)
.build(),
);
let late = Sut::YearMonthAndFortnight(
YearMonthAndFortnight::builder()
.year(2025.into())
.month(Month::February)
.half(MonthHalf::First)
.build(),
);
assert_eq!(late.elapsed_periods_since(&early).unwrap(), 3); }
#[test]
fn test_to_date_end_of_period_year_and_month() {
let period = Sut::YearAndMonth(YearAndMonth::december(2024));
assert_eq!(
period.to_date_end_of_period(),
Date::from_str("2024-12-31").unwrap()
);
}
#[test]
fn test_to_date_end_of_period_year_and_fortnight() {
let period = Sut::YearMonthAndFortnight(
YearMonthAndFortnight::builder()
.year(2024.into())
.month(Month::December)
.half(MonthHalf::First)
.build(),
);
assert_eq!(
period.to_date_end_of_period(),
Date::from_str("2024-12-15").unwrap()
);
}
#[test]
fn serde_fortnight() {
assert_ron_snapshot!(Sut::YearMonthAndFortnight(
YearMonthAndFortnight::builder()
.year(2025.into())
.month(Month::May)
.half(MonthHalf::Second)
.build()
));
}
#[test]
fn test_deserialize_ron_year_month_and_fortnight() {
let ron_str = r#""2025-05-second-half""#;
let period: Sut = ron::de::from_str(ron_str).expect("Failed to deserialize RON");
assert_eq!(
period,
Sut::YearMonthAndFortnight(
YearMonthAndFortnight::builder()
.year(2025.into())
.month(Month::May)
.half(MonthHalf::Second)
.build()
)
);
}
#[test]
fn year_month_and_fortnight_max_granularity() {
let sut = Sut::YearMonthAndFortnight(YearMonthAndFortnight::sample());
assert_eq!(sut.max_granularity(), Granularity::Fortnight);
}
#[test]
fn mix_ym_ymf_throws() {
let result = Sut::YearAndMonth(YearAndMonth::sample())
.elapsed_periods_since(Sut::YearMonthAndFortnight(YearMonthAndFortnight::sample()));
assert!(result.is_err(), "Expected error when mixing period kinds");
}
#[test]
fn mix_ymf_ym_throws() {
let result = Sut::YearMonthAndFortnight(YearMonthAndFortnight::sample())
.elapsed_periods_since(Sut::YearAndMonth(YearAndMonth::sample()));
assert!(result.is_err(), "Expected error when mixing period kinds");
}
#[test]
fn year_month_and_fortnight_get_year() {
let sut = Sut::YearMonthAndFortnight(
YearMonthAndFortnight::builder()
.year(2025.into())
.month(Month::May)
.half(MonthHalf::Second)
.build(),
);
assert_eq!(*sut.year(), Year::from(2025));
}
#[test]
fn year_month_and_fortnight_get_month() {
let sut = Sut::YearMonthAndFortnight(
YearMonthAndFortnight::builder()
.year(2025.into())
.month(Month::May)
.half(MonthHalf::Second)
.build(),
);
assert_eq!(*sut.month(), Month::May);
}
#[test]
fn from_str_valid() {
let valid_formats = vec![
"2024-12",
"2024-01",
"2024-12-first-half",
"2024-12-first",
"2024-12-1",
"2024-12-second-half",
"2024-12-second",
"2024-12-2",
];
for format in valid_formats {
let period: Sut = format.parse().expect("Failed to parse valid format");
assert!(matches!(
period,
Sut::YearAndMonth(_) | Sut::YearMonthAndFortnight(_)
));
}
}
#[test]
fn from_str_invalid() {
let invalid_formats = vec![
"2024-13", "2024-00", "2024-11-11", "2024-12-third-half", "2024-12-third", "2024-12-3", "2024-12-fourth-half", "2024-12-fourth", "2024-12-4", ];
for format in invalid_formats {
let result: Result<Sut, _> = format.parse();
assert!(result.is_err(), "Expected error for format: {}", format);
}
}
#[test]
fn display_sample() {
assert_snapshot!(Sut::sample())
}
#[test]
fn display_sample_other() {
assert_snapshot!(Sut::sample_other())
}
}