mod easter_offset;
mod fixed;
mod last_weekday;
mod nth_weekday;
mod one_off;
pub use easter_offset::EasterOffset;
pub use fixed::{FixedDate, WeekendShift};
pub use last_weekday::LastWeekday;
pub use nth_weekday::NthWeekday;
pub use one_off::OneOff;
use crate::{Date, Year};
#[derive(Debug, Clone, Copy)]
pub enum Rule {
Fixed(FixedDate),
NthWeekday(NthWeekday),
LastWeekday(LastWeekday),
Easter(EasterOffset),
OneOff(OneOff),
Custom(fn(Date) -> bool),
}
impl Rule {
pub(crate) fn weekend_shift(&self) -> WeekendShift {
match self {
Self::Fixed(r) => r.weekend_shift(),
_ => WeekendShift::None,
}
}
#[must_use]
pub fn is_holiday(&self, date: Date) -> bool {
match self {
Self::Fixed(r) => r.is_holiday(date),
Self::NthWeekday(r) => r.is_holiday(date),
Self::LastWeekday(r) => r.is_holiday(date),
Self::Easter(r) => r.is_holiday(date),
Self::OneOff(r) => r.is_holiday(date),
Self::Custom(f) => f(date),
}
}
pub(crate) const fn natural_date_in(&self, year: Year) -> Option<Date> {
match self {
Self::Fixed(r) => r.natural_date_in(year),
Self::NthWeekday(r) => r.natural_date_in(year),
Self::LastWeekday(r) => r.natural_date_in(year),
Self::Easter(r) => r.natural_date_in(year),
Self::OneOff(r) => r.natural_date_in(year),
Self::Custom(_) => None,
}
}
}
pub(crate) const fn date_ok(result: Result<Date, crate::TimeError>) -> Option<Date> {
match result {
Ok(d) => Some(d),
Err(_) => None,
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::{Month, Ordinal, Weekday, Year};
fn ymd(y: u16, m: Month, d: u8) -> Date {
Date::from_ymd(y, m, d).unwrap()
}
#[test]
fn rule_dispatches_to_each_variant() {
let rules = [
Rule::Fixed(FixedDate::new(Month::Jul, 4)),
Rule::NthWeekday(NthWeekday::new(Ordinal::Third, Weekday::Mon, Month::Jan)),
Rule::LastWeekday(LastWeekday::new(Weekday::Mon, Month::May)),
Rule::Easter(EasterOffset::easter_monday()),
Rule::OneOff(OneOff::new(ymd(2026, Month::Aug, 15))),
];
assert!(rules[0].is_holiday(ymd(2024, Month::Jul, 4)));
assert!(rules[1].is_holiday(ymd(2024, Month::Jan, 15)));
assert!(rules[2].is_holiday(ymd(2024, Month::May, 27)));
assert!(rules[3].is_holiday(ymd(2024, Month::Apr, 1)));
assert!(rules[4].is_holiday(ymd(2026, Month::Aug, 15)));
}
#[test]
fn custom_rule_invokes_fn_pointer() {
fn every_friday_13th(d: Date) -> bool {
d.day() == 13 && matches!(d.weekday(), Weekday::Fri)
}
let rule = Rule::Custom(every_friday_13th);
assert!(rule.is_holiday(ymd(2026, Month::Feb, 13)));
assert!(!rule.is_holiday(ymd(2026, Month::Feb, 14)));
}
fn expansion_cases() -> [Rule; 12] {
use crate::{FixedDate, LastWeekday, NthWeekday, OneOff, WeekendShift, YearRange};
[
Rule::Fixed(FixedDate::new(Month::Jul, 4).shift(WeekendShift::SatBackSunForward)),
Rule::Fixed(FixedDate::new(Month::Feb, 29)),
Rule::Fixed(
FixedDate::new(Month::Jan, 1).years(YearRange::literal_between(1971, 1977)),
),
Rule::Fixed(FixedDate::new(Month::Dec, 31).shift(WeekendShift::Forward)),
Rule::NthWeekday(NthWeekday::new(Ordinal::Third, Weekday::Mon, Month::Jan)),
Rule::NthWeekday(NthWeekday::new(Ordinal::Fifth, Weekday::Sun, Month::Feb)),
Rule::LastWeekday(LastWeekday::new(Weekday::Mon, Month::May)),
Rule::LastWeekday(
LastWeekday::new(Weekday::Wed, Month::Dec).years(YearRange::literal_through(1950)),
),
Rule::Easter(EasterOffset::good_friday()),
Rule::Easter(EasterOffset::new_orthodox(1)),
Rule::Easter(EasterOffset::new(280)), Rule::OneOff(OneOff::new(Date::literal(2026, Month::Aug, 15))),
]
}
#[test]
fn expansion_matches_probing_over_every_year() {
extern crate alloc;
use alloc::vec::Vec;
for rule in expansion_cases() {
for y in Year::MIN.get()..=Year::MAX.get() {
let year = Year::new(y).unwrap();
let jan1 = Date::from_ymd(y, Month::Jan, 1).unwrap();
let probed: Vec<Date> = (0..u32::from(year.length()))
.map(|offset| Date::from_serial(jan1.serial() + offset).unwrap())
.filter(|d| rule.is_holiday(*d))
.collect();
let expanded: Vec<Date> = rule.natural_date_in(year).into_iter().collect();
assert_eq!(expanded, probed, "{rule:?} in {y}");
}
}
}
const JULY_FOURTH: Rule = Rule::Fixed(FixedDate::new(Month::Jul, 4));
const MLK: Rule = Rule::NthWeekday(NthWeekday::new(Ordinal::Third, Weekday::Mon, Month::Jan));
const GOOD_FRIDAY: Rule = Rule::Easter(EasterOffset::good_friday());
const RULES: &[Rule] = &[JULY_FOURTH, MLK, GOOD_FRIDAY];
const ORTHO: Rule = Rule::Easter(EasterOffset::new_orthodox(1));
#[test]
fn const_context_construction() {
assert_eq!(RULES.len(), 3);
assert!(matches!(ORTHO, Rule::Easter(_)));
let _ = Year::MAX;
}
}