use std::collections::BTreeSet;
use chrono::Datelike;
use chrono::Duration;
use chrono::NaiveDate;
use chrono::Weekday;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum HolidayCalendar {
UnitedStates,
UnitedKingdom,
Target,
Tokyo,
}
impl std::fmt::Display for HolidayCalendar {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnitedStates => write!(f, "US"),
Self::UnitedKingdom => write!(f, "UK"),
Self::Target => write!(f, "TARGET"),
Self::Tokyo => write!(f, "Tokyo"),
}
}
}
impl HolidayCalendar {
fn is_holiday(self, date: NaiveDate) -> bool {
match self {
Self::UnitedStates => is_us_holiday(date),
Self::UnitedKingdom => is_uk_holiday(date),
Self::Target => is_target_holiday(date),
Self::Tokyo => is_tokyo_holiday(date),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Calendar {
calendars: Vec<HolidayCalendar>,
extra_holidays: BTreeSet<NaiveDate>,
}
impl Calendar {
pub fn new(kind: HolidayCalendar) -> Self {
Self {
calendars: vec![kind],
extra_holidays: BTreeSet::new(),
}
}
pub fn joint(calendars: impl IntoIterator<Item = HolidayCalendar>) -> Self {
Self {
calendars: calendars.into_iter().collect(),
extra_holidays: BTreeSet::new(),
}
}
pub fn add_holiday(&mut self, date: NaiveDate) {
self.extra_holidays.insert(date);
}
pub fn remove_holiday(&mut self, date: NaiveDate) {
self.extra_holidays.remove(&date);
}
pub fn is_weekend(&self, date: NaiveDate) -> bool {
matches!(date.weekday(), Weekday::Sat | Weekday::Sun)
}
pub fn is_holiday(&self, date: NaiveDate) -> bool {
if self.extra_holidays.contains(&date) {
return true;
}
self.calendars.iter().any(|c| c.is_holiday(date))
}
pub fn is_business_day(&self, date: NaiveDate) -> bool {
!self.is_weekend(date) && !self.is_holiday(date)
}
pub fn business_days_between(&self, d1: NaiveDate, d2: NaiveDate) -> i64 {
let step = if d2 >= d1 { 1 } else { -1 };
let delta = Duration::days(step);
let mut count: i64 = 0;
let mut d = d1;
while d != d2 {
if self.is_business_day(d) {
count += step;
}
d += delta;
}
count
}
pub fn advance(&self, mut date: NaiveDate, business_days: i32) -> NaiveDate {
let step = if business_days >= 0 { 1 } else { -1 };
let delta = Duration::days(step);
let mut remaining = business_days.unsigned_abs();
while remaining > 0 {
date += delta;
if self.is_business_day(date) {
remaining -= 1;
}
}
date
}
}
fn easter_sunday(year: i32) -> NaiveDate {
let a = year % 19;
let b = year / 100;
let c = year % 100;
let d = b / 4;
let e = b % 4;
let f = (b + 8) / 25;
let g = (b - f + 1) / 3;
let h = (19 * a + b - d - g + 15) % 30;
let i = c / 4;
let k = c % 4;
let l = (32 + 2 * e + 2 * i - h - k) % 7;
let m = (a + 11 * h + 22 * l) / 451;
let month = (h + l - 7 * m + 114) / 31;
let day = ((h + l - 7 * m + 114) % 31) + 1;
NaiveDate::from_ymd_opt(year, month as u32, day as u32).unwrap()
}
fn vernal_equinox_day(year: i32) -> u32 {
let y = year as f64;
let base = if year <= 1979 {
20.8357
} else if year <= 2099 {
20.8431
} else {
21.851
};
(base + 0.242194 * (y - 1980.0) - ((y - 1980.0) / 4.0).floor()) as u32
}
fn autumnal_equinox_day(year: i32) -> u32 {
let y = year as f64;
let base = if year <= 1979 {
23.2588
} else if year <= 2099 {
23.2488
} else {
24.2488
};
(base + 0.242194 * (y - 1980.0) - ((y - 1980.0) / 4.0).floor()) as u32
}
fn is_us_holiday(date: NaiveDate) -> bool {
let (y, m, d) = (date.year(), date.month(), date.day());
let w = date.weekday();
use Weekday::*;
if (m == 1 && d == 1 && w != Sat && w != Sun)
|| (m == 12 && d == 31 && w == Fri)
|| (m == 1 && d == 2 && w == Mon)
{
return true;
}
if y >= 1983 && m == 1 && w == Mon && (15..=21).contains(&d) {
return true;
}
if m == 2 && w == Mon && (15..=21).contains(&d) {
return true;
}
if m == 5 && w == Mon && d >= 25 {
return true;
}
if y >= 2021
&& ((m == 6 && d == 19 && w != Sat && w != Sun)
|| (m == 6 && d == 18 && w == Fri)
|| (m == 6 && d == 20 && w == Mon))
{
return true;
}
if (m == 7 && d == 4 && w != Sat && w != Sun)
|| (m == 7 && d == 3 && w == Fri)
|| (m == 7 && d == 5 && w == Mon)
{
return true;
}
if m == 9 && w == Mon && d <= 7 {
return true;
}
if m == 10 && w == Mon && (8..=14).contains(&d) {
return true;
}
if (m == 11 && d == 11 && w != Sat && w != Sun)
|| (m == 11 && d == 10 && w == Fri)
|| (m == 11 && d == 12 && w == Mon)
{
return true;
}
if m == 11 && w == Thu && (22..=28).contains(&d) {
return true;
}
if (m == 12 && d == 25 && w != Sat && w != Sun)
|| (m == 12 && d == 24 && w == Fri)
|| (m == 12 && d == 26 && w == Mon)
{
return true;
}
false
}
fn is_uk_holiday(date: NaiveDate) -> bool {
let (y, m, d) = (date.year(), date.month(), date.day());
let w = date.weekday();
use Weekday::*;
if (m == 1 && d == 1 && w != Sat && w != Sun)
|| (m == 1 && d == 2 && w == Mon)
|| (m == 1 && d == 3 && w == Mon)
{
return true;
}
let easter = easter_sunday(y);
if date == easter - Duration::days(2) {
return true;
}
if date == easter + Duration::days(1) {
return true;
}
if m == 5 && w == Mon && d <= 7 {
return true;
}
if m == 5 && w == Mon && d >= 25 {
return true;
}
if m == 8 && w == Mon && d >= 25 {
return true;
}
if (d == 25 || (d == 27 && matches!(w, Mon | Tue))) && m == 12 {
return true;
}
if (d == 26 || (d == 28 && matches!(w, Mon | Tue))) && m == 12 {
return true;
}
false
}
fn is_target_holiday(date: NaiveDate) -> bool {
let (y, m, d) = (date.year(), date.month(), date.day());
if m == 1 && d == 1 {
return true;
}
let easter = easter_sunday(y);
if date == easter - Duration::days(2) {
return true;
}
if date == easter + Duration::days(1) {
return true;
}
if m == 5 && d == 1 {
return true;
}
if m == 12 && d == 25 {
return true;
}
if m == 12 && d == 26 {
return true;
}
false
}
fn is_tokyo_holiday(date: NaiveDate) -> bool {
let (y, m, d) = (date.year(), date.month(), date.day());
let w = date.weekday();
use Weekday::*;
if m == 1 && d <= 3 {
return true;
}
if m == 1 && w == Mon && (8..=14).contains(&d) {
return true;
}
if is_observed_jp(date, m, d, 2, 11) {
return true;
}
if y >= 2020 && is_observed_jp(date, m, d, 2, 23) {
return true;
}
if (1989..=2018).contains(&y) && is_observed_jp(date, m, d, 12, 23) {
return true;
}
let ve = vernal_equinox_day(y);
if is_observed_jp(date, m, d, 3, ve) {
return true;
}
if is_observed_jp(date, m, d, 4, 29) {
return true;
}
if m == 5 && d == 3 && w != Sat && w != Sun {
return true;
}
if m == 5 && d == 4 && w != Sat && w != Sun {
return true;
}
if is_observed_jp(date, m, d, 5, 5) {
return true;
}
if m == 5 && d == 6 && matches!(w, Mon | Tue | Wed) {
return true;
}
if m == 7 && w == Mon && (15..=21).contains(&d) {
return true;
}
if y >= 2016 && is_observed_jp(date, m, d, 8, 11) {
return true;
}
if m == 9 && w == Mon && (15..=21).contains(&d) {
return true;
}
let ae = autumnal_equinox_day(y);
if is_observed_jp(date, m, d, 9, ae) {
return true;
}
if m == 9 && w == Tue {
let prev = date - Duration::days(1);
let next = date + Duration::days(1);
if prev.weekday() == Mon
&& (15..=21).contains(&prev.day())
&& next.month() == 9
&& next.day() == ae
{
return true;
}
}
if m == 10 && w == Mon && (8..=14).contains(&d) {
return true;
}
if is_observed_jp(date, m, d, 11, 3) {
return true;
}
if is_observed_jp(date, m, d, 11, 23) {
return true;
}
false
}
fn is_observed_jp(date: NaiveDate, m: u32, d: u32, hol_month: u32, hol_day: u32) -> bool {
if m == hol_month && d == hol_day && date.weekday() != Weekday::Sun {
return true;
}
if m == hol_month
&& d == hol_day + 1
&& date.weekday() == Weekday::Mon
&& NaiveDate::from_ymd_opt(date.year(), hol_month, hol_day)
.is_some_and(|h| h.weekday() == Weekday::Sun)
{
return true;
}
false
}
#[cfg(test)]
mod tests {
use chrono::NaiveDate;
use super::*;
#[test]
fn weekend_detection() {
let cal = Calendar::new(HolidayCalendar::UnitedStates);
assert!(cal.is_weekend(NaiveDate::from_ymd_opt(2024, 1, 6).unwrap()));
assert!(cal.is_weekend(NaiveDate::from_ymd_opt(2024, 1, 7).unwrap()));
assert!(!cal.is_weekend(NaiveDate::from_ymd_opt(2024, 1, 8).unwrap()));
}
#[test]
fn us_independence_day() {
let cal = Calendar::new(HolidayCalendar::UnitedStates);
assert!(cal.is_holiday(NaiveDate::from_ymd_opt(2024, 7, 4).unwrap()));
}
#[test]
fn target_christmas() {
let cal = Calendar::new(HolidayCalendar::Target);
assert!(cal.is_holiday(NaiveDate::from_ymd_opt(2024, 12, 25).unwrap()));
}
#[test]
fn manual_holiday_addition() {
let mut cal = Calendar::new(HolidayCalendar::UnitedStates);
let date = NaiveDate::from_ymd_opt(2024, 3, 15).unwrap();
assert!(!cal.is_holiday(date));
cal.add_holiday(date);
assert!(cal.is_holiday(date));
cal.remove_holiday(date);
assert!(!cal.is_holiday(date));
}
}