use crate::{Date, Month, Weekday, Year, YearRange};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum WeekendShift {
#[default]
None,
Forward,
SunForward,
SatBackSunForward,
}
impl WeekendShift {
pub(crate) const fn chains(self) -> bool {
matches!(self, Self::Forward)
}
pub(crate) fn direction(self, day: Weekday) -> Option<i32> {
match (self, day) {
(Self::None, _) => None,
(_, Weekday::Sun) | (Self::Forward, Weekday::Sat) => Some(1),
(Self::SatBackSunForward, Weekday::Sat) => Some(-1),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FixedDate {
month: Month,
day: u8,
shift: WeekendShift,
years: YearRange,
}
impl FixedDate {
#[must_use]
pub const fn new(month: Month, day: u8) -> Self {
Self {
month,
day,
shift: WeekendShift::None,
years: YearRange::ALWAYS,
}
}
#[must_use]
pub const fn shift(mut self, shift: WeekendShift) -> Self {
self.shift = shift;
self
}
#[must_use]
pub const fn from_year(mut self, year: Year) -> Self {
self.years = YearRange::from_year(year);
self
}
#[must_use]
pub const fn years(mut self, range: YearRange) -> Self {
self.years = range;
self
}
#[must_use]
pub const fn month(&self) -> Month {
self.month
}
#[must_use]
pub const fn day(&self) -> u8 {
self.day
}
#[must_use]
pub const fn weekend_shift(&self) -> WeekendShift {
self.shift
}
#[must_use]
pub const fn year_range(&self) -> YearRange {
self.years
}
#[must_use]
pub fn is_holiday(&self, date: Date) -> bool {
self.years.contains(date.year()) && date.month() == self.month && date.day() == self.day
}
pub(crate) const fn natural_date_in(self, year: Year) -> Option<Date> {
if !self.years.contains(year) {
return None;
}
super::date_ok(Date::from_ymd(year.get(), self.month, self.day))
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
fn ymd(y: u16, m: Month, d: u8) -> Date {
Date::from_ymd(y, m, d).unwrap()
}
#[test]
fn matches_the_natural_date_whatever_the_shift() {
for shift in [
WeekendShift::None,
WeekendShift::SatBackSunForward,
WeekendShift::SunForward,
WeekendShift::Forward,
] {
let rule = FixedDate::new(Month::Jul, 4).shift(shift);
assert!(rule.is_holiday(ymd(2024, Month::Jul, 4)), "{shift:?}");
assert!(rule.is_holiday(ymd(2026, Month::Jul, 4)), "{shift:?}");
assert!(!rule.is_holiday(ymd(2026, Month::Jul, 3)), "{shift:?}");
assert_eq!(rule.weekend_shift(), shift);
}
}
#[test]
fn does_not_match_another_month_or_day() {
let rule = FixedDate::new(Month::Jul, 4);
assert!(!rule.is_holiday(ymd(2024, Month::Aug, 4)));
assert!(!rule.is_holiday(ymd(2024, Month::Jul, 5)));
}
#[test]
fn leap_day_rule_matches_only_in_leap_years() {
let rule = FixedDate::new(Month::Feb, 29);
assert!(rule.is_holiday(ymd(2024, Month::Feb, 29)));
assert!(!rule.is_holiday(ymd(2025, Month::Feb, 28)));
}
#[test]
fn year_range_filter() {
let rule = FixedDate::new(Month::Jun, 19).from_year(Year::new(2021).unwrap());
assert!(!rule.is_holiday(ymd(2020, Month::Jun, 19)));
assert!(rule.is_holiday(ymd(2021, Month::Jun, 19)));
assert!(rule.is_holiday(ymd(2030, Month::Jun, 19)));
assert_eq!(
rule.year_range(),
YearRange::from_year(Year::new(2021).unwrap())
);
}
#[test]
fn bounded_year_range() {
let rule = FixedDate::new(Month::Feb, 22).years(YearRange::literal_through(1970));
assert!(rule.is_holiday(ymd(1970, Month::Feb, 22)));
assert!(!rule.is_holiday(ymd(1971, Month::Feb, 22)));
}
#[test]
fn accessors_round_trip() {
let rule = FixedDate::new(Month::Dec, 25);
assert_eq!(rule.month(), Month::Dec);
assert_eq!(rule.day(), 25);
assert_eq!(rule.weekend_shift(), WeekendShift::None);
}
}