use crate::date::Date;
pub const SNAPSHOT_DATE: &str = "2026-05-23";
pub const COVERAGE: (i32, i32) = (2020, 2040);
pub const HOLIDAYS: &[(i32, u8, u8)] = &[
(2020, 1, 1), (2020, 1, 2), (2020, 4, 10), (2020, 4, 13), (2020, 5, 1), (2020, 5, 21), (2020, 6, 1), (2020, 8, 1), (2020, 12, 25), (2020, 12, 26), (2021, 1, 1), (2021, 1, 2), (2021, 4, 2), (2021, 4, 5), (2021, 5, 1), (2021, 5, 13), (2021, 5, 24), (2021, 8, 1), (2021, 12, 25), (2021, 12, 26), (2022, 1, 1), (2022, 1, 2), (2022, 4, 15), (2022, 4, 18), (2022, 5, 1), (2022, 5, 26), (2022, 6, 6), (2022, 8, 1), (2022, 12, 25), (2022, 12, 26), (2023, 1, 1), (2023, 1, 2), (2023, 4, 7), (2023, 4, 10), (2023, 5, 1), (2023, 5, 18), (2023, 5, 29), (2023, 8, 1), (2023, 12, 25), (2023, 12, 26), (2024, 1, 1), (2024, 1, 2), (2024, 3, 29), (2024, 4, 1), (2024, 5, 1), (2024, 5, 9), (2024, 5, 20), (2024, 8, 1), (2024, 12, 25), (2024, 12, 26), (2025, 1, 1), (2025, 1, 2), (2025, 4, 18), (2025, 4, 21), (2025, 5, 1), (2025, 5, 29), (2025, 6, 9), (2025, 8, 1), (2025, 12, 25), (2025, 12, 26), (2026, 1, 1), (2026, 1, 2), (2026, 4, 3), (2026, 4, 6), (2026, 5, 1), (2026, 5, 14), (2026, 5, 25), (2026, 8, 1), (2026, 12, 25), (2026, 12, 26), (2027, 1, 1), (2027, 1, 2), (2027, 3, 26), (2027, 3, 29), (2027, 5, 1), (2027, 5, 6), (2027, 5, 17), (2027, 8, 1), (2027, 12, 25), (2027, 12, 26), (2028, 1, 1), (2028, 1, 2), (2028, 4, 14), (2028, 4, 17), (2028, 5, 1), (2028, 5, 25), (2028, 6, 5), (2028, 8, 1), (2028, 12, 25), (2028, 12, 26), (2029, 1, 1), (2029, 1, 2), (2029, 3, 30), (2029, 4, 2), (2029, 5, 1), (2029, 5, 10), (2029, 5, 21), (2029, 8, 1), (2029, 12, 25), (2029, 12, 26), (2030, 1, 1), (2030, 1, 2), (2030, 4, 19), (2030, 4, 22), (2030, 5, 1), (2030, 5, 30), (2030, 6, 10), (2030, 8, 1), (2030, 12, 25), (2030, 12, 26), (2031, 1, 1), (2031, 1, 2), (2031, 4, 11), (2031, 4, 14), (2031, 5, 1), (2031, 5, 22), (2031, 6, 2), (2031, 8, 1), (2031, 12, 25), (2031, 12, 26), (2032, 1, 1), (2032, 1, 2), (2032, 3, 26), (2032, 3, 29), (2032, 5, 1), (2032, 5, 6), (2032, 5, 17), (2032, 8, 1), (2032, 12, 25), (2032, 12, 26), (2033, 1, 1), (2033, 1, 2), (2033, 4, 15), (2033, 4, 18), (2033, 5, 1), (2033, 5, 26), (2033, 6, 6), (2033, 8, 1), (2033, 12, 25), (2033, 12, 26), (2034, 1, 1), (2034, 1, 2), (2034, 4, 7), (2034, 4, 10), (2034, 5, 1), (2034, 5, 18), (2034, 5, 29), (2034, 8, 1), (2034, 12, 25), (2034, 12, 26), (2035, 1, 1), (2035, 1, 2), (2035, 3, 23), (2035, 3, 26), (2035, 5, 1), (2035, 5, 3), (2035, 5, 14), (2035, 8, 1), (2035, 12, 25), (2035, 12, 26), (2036, 1, 1), (2036, 1, 2), (2036, 4, 11), (2036, 4, 14), (2036, 5, 1), (2036, 5, 22), (2036, 6, 2), (2036, 8, 1), (2036, 12, 25), (2036, 12, 26), (2037, 1, 1), (2037, 1, 2), (2037, 4, 3), (2037, 4, 6), (2037, 5, 1), (2037, 5, 14), (2037, 5, 25), (2037, 8, 1), (2037, 12, 25), (2037, 12, 26), (2038, 1, 1), (2038, 1, 2), (2038, 4, 23), (2038, 4, 26), (2038, 5, 1), (2038, 6, 3), (2038, 6, 14), (2038, 8, 1), (2038, 12, 25), (2038, 12, 26), (2039, 1, 1), (2039, 1, 2), (2039, 4, 8), (2039, 4, 11), (2039, 5, 1), (2039, 5, 19), (2039, 5, 30), (2039, 8, 1), (2039, 12, 25), (2039, 12, 26), (2040, 1, 1), (2040, 1, 2), (2040, 3, 30), (2040, 4, 2), (2040, 5, 1), (2040, 5, 10), (2040, 5, 21), (2040, 8, 1), (2040, 12, 25), (2040, 12, 26), ];
#[must_use]
pub fn is_holiday(date: Date) -> bool {
let (first, last) = COVERAGE;
let year = date.year();
if year < first || year > last {
return false;
}
let key = (year, date.month(), date.day());
HOLIDAYS.binary_search(&key).is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn holidays_2024() {
let dates = [
(1, 1), (1, 2), (3, 29), (4, 1), (5, 1), (5, 9), (5, 20), (8, 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 SIX holiday",
);
}
let count_2024 = HOLIDAYS.iter().filter(|(y, _, _)| *y == 2024).count();
assert_eq!(count_2024, 10, "2024 must have exactly 10 SIX holidays");
}
#[test]
fn holidays_2026() {
let dates = [
(1, 1), (1, 2), (4, 3), (4, 6), (5, 1), (5, 14), (5, 25), (8, 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 SIX holiday",
);
}
let count_2026 = HOLIDAYS.iter().filter(|(y, _, _)| *y == 2026).count();
assert_eq!(count_2026, 10, "2026 must have exactly 10 SIX holidays");
}
#[test]
fn easter_derived_ascension_and_whit_monday() {
for year in [2024, 2026, 2030] {
let easter = Date::easter_sunday(year);
let ascension = easter.add_days(39);
let whit_monday = easter.add_days(50);
assert!(
is_holiday(ascension),
"Ascension {year} ({ascension:?}) must be a SIX holiday",
);
assert!(
is_holiday(whit_monday),
"Whit Monday {year} ({whit_monday:?}) must be a SIX holiday",
);
}
}
#[test]
fn no_weekend_observance_shift() {
assert!(
is_holiday(Date::ymd(2022, 1, 1).unwrap()),
"2022-01-01 (Sat) must remain a SIX holiday on its rule date",
);
assert!(
!is_holiday(Date::ymd(2022, 1, 3).unwrap()),
"2022-01-03 (Mon) must NOT be a SIX holiday — no observed shift",
);
assert!(
is_holiday(Date::ymd(2022, 1, 2).unwrap()),
"2022-01-02 must be a SIX holiday on its own rule (Berchtoldstag)",
);
}
#[test]
fn out_of_coverage_returns_false() {
assert!(
!is_holiday(Date::ymd(2019, 1, 1).unwrap()),
"below coverage must return false",
);
assert!(
!is_holiday(Date::ymd(2041, 1, 1).unwrap()),
"above coverage must return false",
);
}
#[test]
fn holidays_table_is_sorted_strictly_ascending() {
for pair in HOLIDAYS.windows(2) {
assert!(
pair[0] < pair[1],
"HOLIDAYS must be sorted strictly ascending; {:?} >= {:?}",
pair[0],
pair[1],
);
}
}
#[test]
fn holidays_total_count_matches_rule() {
assert_eq!(HOLIDAYS.len(), 10 * 21, "expected 210 SIX holiday entries");
}
}