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, 27), (2020, 1, 28), (2020, 4, 4), (2020, 4, 10), (2020, 4, 11), (2020, 4, 13), (2020, 4, 30), (2020, 5, 1), (2020, 6, 25), (2020, 7, 1), (2020, 10, 1), (2020, 10, 2), (2020, 10, 26), (2020, 12, 25), (2021, 1, 1), (2021, 2, 12), (2021, 2, 15), (2021, 4, 2), (2021, 4, 3), (2021, 4, 5), (2021, 4, 6), (2021, 5, 1), (2021, 5, 19), (2021, 6, 14), (2021, 7, 1), (2021, 9, 22), (2021, 10, 1), (2021, 10, 14), (2021, 12, 25), (2021, 12, 27), (2022, 1, 1), (2022, 2, 1), (2022, 2, 2), (2022, 2, 3), (2022, 4, 5), (2022, 4, 15), (2022, 4, 16), (2022, 4, 18), (2022, 5, 2), (2022, 5, 9), (2022, 6, 3), (2022, 7, 1), (2022, 9, 12), (2022, 10, 4), (2022, 12, 26), (2022, 12, 27), (2023, 1, 2), (2023, 1, 23), (2023, 1, 24), (2023, 1, 25), (2023, 4, 5), (2023, 4, 7), (2023, 4, 8), (2023, 4, 10), (2023, 5, 1), (2023, 5, 26), (2023, 6, 22), (2023, 7, 1), (2023, 10, 2), (2023, 10, 23), (2023, 12, 25), (2023, 12, 26), (2024, 1, 1), (2024, 2, 10), (2024, 2, 12), (2024, 2, 13), (2024, 3, 29), (2024, 3, 30), (2024, 4, 1), (2024, 4, 4), (2024, 5, 1), (2024, 5, 15), (2024, 6, 10), (2024, 7, 1), (2024, 9, 18), (2024, 10, 1), (2024, 10, 11), (2024, 12, 25), (2024, 12, 26), (2025, 1, 1), (2025, 1, 29), (2025, 1, 30), (2025, 1, 31), (2025, 4, 4), (2025, 4, 18), (2025, 4, 19), (2025, 4, 21), (2025, 5, 1), (2025, 5, 5), (2025, 7, 1), (2025, 10, 1), (2025, 10, 7), (2025, 10, 29), (2025, 12, 25), (2025, 12, 26), (2026, 1, 1), (2026, 2, 17), (2026, 2, 18), (2026, 2, 19), (2026, 4, 3), (2026, 4, 4), (2026, 4, 6), (2026, 4, 7), (2026, 5, 1), (2026, 5, 25), (2026, 6, 19), (2026, 7, 1), (2026, 10, 1), (2026, 10, 19), (2026, 12, 25), (2027, 1, 1), (2027, 2, 8), (2027, 2, 9), (2027, 3, 26), (2027, 3, 27), (2027, 3, 29), (2027, 4, 5), (2027, 5, 13), (2027, 6, 9), (2027, 7, 1), (2027, 9, 16), (2027, 10, 1), (2027, 10, 8), (2027, 12, 27), (2027, 12, 28), (2028, 1, 3), (2028, 1, 26), (2028, 1, 27), (2028, 1, 28), (2028, 4, 4), (2028, 4, 14), (2028, 4, 15), (2028, 4, 17), (2028, 5, 1), (2028, 5, 2), (2028, 5, 29), (2028, 7, 3), (2028, 10, 2), (2028, 10, 4), (2028, 10, 26), (2028, 12, 25), (2028, 12, 26), (2029, 1, 1), (2029, 2, 13), (2029, 2, 14), (2029, 2, 15), (2029, 3, 30), (2029, 3, 31), (2029, 4, 2), (2029, 4, 4), (2029, 5, 1), (2029, 5, 21), (2029, 7, 2), (2029, 9, 24), (2029, 10, 1), (2029, 10, 16), (2029, 12, 25), (2029, 12, 26), (2030, 1, 1), (2030, 2, 4), (2030, 2, 5), (2030, 2, 6), (2030, 4, 5), (2030, 4, 19), (2030, 4, 20), (2030, 4, 22), (2030, 5, 1), (2030, 5, 9), (2030, 6, 5), (2030, 7, 1), (2030, 9, 13), (2030, 10, 1), (2030, 12, 25), (2030, 12, 26), (2031, 1, 1), (2031, 1, 23), (2031, 1, 24), (2031, 4, 7), (2031, 4, 11), (2031, 4, 12), (2031, 4, 14), (2031, 5, 1), (2031, 5, 28), (2031, 6, 24), (2031, 7, 1), (2031, 10, 1), (2031, 10, 2), (2031, 10, 24), (2031, 12, 25), (2031, 12, 26), (2032, 1, 1), (2032, 2, 11), (2032, 2, 12), (2032, 2, 13), (2032, 3, 26), (2032, 3, 27), (2032, 3, 29), (2032, 4, 5), (2032, 5, 17), (2032, 7, 1), (2032, 9, 20), (2032, 10, 1), (2032, 10, 12), (2032, 12, 27), (2032, 12, 28), (2033, 1, 3), (2033, 1, 31), (2033, 2, 1), (2033, 2, 2), (2033, 4, 5), (2033, 4, 15), (2033, 4, 16), (2033, 4, 18), (2033, 5, 2), (2033, 5, 6), (2033, 6, 1), (2033, 7, 1), (2033, 9, 9), (2033, 12, 26), (2033, 12, 27), (2034, 1, 2), (2034, 2, 20), (2034, 2, 21), (2034, 2, 22), (2034, 4, 5), (2034, 4, 7), (2034, 4, 8), (2034, 4, 10), (2034, 5, 1), (2034, 5, 25), (2034, 6, 20), (2034, 7, 3), (2034, 9, 28), (2034, 10, 2), (2034, 10, 20), (2034, 12, 25), (2034, 12, 26), (2035, 1, 1), (2035, 2, 8), (2035, 2, 9), (2035, 3, 23), (2035, 3, 24), (2035, 3, 26), (2035, 4, 5), (2035, 5, 1), (2035, 5, 15), (2035, 6, 11), (2035, 7, 2), (2035, 9, 17), (2035, 10, 1), (2035, 10, 10), (2035, 12, 25), (2035, 12, 26), (2036, 1, 1), (2036, 1, 28), (2036, 1, 29), (2036, 1, 30), (2036, 4, 4), (2036, 4, 11), (2036, 4, 12), (2036, 4, 14), (2036, 5, 1), (2036, 5, 30), (2036, 7, 1), (2036, 9, 29), (2036, 10, 1), (2036, 10, 6), (2036, 12, 25), (2036, 12, 26), (2037, 1, 1), (2037, 2, 16), (2037, 2, 17), (2037, 2, 18), (2037, 4, 3), (2037, 4, 4), (2037, 4, 6), (2037, 4, 7), (2037, 5, 1), (2037, 5, 22), (2037, 6, 17), (2037, 7, 1), (2037, 9, 25), (2037, 10, 1), (2037, 12, 25), (2038, 1, 1), (2038, 2, 4), (2038, 2, 5), (2038, 4, 5), (2038, 4, 23), (2038, 4, 24), (2038, 4, 26), (2038, 5, 11), (2038, 6, 7), (2038, 7, 1), (2038, 9, 14), (2038, 10, 1), (2038, 10, 6), (2038, 12, 27), (2038, 12, 28), (2039, 1, 3), (2039, 1, 24), (2039, 1, 25), (2039, 1, 26), (2039, 4, 5), (2039, 4, 8), (2039, 4, 9), (2039, 4, 11), (2039, 5, 2), (2039, 5, 27), (2039, 7, 1), (2039, 10, 3), (2039, 10, 4), (2039, 10, 25), (2039, 12, 26), (2039, 12, 27), (2040, 1, 2), (2040, 2, 13), (2040, 2, 14), (2040, 2, 15), (2040, 3, 30), (2040, 3, 31), (2040, 4, 2), (2040, 4, 4), (2040, 5, 1), (2040, 5, 18), (2040, 6, 14), (2040, 7, 2), (2040, 9, 21), (2040, 10, 1), (2040, 12, 25), (2040, 12, 26), ];
#[must_use]
pub fn is_holiday(date: Date) -> bool {
let key = (date.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), (2, 12), (2, 13), (3, 29), (3, 30), (4, 1), (4, 4), (5, 1), (5, 15), (6, 10), (7, 1), (9, 18), (10, 1), (10, 11), (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 an HKEX holiday",
);
}
}
#[test]
fn lunar_new_year_2026() {
assert!(is_holiday(Date::ymd(2026, 2, 17).unwrap()));
assert!(is_holiday(Date::ymd(2026, 2, 18).unwrap()));
assert!(is_holiday(Date::ymd(2026, 2, 19).unwrap()));
}
#[test]
fn out_of_coverage_returns_false() {
assert!(!is_holiday(Date::ymd(2019, 1, 1).unwrap()));
assert!(!is_holiday(Date::ymd(2041, 1, 1).unwrap()));
}
#[test]
fn holidays_table_is_sorted() {
for window in HOLIDAYS.windows(2) {
assert!(
window[0] < window[1],
"HOLIDAYS not strictly sorted at {:?} -> {:?}",
window[0],
window[1],
);
}
}
#[test]
fn non_holidays_are_not_marked() {
assert!(!is_holiday(Date::ymd(2024, 3, 15).unwrap()));
assert!(!is_holiday(Date::ymd(2026, 8, 1).unwrap()));
}
#[test]
fn snapshot_metadata_is_consistent() {
assert_eq!(SNAPSHOT_DATE, "2026-05-23");
assert_eq!(COVERAGE, (2020, 2040));
for &(y, _, _) in HOLIDAYS {
assert!(
y >= COVERAGE.0 && y <= COVERAGE.1,
"row year {y} outside COVERAGE {COVERAGE:?}",
);
}
}
#[test]
fn every_covered_year_has_new_years_day_entry() {
for year in COVERAGE.0..=COVERAGE.1 {
assert!(
HOLIDAYS.iter().any(|&(y, _, _)| y == year),
"no holidays listed for {year}",
);
}
}
}