use crate::date::Date;
#[must_use]
pub fn is_holiday(date: Date) -> bool {
let year = date.year();
let easter = Date::easter_sunday(year);
let easter_monday = easter.add_days(1);
let ascension = easter.add_days(39);
let whit_monday = easter.add_days(50);
let fixed = matches!(
(date.month(), date.day()),
(1 | 5 | 11, 1) | (6, 23) | (8, 15) | (12, 25 | 26)
);
let europe_day = date == Date::ymd_unchecked(year, 5, 9) && year >= 2019;
fixed || europe_day || date == easter_monday || date == ascension || date == whit_monday
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn holidays_2024() {
let dates = [
(1, 1), (4, 1), (5, 1), (5, 9), (5, 20), (6, 23), (8, 15), (11, 1), (12, 25), (12, 26), ];
for (m, d) in dates {
assert!(
is_holiday(Date::ymd(2024, m, d).unwrap()),
"2024-{m:02}-{d:02} must be a Luxembourg holiday",
);
}
let collision = Date::ymd(2024, 5, 9).unwrap();
assert_eq!(collision, Date::easter_sunday(2024).add_days(39));
assert!(is_holiday(collision));
}
#[test]
fn holidays_2026() {
let dates = [
(1, 1), (4, 6), (5, 1), (5, 9), (5, 14), (5, 25), (6, 23), (8, 15), (11, 1), (12, 25), (12, 26), ];
for (m, d) in dates {
assert!(
is_holiday(Date::ymd(2026, m, d).unwrap()),
"2026-{m:02}-{d:02} must be a Luxembourg holiday",
);
}
}
#[test]
fn non_holidays_2026() {
let dates = [
(1, 2), (5, 8), (12, 24), (12, 27), ];
for (m, d) in dates {
assert!(
!is_holiday(Date::ymd(2026, m, d).unwrap()),
"2026-{m:02}-{d:02} must NOT be a Luxembourg holiday",
);
}
}
#[test]
fn europe_day_cutover_in_2019() {
assert!(
!is_holiday(Date::ymd(2018, 5, 9).unwrap()),
"2018-05-09 must NOT be a Luxembourg holiday (Europe Day not yet gazetted)",
);
assert!(
is_holiday(Date::ymd(2019, 5, 9).unwrap()),
"2019-05-09 must be a Luxembourg holiday (first year of Europe Day)",
);
assert!(
is_holiday(Date::ymd(2024, 5, 9).unwrap()),
"2024-05-09 must be a Luxembourg holiday (Europe Day; also Ascension in 2024)",
);
}
#[test]
fn whit_monday_is_a_holiday() {
let easter = Date::easter_sunday(2026);
let whit_monday = easter.add_days(50);
assert_eq!(whit_monday, Date::ymd(2026, 5, 25).unwrap());
assert!(is_holiday(whit_monday));
for year in [2024, 2025, 2027, 2028] {
let wm = Date::easter_sunday(year).add_days(50);
assert!(
is_holiday(wm),
"Whit Monday {year} ({wm:?}) must be a Luxembourg holiday",
);
}
}
#[test]
fn good_friday_is_not_a_holiday() {
let easter_2024 = Date::easter_sunday(2024);
let good_friday_2024 = easter_2024.add_days(-2);
assert_eq!(good_friday_2024, Date::ymd(2024, 3, 29).unwrap());
assert!(
!is_holiday(good_friday_2024),
"Good Friday 2024 (2024-03-29) must NOT be a Luxembourg holiday",
);
for year in [2025, 2026, 2027, 2028] {
let gf = Date::easter_sunday(year).add_days(-2);
assert!(
!is_holiday(gf),
"Good Friday {year} ({gf:?}) must NOT be a Luxembourg holiday",
);
}
}
#[test]
fn independence_from_weekday() {
assert!(is_holiday(Date::ymd(2027, 1, 1).unwrap()));
assert!(is_holiday(Date::ymd(2028, 1, 1).unwrap()));
assert!(is_holiday(Date::ymd(2027, 12, 25).unwrap()));
assert!(is_holiday(Date::ymd(2027, 12, 26).unwrap()));
assert!(is_holiday(Date::ymd(2024, 6, 23).unwrap()));
}
#[test]
fn easter_anchored_dates_match_computus() {
let cases = [
(2024, 3, 31), (2025, 4, 20), (2026, 4, 5), (2030, 4, 21), (2038, 4, 25), ];
for (y, m, d) in cases {
let easter = Date::ymd(y, m, d).unwrap();
assert_eq!(
Date::easter_sunday(y),
easter,
"Computus disagrees with the published Easter date for {y}",
);
assert!(
is_holiday(easter.add_days(1)),
"Easter Monday {y} must be a Luxembourg holiday",
);
assert!(
is_holiday(easter.add_days(39)),
"Ascension {y} must be a Luxembourg holiday",
);
assert!(
is_holiday(easter.add_days(50)),
"Whit Monday {y} must be a Luxembourg holiday",
);
}
}
}