use crate::{Date, Month, Weekday, Year, YearRange};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct LastWeekday {
weekday: Weekday,
month: Month,
years: YearRange,
}
impl LastWeekday {
#[must_use]
pub const fn new(weekday: Weekday, month: Month) -> Self {
Self {
weekday,
month,
years: YearRange::ALWAYS,
}
}
#[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 weekday(&self) -> Weekday {
self.weekday
}
#[must_use]
pub const fn month(&self) -> Month {
self.month
}
#[must_use]
pub const fn year_range(&self) -> YearRange {
self.years
}
#[must_use]
pub fn is_holiday(&self, date: Date) -> bool {
if !self.years.contains(date.year()) {
return false;
}
if date.month() as u8 != self.month as u8 {
return false;
}
if date.weekday() as u8 != self.weekday as u8 {
return false;
}
match date.add_days(7) {
Ok(next) => next.month() as u8 != self.month as u8,
Err(_) => true,
}
}
#[allow(clippy::cast_lossless)]
pub(crate) const fn natural_date_in(self, year: Year) -> Option<Date> {
if !self.years.contains(year) {
return None;
}
let Ok(last) = Date::from_ymd(year.get(), self.month, self.month.length(year)) else {
return None;
};
let back_to_weekday =
(last.weekday().get() as i32 - self.weekday.get() as i32).rem_euclid(7);
super::date_ok(last.add_days(-back_to_weekday))
}
}
#[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 last_monday_of_may_is_memorial_day() {
let rule = LastWeekday::new(Weekday::Mon, Month::May);
assert!(rule.is_holiday(ymd(2024, Month::May, 27)));
assert!(rule.is_holiday(ymd(2025, Month::May, 26)));
assert!(rule.is_holiday(ymd(2026, Month::May, 25)));
}
#[test]
fn earlier_mondays_are_not_last() {
let rule = LastWeekday::new(Weekday::Mon, Month::May);
assert!(!rule.is_holiday(ymd(2026, Month::May, 4)));
assert!(!rule.is_holiday(ymd(2026, Month::May, 11)));
assert!(!rule.is_holiday(ymd(2026, Month::May, 18)));
assert!(rule.is_holiday(ymd(2026, Month::May, 25)));
}
#[test]
fn rejects_other_weekdays_and_months() {
let rule = LastWeekday::new(Weekday::Mon, Month::May);
assert!(!rule.is_holiday(ymd(2026, Month::May, 26)));
assert!(!rule.is_holiday(ymd(2026, Month::Jun, 29)));
}
#[test]
fn last_day_of_month_edge_cases() {
let rule = LastWeekday::new(Weekday::Fri, Month::Feb);
assert!(rule.is_holiday(ymd(2024, Month::Feb, 23)));
assert!(!rule.is_holiday(ymd(2024, Month::Feb, 16)));
}
}