use chrono::{NaiveDate, Datelike, Weekday};
use computus;
fn easter_ordinal(y: i32) -> u32 {
let easter = computus::gregorian(y).expect("computus error");
NaiveDate::from_ymd(y, easter.month, easter.day).ordinal()
}
pub fn is_bankholiday<T: Datelike>(date: &T) -> bool {
let day = date.weekday();
let (y, m, d) = (date.year(), date.month(), date.day());
match (y, m, d) {
(1995, 05, 01) => return false, (1995, 05, 08) => return true,
(1999, 12, 31) => return true, (2002, 05, 27) => return false, (2002, 06, 03) => return true,
(2002, 06, 04) => return true, (2011, 04, 29) => return true, (2012, 05, 28) => return false, (2012, 06, 04) => return true,
(2012, 06, 05) => return true, (2020, 05, 04) => return false, (2020, 05, 08) => return true,
(2022, 05, 30) => return false, (2022, 06, 02) => return true,
(2022, 06, 03) => return true, (2022, 09, 19) => return true, _ => {}
}
let new_years_day = |m, d| m == 1 && d == 1;
let new_years_sub = |m, d| m == 1 && d <= 3;
let early_may = |m, d| m == 5 && d <= 7;
let spring = |m, d| m == 5 && 31 - 7 < d;
let summer = |m, d| m == 8 && 31 - 7 < d;
let christmas_or_boxingday = |day, m, d| {
m == 12 &&
match day {
Weekday::Mon | Weekday::Tue => d >= 25 && d < 29,
_ => d >= 25 && d < 27,
}
};
match day {
Weekday::Sat | Weekday::Sun => false,
Weekday::Mon => {
new_years_sub(m, d) || early_may(m, d) || spring(m, d) || summer(m, d) ||
christmas_or_boxingday(day, m, d) ||
((m == 3 || m == 4) && easter_ordinal(y) + 1 == date.ordinal())
}
_ => {
new_years_day(m, d) || christmas_or_boxingday(day, m, d) ||
(day == Weekday::Fri && (m == 3 || m == 4) && easter_ordinal(y) == date.ordinal() + 2)
}
}
}
pub trait BankHoliday {
fn is_bankholiday(&self) -> bool;
}
impl<T: Datelike> BankHoliday for T {
fn is_bankholiday(&self) -> bool {
self::is_bankholiday(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::{NaiveDate, Datelike, Duration};
macro_rules! test {
($name:ident, $year:expr, $dates:expr) => {
#[test]
fn $name() {
let ymd = |y, m, d| NaiveDate::from_ymd(y, m, d);
let jan1 = ymd($year, 1, 1);
let days = if NaiveDate::from_ymd_opt($year, 2, 29).is_some() {
366
} else {
365
};
for i in 0..days {
let date = jan1 + Duration::days(i);
let holiday = date.is_bankholiday();
let expected = $dates.contains(&(date.day(), date.month()));
assert!(expected == holiday,
format!("Expected {} for {} but got {}", expected, date, holiday));
assert_eq!(is_bankholiday(&date), holiday);
}
}
}
}
test!(year_1999, 1999,
[(1, 1), (2, 4), (5, 4), (3, 5), (31, 5), (30, 8), (27, 12), (28, 12), (31, 12)]);
test!(year_2002, 2002,
[(1, 1), (29, 3), (1, 4), (6, 5), (3, 6), (4, 6), (26, 8), (25, 12), (26, 12)]);
test!(year_2012, 2012,
[(2, 1), (6, 4), (9, 4), (7, 5), (4, 6), (5, 6), (27, 8), (25, 12), (26, 12)]);
test!(year_2013, 2013,
[(1, 1), (29, 3), (1, 4), (6, 5), (27, 5), (26, 8), (25, 12), (26, 12)]);
test!(year_2014, 2014,
[(1, 1), (18, 4), (21, 4), (5, 5), (26, 5), (25, 8), (25, 12), (26, 12)]);
test!(year_2015, 2015,
[(1, 1), (3, 4), (6, 4), (4, 5), (25, 5), (31, 8), (25, 12), (28, 12)]);
test!(year_2016, 2016,
[(1, 1), (25, 3), (28, 3), (2, 5), (30, 5), (29, 8), (26, 12), (27, 12)]);
test!(year_2017, 2017,
[(2, 1), (14, 4), (17, 4), (1, 5), (29, 5), (28, 8), (25, 12), (26, 12)]);
test!(year_2018, 2018,
[(1, 1), (30, 3), (2, 4), (7, 5), (28, 5), (27, 8), (25, 12), (26, 12)]);
test!(year_2020, 2020,
[(1, 1), (10, 4), (13, 4), (8, 5), (25, 5), (31, 8), (25, 12), (28, 12)]);
test!(year_2022, 2022,
[(3, 1), (15, 4), (18, 4), (2, 5), (2, 6), (3, 6), (29, 8), (19, 9), (26, 12), (27, 12)]);
}