use chrono::{Datelike, Duration, NaiveDate, Weekday};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum Region {
US,
DE,
GB,
CN,
JP,
IN,
BR,
MX,
AU,
SG,
KR,
FR,
IT,
ES,
CA,
}
impl std::fmt::Display for Region {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Region::US => write!(f, "United States"),
Region::DE => write!(f, "Germany"),
Region::GB => write!(f, "United Kingdom"),
Region::CN => write!(f, "China"),
Region::JP => write!(f, "Japan"),
Region::IN => write!(f, "India"),
Region::BR => write!(f, "Brazil"),
Region::MX => write!(f, "Mexico"),
Region::AU => write!(f, "Australia"),
Region::SG => write!(f, "Singapore"),
Region::KR => write!(f, "South Korea"),
Region::FR => write!(f, "France"),
Region::IT => write!(f, "Italy"),
Region::ES => write!(f, "Spain"),
Region::CA => write!(f, "Canada"),
}
}
}
#[derive(Debug, Clone)]
pub struct Holiday {
pub name: String,
pub date: NaiveDate,
pub activity_multiplier: f64,
pub is_bank_holiday: bool,
}
impl Holiday {
pub fn new(name: impl Into<String>, date: NaiveDate, multiplier: f64) -> Self {
Self {
name: name.into(),
date,
activity_multiplier: multiplier,
is_bank_holiday: true,
}
}
pub fn with_bank_holiday(mut self, is_bank_holiday: bool) -> Self {
self.is_bank_holiday = is_bank_holiday;
self
}
}
#[derive(Debug, Clone)]
pub struct HolidayCalendar {
pub region: Region,
pub year: i32,
pub holidays: Vec<Holiday>,
}
impl HolidayCalendar {
pub fn new(region: Region, year: i32) -> Self {
Self {
region,
year,
holidays: Vec::new(),
}
}
pub fn for_region(region: Region, year: i32) -> Self {
match region {
Region::US => Self::us_holidays(year),
Region::DE => Self::de_holidays(year),
Region::GB => Self::gb_holidays(year),
Region::CN => Self::cn_holidays(year),
Region::JP => Self::jp_holidays(year),
Region::IN => Self::in_holidays(year),
Region::BR => Self::br_holidays(year),
Region::MX => Self::mx_holidays(year),
Region::AU => Self::au_holidays(year),
Region::SG => Self::sg_holidays(year),
Region::KR => Self::kr_holidays(year),
Region::FR => Self::fr_holidays(year),
Region::IT => Self::it_holidays(year),
Region::ES => Self::es_holidays(year),
Region::CA => Self::ca_holidays(year),
}
}
pub fn is_holiday(&self, date: NaiveDate) -> bool {
self.holidays.iter().any(|h| h.date == date)
}
pub fn get_multiplier(&self, date: NaiveDate) -> f64 {
self.holidays
.iter()
.find(|h| h.date == date)
.map(|h| h.activity_multiplier)
.unwrap_or(1.0)
}
pub fn get_holidays(&self, date: NaiveDate) -> Vec<&Holiday> {
self.holidays.iter().filter(|h| h.date == date).collect()
}
pub fn add_holiday(&mut self, holiday: Holiday) {
self.holidays.push(holiday);
}
pub fn all_dates(&self) -> Vec<NaiveDate> {
self.holidays.iter().map(|h| h.date).collect()
}
pub fn from_country_pack(pack: &crate::country::schema::CountryPack, year: i32) -> Self {
let region = match pack.country_code.as_str() {
"US" => Region::US,
"DE" => Region::DE,
"GB" => Region::GB,
"CN" => Region::CN,
"JP" => Region::JP,
"IN" => Region::IN,
"BR" => Region::BR,
"MX" => Region::MX,
"AU" => Region::AU,
"SG" => Region::SG,
"KR" => Region::KR,
"FR" => Region::FR,
"IT" => Region::IT,
"ES" => Region::ES,
"CA" => Region::CA,
_ => Region::US,
};
let mut cal = Self::new(region, year);
let holidays = &pack.holidays;
for h in &holidays.fixed {
if let Some(date) = NaiveDate::from_ymd_opt(year, h.month, h.day) {
let date = if h.observe_weekend_rule {
Self::observe_weekend(date)
} else {
date
};
cal.add_holiday(Holiday::new(&h.name, date, h.activity_multiplier));
}
}
if let Some(easter) = crate::country::easter::compute_easter(year) {
for h in &holidays.easter_relative {
let date = easter + Duration::days(h.offset_days as i64);
cal.add_holiday(Holiday::new(&h.name, date, h.activity_multiplier));
}
}
for h in &holidays.nth_weekday {
if let Some(weekday) = Self::parse_weekday(&h.weekday) {
let date = Self::nth_weekday_of_month(year, h.month, weekday, h.occurrence);
let date = date + Duration::days(h.offset_days as i64);
cal.add_holiday(Holiday::new(&h.name, date, h.activity_multiplier));
}
}
for h in &holidays.last_weekday {
if let Some(weekday) = Self::parse_weekday(&h.weekday) {
let date = Self::last_weekday_of_month(year, h.month, weekday);
cal.add_holiday(Holiday::new(&h.name, date, h.activity_multiplier));
}
}
for h in &holidays.lunar {
if let Some(dates) =
crate::country::lunar::resolve_lunar_holiday(&h.algorithm, year, h.duration_days)
{
for date in dates {
cal.add_holiday(Holiday::new(&h.name, date, h.activity_multiplier));
}
}
}
cal
}
fn parse_weekday(s: &str) -> Option<Weekday> {
match s.to_lowercase().as_str() {
"monday" | "mon" => Some(Weekday::Mon),
"tuesday" | "tue" => Some(Weekday::Tue),
"wednesday" | "wed" => Some(Weekday::Wed),
"thursday" | "thu" => Some(Weekday::Thu),
"friday" | "fri" => Some(Weekday::Fri),
"saturday" | "sat" => Some(Weekday::Sat),
"sunday" | "sun" => Some(Weekday::Sun),
_ => None,
}
}
fn us_holidays(year: i32) -> Self {
let mut cal = Self::new(Region::US, year);
let new_years = NaiveDate::from_ymd_opt(year, 1, 1).expect("valid date components");
cal.add_holiday(Holiday::new(
"New Year's Day",
Self::observe_weekend(new_years),
0.02,
));
let mlk = Self::nth_weekday_of_month(year, 1, Weekday::Mon, 3);
cal.add_holiday(Holiday::new("Martin Luther King Jr. Day", mlk, 0.1));
let presidents = Self::nth_weekday_of_month(year, 2, Weekday::Mon, 3);
cal.add_holiday(Holiday::new("Presidents' Day", presidents, 0.1));
let memorial = Self::last_weekday_of_month(year, 5, Weekday::Mon);
cal.add_holiday(Holiday::new("Memorial Day", memorial, 0.05));
let juneteenth = NaiveDate::from_ymd_opt(year, 6, 19).expect("valid date components");
cal.add_holiday(Holiday::new(
"Juneteenth",
Self::observe_weekend(juneteenth),
0.1,
));
let independence = NaiveDate::from_ymd_opt(year, 7, 4).expect("valid date components");
cal.add_holiday(Holiday::new(
"Independence Day",
Self::observe_weekend(independence),
0.02,
));
let labor = Self::nth_weekday_of_month(year, 9, Weekday::Mon, 1);
cal.add_holiday(Holiday::new("Labor Day", labor, 0.05));
let columbus = Self::nth_weekday_of_month(year, 10, Weekday::Mon, 2);
cal.add_holiday(Holiday::new("Columbus Day", columbus, 0.2));
let veterans = NaiveDate::from_ymd_opt(year, 11, 11).expect("valid date components");
cal.add_holiday(Holiday::new(
"Veterans Day",
Self::observe_weekend(veterans),
0.1,
));
let thanksgiving = Self::nth_weekday_of_month(year, 11, Weekday::Thu, 4);
cal.add_holiday(Holiday::new("Thanksgiving", thanksgiving, 0.02));
cal.add_holiday(Holiday::new(
"Day after Thanksgiving",
thanksgiving + Duration::days(1),
0.1,
));
let christmas_eve = NaiveDate::from_ymd_opt(year, 12, 24).expect("valid date components");
cal.add_holiday(Holiday::new("Christmas Eve", christmas_eve, 0.1));
let christmas = NaiveDate::from_ymd_opt(year, 12, 25).expect("valid date components");
cal.add_holiday(Holiday::new(
"Christmas Day",
Self::observe_weekend(christmas),
0.02,
));
let new_years_eve = NaiveDate::from_ymd_opt(year, 12, 31).expect("valid date components");
cal.add_holiday(Holiday::new("New Year's Eve", new_years_eve, 0.1));
cal
}
fn de_holidays(year: i32) -> Self {
let mut cal = Self::new(Region::DE, year);
cal.add_holiday(Holiday::new(
"Neujahr",
NaiveDate::from_ymd_opt(year, 1, 1).expect("valid date components"),
0.02,
));
let easter = Self::easter_date(year);
cal.add_holiday(Holiday::new("Karfreitag", easter - Duration::days(2), 0.02));
cal.add_holiday(Holiday::new(
"Ostermontag",
easter + Duration::days(1),
0.02,
));
cal.add_holiday(Holiday::new(
"Tag der Arbeit",
NaiveDate::from_ymd_opt(year, 5, 1).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Christi Himmelfahrt",
easter + Duration::days(39),
0.02,
));
cal.add_holiday(Holiday::new(
"Pfingstmontag",
easter + Duration::days(50),
0.02,
));
cal.add_holiday(Holiday::new(
"Tag der Deutschen Einheit",
NaiveDate::from_ymd_opt(year, 10, 3).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"1. Weihnachtstag",
NaiveDate::from_ymd_opt(year, 12, 25).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"2. Weihnachtstag",
NaiveDate::from_ymd_opt(year, 12, 26).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Silvester",
NaiveDate::from_ymd_opt(year, 12, 31).expect("valid date components"),
0.1,
));
cal
}
fn gb_holidays(year: i32) -> Self {
let mut cal = Self::new(Region::GB, year);
let new_years = NaiveDate::from_ymd_opt(year, 1, 1).expect("valid date components");
cal.add_holiday(Holiday::new(
"New Year's Day",
Self::observe_weekend(new_years),
0.02,
));
let easter = Self::easter_date(year);
cal.add_holiday(Holiday::new(
"Good Friday",
easter - Duration::days(2),
0.02,
));
cal.add_holiday(Holiday::new(
"Easter Monday",
easter + Duration::days(1),
0.02,
));
let early_may = Self::nth_weekday_of_month(year, 5, Weekday::Mon, 1);
cal.add_holiday(Holiday::new("Early May Bank Holiday", early_may, 0.02));
let spring = Self::last_weekday_of_month(year, 5, Weekday::Mon);
cal.add_holiday(Holiday::new("Spring Bank Holiday", spring, 0.02));
let summer = Self::last_weekday_of_month(year, 8, Weekday::Mon);
cal.add_holiday(Holiday::new("Summer Bank Holiday", summer, 0.02));
let christmas = NaiveDate::from_ymd_opt(year, 12, 25).expect("valid date components");
cal.add_holiday(Holiday::new(
"Christmas Day",
Self::observe_weekend(christmas),
0.02,
));
let boxing = NaiveDate::from_ymd_opt(year, 12, 26).expect("valid date components");
cal.add_holiday(Holiday::new(
"Boxing Day",
Self::observe_weekend(boxing),
0.02,
));
cal
}
fn cn_holidays(year: i32) -> Self {
let mut cal = Self::new(Region::CN, year);
cal.add_holiday(Holiday::new(
"New Year",
NaiveDate::from_ymd_opt(year, 1, 1).expect("valid date components"),
0.05,
));
let cny = Self::approximate_chinese_new_year(year);
for i in 0..7 {
cal.add_holiday(Holiday::new(
if i == 0 {
"Spring Festival"
} else {
"Spring Festival Holiday"
},
cny + Duration::days(i),
0.02,
));
}
cal.add_holiday(Holiday::new(
"Qingming Festival",
NaiveDate::from_ymd_opt(year, 4, 5).expect("valid date components"),
0.05,
));
for i in 0..3 {
cal.add_holiday(Holiday::new(
if i == 0 {
"Labor Day"
} else {
"Labor Day Holiday"
},
NaiveDate::from_ymd_opt(year, 5, 1).expect("valid date components")
+ Duration::days(i),
0.05,
));
}
cal.add_holiday(Holiday::new(
"Dragon Boat Festival",
NaiveDate::from_ymd_opt(year, 6, 10).expect("valid date components"),
0.05,
));
cal.add_holiday(Holiday::new(
"Mid-Autumn Festival",
NaiveDate::from_ymd_opt(year, 9, 15).expect("valid date components"),
0.05,
));
for i in 0..7 {
cal.add_holiday(Holiday::new(
if i == 0 {
"National Day"
} else {
"National Day Holiday"
},
NaiveDate::from_ymd_opt(year, 10, 1).expect("valid date components")
+ Duration::days(i),
0.02,
));
}
cal
}
fn jp_holidays(year: i32) -> Self {
let mut cal = Self::new(Region::JP, year);
cal.add_holiday(Holiday::new(
"Ganjitsu (New Year)",
NaiveDate::from_ymd_opt(year, 1, 1).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"New Year Holiday",
NaiveDate::from_ymd_opt(year, 1, 2).expect("valid date components"),
0.05,
));
cal.add_holiday(Holiday::new(
"New Year Holiday",
NaiveDate::from_ymd_opt(year, 1, 3).expect("valid date components"),
0.05,
));
let seijin = Self::nth_weekday_of_month(year, 1, Weekday::Mon, 2);
cal.add_holiday(Holiday::new("Seijin no Hi", seijin, 0.05));
cal.add_holiday(Holiday::new(
"Kenkoku Kinen no Hi",
NaiveDate::from_ymd_opt(year, 2, 11).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Tenno Tanjobi",
NaiveDate::from_ymd_opt(year, 2, 23).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Shunbun no Hi",
NaiveDate::from_ymd_opt(year, 3, 20).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Showa no Hi",
NaiveDate::from_ymd_opt(year, 4, 29).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Kenpo Kinenbi",
NaiveDate::from_ymd_opt(year, 5, 3).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Midori no Hi",
NaiveDate::from_ymd_opt(year, 5, 4).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Kodomo no Hi",
NaiveDate::from_ymd_opt(year, 5, 5).expect("valid date components"),
0.02,
));
let umi = Self::nth_weekday_of_month(year, 7, Weekday::Mon, 3);
cal.add_holiday(Holiday::new("Umi no Hi", umi, 0.05));
cal.add_holiday(Holiday::new(
"Yama no Hi",
NaiveDate::from_ymd_opt(year, 8, 11).expect("valid date components"),
0.05,
));
let keiro = Self::nth_weekday_of_month(year, 9, Weekday::Mon, 3);
cal.add_holiday(Holiday::new("Keiro no Hi", keiro, 0.05));
cal.add_holiday(Holiday::new(
"Shubun no Hi",
NaiveDate::from_ymd_opt(year, 9, 23).expect("valid date components"),
0.02,
));
let sports = Self::nth_weekday_of_month(year, 10, Weekday::Mon, 2);
cal.add_holiday(Holiday::new("Sports Day", sports, 0.05));
cal.add_holiday(Holiday::new(
"Bunka no Hi",
NaiveDate::from_ymd_opt(year, 11, 3).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Kinro Kansha no Hi",
NaiveDate::from_ymd_opt(year, 11, 23).expect("valid date components"),
0.02,
));
cal
}
fn in_holidays(year: i32) -> Self {
let mut cal = Self::new(Region::IN, year);
cal.add_holiday(Holiday::new(
"Republic Day",
NaiveDate::from_ymd_opt(year, 1, 26).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Holi",
NaiveDate::from_ymd_opt(year, 3, 10).expect("valid date components"),
0.05,
));
let easter = Self::easter_date(year);
cal.add_holiday(Holiday::new(
"Good Friday",
easter - Duration::days(2),
0.05,
));
cal.add_holiday(Holiday::new(
"Independence Day",
NaiveDate::from_ymd_opt(year, 8, 15).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Gandhi Jayanti",
NaiveDate::from_ymd_opt(year, 10, 2).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Dussehra",
NaiveDate::from_ymd_opt(year, 10, 15).expect("valid date components"),
0.05,
));
let diwali = Self::approximate_diwali(year);
for i in 0..5 {
cal.add_holiday(Holiday::new(
match i {
0 => "Dhanteras",
1 => "Naraka Chaturdashi",
2 => "Diwali",
3 => "Govardhan Puja",
_ => "Bhai Dooj",
},
diwali + Duration::days(i),
if i == 2 { 0.02 } else { 0.1 },
));
}
cal.add_holiday(Holiday::new(
"Christmas",
NaiveDate::from_ymd_opt(year, 12, 25).expect("valid date components"),
0.1,
));
cal
}
fn br_holidays(year: i32) -> Self {
let mut cal = Self::new(Region::BR, year);
cal.add_holiday(Holiday::new(
"Confraternização Universal",
NaiveDate::from_ymd_opt(year, 1, 1).expect("valid date components"),
0.02,
));
let easter = Self::easter_date(year);
let carnival_tuesday = easter - Duration::days(47);
let carnival_monday = carnival_tuesday - Duration::days(1);
cal.add_holiday(Holiday::new("Carnaval (Segunda)", carnival_monday, 0.02));
cal.add_holiday(Holiday::new("Carnaval (Terça)", carnival_tuesday, 0.02));
cal.add_holiday(Holiday::new(
"Sexta-feira Santa",
easter - Duration::days(2),
0.02,
));
cal.add_holiday(Holiday::new(
"Tiradentes",
NaiveDate::from_ymd_opt(year, 4, 21).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Dia do Trabalho",
NaiveDate::from_ymd_opt(year, 5, 1).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Corpus Christi",
easter + Duration::days(60),
0.05,
));
cal.add_holiday(Holiday::new(
"Independência do Brasil",
NaiveDate::from_ymd_opt(year, 9, 7).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Nossa Senhora Aparecida",
NaiveDate::from_ymd_opt(year, 10, 12).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Finados",
NaiveDate::from_ymd_opt(year, 11, 2).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Proclamação da República",
NaiveDate::from_ymd_opt(year, 11, 15).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Natal",
NaiveDate::from_ymd_opt(year, 12, 25).expect("valid date components"),
0.02,
));
cal
}
fn mx_holidays(year: i32) -> Self {
let mut cal = Self::new(Region::MX, year);
cal.add_holiday(Holiday::new(
"Año Nuevo",
NaiveDate::from_ymd_opt(year, 1, 1).expect("valid date components"),
0.02,
));
let constitution = Self::nth_weekday_of_month(year, 2, Weekday::Mon, 1);
cal.add_holiday(Holiday::new("DÃa de la Constitución", constitution, 0.02));
let juarez = Self::nth_weekday_of_month(year, 3, Weekday::Mon, 3);
cal.add_holiday(Holiday::new("Natalicio de Benito Juárez", juarez, 0.02));
let easter = Self::easter_date(year);
cal.add_holiday(Holiday::new(
"Jueves Santo",
easter - Duration::days(3),
0.05,
));
cal.add_holiday(Holiday::new(
"Viernes Santo",
easter - Duration::days(2),
0.02,
));
cal.add_holiday(Holiday::new(
"DÃa del Trabajo",
NaiveDate::from_ymd_opt(year, 5, 1).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"DÃa de la Independencia",
NaiveDate::from_ymd_opt(year, 9, 16).expect("valid date components"),
0.02,
));
let revolution = Self::nth_weekday_of_month(year, 11, Weekday::Mon, 3);
cal.add_holiday(Holiday::new("DÃa de la Revolución", revolution, 0.02));
cal.add_holiday(Holiday::new(
"DÃa de Muertos",
NaiveDate::from_ymd_opt(year, 11, 1).expect("valid date components"),
0.1,
));
cal.add_holiday(Holiday::new(
"DÃa de Muertos",
NaiveDate::from_ymd_opt(year, 11, 2).expect("valid date components"),
0.1,
));
cal.add_holiday(Holiday::new(
"Navidad",
NaiveDate::from_ymd_opt(year, 12, 25).expect("valid date components"),
0.02,
));
cal
}
fn au_holidays(year: i32) -> Self {
let mut cal = Self::new(Region::AU, year);
let new_years = NaiveDate::from_ymd_opt(year, 1, 1).expect("valid date components");
cal.add_holiday(Holiday::new(
"New Year's Day",
Self::observe_weekend(new_years),
0.02,
));
let australia_day = NaiveDate::from_ymd_opt(year, 1, 26).expect("valid date components");
cal.add_holiday(Holiday::new(
"Australia Day",
Self::observe_weekend(australia_day),
0.02,
));
let easter = Self::easter_date(year);
cal.add_holiday(Holiday::new(
"Good Friday",
easter - Duration::days(2),
0.02,
));
cal.add_holiday(Holiday::new(
"Easter Saturday",
easter - Duration::days(1),
0.02,
));
cal.add_holiday(Holiday::new(
"Easter Monday",
easter + Duration::days(1),
0.02,
));
let anzac = NaiveDate::from_ymd_opt(year, 4, 25).expect("valid date components");
cal.add_holiday(Holiday::new("ANZAC Day", anzac, 0.02));
let queens_birthday = Self::nth_weekday_of_month(year, 6, Weekday::Mon, 2);
cal.add_holiday(Holiday::new("Queen's Birthday", queens_birthday, 0.02));
let christmas = NaiveDate::from_ymd_opt(year, 12, 25).expect("valid date components");
cal.add_holiday(Holiday::new(
"Christmas Day",
Self::observe_weekend(christmas),
0.02,
));
let boxing = NaiveDate::from_ymd_opt(year, 12, 26).expect("valid date components");
cal.add_holiday(Holiday::new(
"Boxing Day",
Self::observe_weekend(boxing),
0.02,
));
cal
}
fn sg_holidays(year: i32) -> Self {
let mut cal = Self::new(Region::SG, year);
cal.add_holiday(Holiday::new(
"New Year's Day",
NaiveDate::from_ymd_opt(year, 1, 1).expect("valid date components"),
0.02,
));
let cny = Self::approximate_chinese_new_year(year);
cal.add_holiday(Holiday::new("Chinese New Year", cny, 0.02));
cal.add_holiday(Holiday::new(
"Chinese New Year (Day 2)",
cny + Duration::days(1),
0.02,
));
let easter = Self::easter_date(year);
cal.add_holiday(Holiday::new(
"Good Friday",
easter - Duration::days(2),
0.02,
));
cal.add_holiday(Holiday::new(
"Labour Day",
NaiveDate::from_ymd_opt(year, 5, 1).expect("valid date components"),
0.02,
));
let vesak = Self::approximate_vesak(year);
cal.add_holiday(Holiday::new("Vesak Day", vesak, 0.02));
let hari_raya_puasa = Self::approximate_hari_raya_puasa(year);
cal.add_holiday(Holiday::new("Hari Raya Puasa", hari_raya_puasa, 0.02));
let hari_raya_haji = Self::approximate_hari_raya_haji(year);
cal.add_holiday(Holiday::new("Hari Raya Haji", hari_raya_haji, 0.02));
cal.add_holiday(Holiday::new(
"National Day",
NaiveDate::from_ymd_opt(year, 8, 9).expect("valid date components"),
0.02,
));
let deepavali = Self::approximate_deepavali(year);
cal.add_holiday(Holiday::new("Deepavali", deepavali, 0.02));
cal.add_holiday(Holiday::new(
"Christmas Day",
NaiveDate::from_ymd_opt(year, 12, 25).expect("valid date components"),
0.02,
));
cal
}
fn kr_holidays(year: i32) -> Self {
let mut cal = Self::new(Region::KR, year);
cal.add_holiday(Holiday::new(
"Sinjeong",
NaiveDate::from_ymd_opt(year, 1, 1).expect("valid date components"),
0.02,
));
let seollal = Self::approximate_korean_new_year(year);
cal.add_holiday(Holiday::new(
"Seollal (Eve)",
seollal - Duration::days(1),
0.02,
));
cal.add_holiday(Holiday::new("Seollal", seollal, 0.02));
cal.add_holiday(Holiday::new(
"Seollal (Day 2)",
seollal + Duration::days(1),
0.02,
));
cal.add_holiday(Holiday::new(
"Samiljeol",
NaiveDate::from_ymd_opt(year, 3, 1).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Eorininal",
NaiveDate::from_ymd_opt(year, 5, 5).expect("valid date components"),
0.02,
));
let buddha_birthday = Self::approximate_korean_buddha_birthday(year);
cal.add_holiday(Holiday::new("Seokgatansinil", buddha_birthday, 0.02));
cal.add_holiday(Holiday::new(
"Hyeonchungil",
NaiveDate::from_ymd_opt(year, 6, 6).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Gwangbokjeol",
NaiveDate::from_ymd_opt(year, 8, 15).expect("valid date components"),
0.02,
));
let chuseok = Self::approximate_chuseok(year);
cal.add_holiday(Holiday::new(
"Chuseok (Eve)",
chuseok - Duration::days(1),
0.02,
));
cal.add_holiday(Holiday::new("Chuseok", chuseok, 0.02));
cal.add_holiday(Holiday::new(
"Chuseok (Day 2)",
chuseok + Duration::days(1),
0.02,
));
cal.add_holiday(Holiday::new(
"Gaecheonjeol",
NaiveDate::from_ymd_opt(year, 10, 3).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Hangullal",
NaiveDate::from_ymd_opt(year, 10, 9).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Seongtanjeol",
NaiveDate::from_ymd_opt(year, 12, 25).expect("valid date components"),
0.02,
));
cal
}
fn fr_holidays(year: i32) -> Self {
let mut cal = Self::new(Region::FR, year);
cal.add_holiday(Holiday::new(
"Jour de l'an",
NaiveDate::from_ymd_opt(year, 1, 1).expect("valid date components"),
0.02,
));
let easter = Self::easter_date(year);
cal.add_holiday(Holiday::new(
"Lundi de Pâques",
easter + Duration::days(1),
0.02,
));
cal.add_holiday(Holiday::new(
"Fête du Travail",
NaiveDate::from_ymd_opt(year, 5, 1).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Victoire 1945",
NaiveDate::from_ymd_opt(year, 5, 8).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new("Ascension", easter + Duration::days(39), 0.02));
cal.add_holiday(Holiday::new(
"Lundi de Pentecôte",
easter + Duration::days(50),
0.05,
));
cal.add_holiday(Holiday::new(
"Fête nationale",
NaiveDate::from_ymd_opt(year, 7, 14).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Assomption",
NaiveDate::from_ymd_opt(year, 8, 15).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Toussaint",
NaiveDate::from_ymd_opt(year, 11, 1).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Armistice",
NaiveDate::from_ymd_opt(year, 11, 11).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Noël",
NaiveDate::from_ymd_opt(year, 12, 25).expect("valid date components"),
0.02,
));
cal
}
fn it_holidays(year: i32) -> Self {
let mut cal = Self::new(Region::IT, year);
cal.add_holiday(Holiday::new(
"Capodanno",
NaiveDate::from_ymd_opt(year, 1, 1).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Epifania",
NaiveDate::from_ymd_opt(year, 1, 6).expect("valid date components"),
0.02,
));
let easter = Self::easter_date(year);
cal.add_holiday(Holiday::new(
"Lunedì dell'Angelo",
easter + Duration::days(1),
0.02,
));
cal.add_holiday(Holiday::new(
"Festa della Liberazione",
NaiveDate::from_ymd_opt(year, 4, 25).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Festa dei Lavoratori",
NaiveDate::from_ymd_opt(year, 5, 1).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Festa della Repubblica",
NaiveDate::from_ymd_opt(year, 6, 2).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Ferragosto",
NaiveDate::from_ymd_opt(year, 8, 15).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Tutti i Santi",
NaiveDate::from_ymd_opt(year, 11, 1).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Immacolata Concezione",
NaiveDate::from_ymd_opt(year, 12, 8).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Natale",
NaiveDate::from_ymd_opt(year, 12, 25).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Santo Stefano",
NaiveDate::from_ymd_opt(year, 12, 26).expect("valid date components"),
0.02,
));
cal
}
fn es_holidays(year: i32) -> Self {
let mut cal = Self::new(Region::ES, year);
cal.add_holiday(Holiday::new(
"Año Nuevo",
NaiveDate::from_ymd_opt(year, 1, 1).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"EpifanÃa del Señor",
NaiveDate::from_ymd_opt(year, 1, 6).expect("valid date components"),
0.02,
));
let easter = Self::easter_date(year);
cal.add_holiday(Holiday::new(
"Viernes Santo",
easter - Duration::days(2),
0.02,
));
cal.add_holiday(Holiday::new(
"Fiesta del Trabajo",
NaiveDate::from_ymd_opt(year, 5, 1).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Asunción de la Virgen",
NaiveDate::from_ymd_opt(year, 8, 15).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Fiesta Nacional de España",
NaiveDate::from_ymd_opt(year, 10, 12).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Todos los Santos",
NaiveDate::from_ymd_opt(year, 11, 1).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"DÃa de la Constitución",
NaiveDate::from_ymd_opt(year, 12, 6).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Inmaculada Concepción",
NaiveDate::from_ymd_opt(year, 12, 8).expect("valid date components"),
0.02,
));
cal.add_holiday(Holiday::new(
"Navidad",
NaiveDate::from_ymd_opt(year, 12, 25).expect("valid date components"),
0.02,
));
cal
}
fn ca_holidays(year: i32) -> Self {
let mut cal = Self::new(Region::CA, year);
let new_years = NaiveDate::from_ymd_opt(year, 1, 1).expect("valid date components");
cal.add_holiday(Holiday::new(
"New Year's Day",
Self::observe_weekend(new_years),
0.02,
));
let easter = Self::easter_date(year);
cal.add_holiday(Holiday::new(
"Good Friday",
easter - Duration::days(2),
0.02,
));
let may24 = NaiveDate::from_ymd_opt(year, 5, 24).expect("valid date components");
let victoria_day = {
let wd = may24.weekday();
let days_back = (wd.num_days_from_monday() as i64 + 7) % 7;
may24 - Duration::days(days_back)
};
cal.add_holiday(Holiday::new("Victoria Day", victoria_day, 0.02));
let canada_day = NaiveDate::from_ymd_opt(year, 7, 1).expect("valid date components");
cal.add_holiday(Holiday::new(
"Canada Day",
Self::observe_weekend(canada_day),
0.02,
));
let labour_day = Self::nth_weekday_of_month(year, 9, Weekday::Mon, 1);
cal.add_holiday(Holiday::new("Labour Day", labour_day, 0.02));
let truth_recon = NaiveDate::from_ymd_opt(year, 9, 30).expect("valid date components");
cal.add_holiday(Holiday::new(
"National Day for Truth and Reconciliation",
Self::observe_weekend(truth_recon),
0.02,
));
let thanksgiving = Self::nth_weekday_of_month(year, 10, Weekday::Mon, 2);
cal.add_holiday(Holiday::new("Thanksgiving", thanksgiving, 0.02));
let remembrance = NaiveDate::from_ymd_opt(year, 11, 11).expect("valid date components");
cal.add_holiday(Holiday::new(
"Remembrance Day",
Self::observe_weekend(remembrance),
0.02,
));
let christmas = NaiveDate::from_ymd_opt(year, 12, 25).expect("valid date components");
cal.add_holiday(Holiday::new(
"Christmas Day",
Self::observe_weekend(christmas),
0.02,
));
let boxing = NaiveDate::from_ymd_opt(year, 12, 26).expect("valid date components");
cal.add_holiday(Holiday::new(
"Boxing Day",
Self::observe_weekend(boxing),
0.02,
));
cal
}
fn easter_date(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).expect("valid date components")
}
fn nth_weekday_of_month(year: i32, month: u32, weekday: Weekday, n: u32) -> NaiveDate {
let first = NaiveDate::from_ymd_opt(year, month, 1).expect("valid date components");
let first_weekday = first.weekday();
let days_until = (weekday.num_days_from_monday() as i64
- first_weekday.num_days_from_monday() as i64
+ 7)
% 7;
first + Duration::days(days_until + (n - 1) as i64 * 7)
}
fn last_weekday_of_month(year: i32, month: u32, weekday: Weekday) -> NaiveDate {
let last = if month == 12 {
NaiveDate::from_ymd_opt(year + 1, 1, 1).expect("valid date components")
- Duration::days(1)
} else {
NaiveDate::from_ymd_opt(year, month + 1, 1).expect("valid date components")
- Duration::days(1)
};
let last_weekday = last.weekday();
let days_back = (last_weekday.num_days_from_monday() as i64
- weekday.num_days_from_monday() as i64
+ 7)
% 7;
last - Duration::days(days_back)
}
fn observe_weekend(date: NaiveDate) -> NaiveDate {
match date.weekday() {
Weekday::Sat => date - Duration::days(1), Weekday::Sun => date + Duration::days(1), _ => date,
}
}
fn approximate_chinese_new_year(year: i32) -> NaiveDate {
let base_year = 2000;
let cny_2000 = NaiveDate::from_ymd_opt(2000, 2, 5).expect("valid date components");
let years_diff = year - base_year;
let lunar_cycle = 29.5306; let days_offset = (years_diff as f64 * 12.0 * lunar_cycle) % 365.25;
let mut result = cny_2000 + Duration::days(days_offset as i64);
while result.month() > 2 || (result.month() == 2 && result.day() > 20) {
result -= Duration::days(29);
}
while result.month() < 1 || (result.month() == 1 && result.day() < 21) {
result += Duration::days(29);
}
if result.year() != year {
result = NaiveDate::from_ymd_opt(year, result.month(), result.day().min(28))
.unwrap_or_else(|| {
NaiveDate::from_ymd_opt(year, result.month(), 28)
.expect("valid date components")
});
}
result
}
fn approximate_diwali(year: i32) -> NaiveDate {
match year % 4 {
0 => NaiveDate::from_ymd_opt(year, 11, 1).expect("valid date components"),
1 => NaiveDate::from_ymd_opt(year, 10, 24).expect("valid date components"),
2 => NaiveDate::from_ymd_opt(year, 11, 12).expect("valid date components"),
_ => NaiveDate::from_ymd_opt(year, 11, 4).expect("valid date components"),
}
}
fn approximate_vesak(year: i32) -> NaiveDate {
let base = match year % 19 {
0 => 18,
1 => 7,
2 => 26,
3 => 15,
4 => 5,
5 => 24,
6 => 13,
7 => 2,
8 => 22,
9 => 11,
10 => 30,
11 => 19,
12 => 8,
13 => 27,
14 => 17,
15 => 6,
16 => 25,
17 => 14,
_ => 3,
};
let month = if base > 20 { 4 } else { 5 };
let day = if base > 20 { base - 10 } else { base };
NaiveDate::from_ymd_opt(year, month, day.clamp(1, 28) as u32)
.expect("valid date components")
}
fn approximate_hari_raya_puasa(year: i32) -> NaiveDate {
let base_year = 2024;
let base_date = NaiveDate::from_ymd_opt(2024, 4, 10).expect("valid date components");
let years_diff = year - base_year;
let days_shift = (years_diff as f64 * -10.63) as i64;
let mut result = base_date + Duration::days(days_shift);
while result.year() != year {
if result.year() > year {
result -= Duration::days(354); } else {
result += Duration::days(354);
}
}
result
}
fn approximate_hari_raya_haji(year: i32) -> NaiveDate {
Self::approximate_hari_raya_puasa(year) + Duration::days(70)
}
fn approximate_deepavali(year: i32) -> NaiveDate {
Self::approximate_diwali(year)
}
fn approximate_korean_new_year(year: i32) -> NaiveDate {
Self::approximate_chinese_new_year(year)
}
fn approximate_korean_buddha_birthday(year: i32) -> NaiveDate {
match year % 19 {
0 => NaiveDate::from_ymd_opt(year, 5, 15).expect("valid date components"),
1 => NaiveDate::from_ymd_opt(year, 5, 4).expect("valid date components"),
2 => NaiveDate::from_ymd_opt(year, 5, 23).expect("valid date components"),
3 => NaiveDate::from_ymd_opt(year, 5, 12).expect("valid date components"),
4 => NaiveDate::from_ymd_opt(year, 5, 1).expect("valid date components"),
5 => NaiveDate::from_ymd_opt(year, 5, 20).expect("valid date components"),
6 => NaiveDate::from_ymd_opt(year, 5, 10).expect("valid date components"),
7 => NaiveDate::from_ymd_opt(year, 4, 29).expect("valid date components"),
8 => NaiveDate::from_ymd_opt(year, 5, 18).expect("valid date components"),
9 => NaiveDate::from_ymd_opt(year, 5, 7).expect("valid date components"),
10 => NaiveDate::from_ymd_opt(year, 5, 26).expect("valid date components"),
11 => NaiveDate::from_ymd_opt(year, 5, 15).expect("valid date components"),
12 => NaiveDate::from_ymd_opt(year, 5, 4).expect("valid date components"),
13 => NaiveDate::from_ymd_opt(year, 5, 24).expect("valid date components"),
14 => NaiveDate::from_ymd_opt(year, 5, 13).expect("valid date components"),
15 => NaiveDate::from_ymd_opt(year, 5, 2).expect("valid date components"),
16 => NaiveDate::from_ymd_opt(year, 5, 21).expect("valid date components"),
17 => NaiveDate::from_ymd_opt(year, 5, 10).expect("valid date components"),
_ => NaiveDate::from_ymd_opt(year, 4, 30).expect("valid date components"),
}
}
fn approximate_chuseok(year: i32) -> NaiveDate {
match year % 19 {
0 => NaiveDate::from_ymd_opt(year, 9, 17).expect("valid date components"),
1 => NaiveDate::from_ymd_opt(year, 10, 6).expect("valid date components"),
2 => NaiveDate::from_ymd_opt(year, 9, 25).expect("valid date components"),
3 => NaiveDate::from_ymd_opt(year, 9, 14).expect("valid date components"),
4 => NaiveDate::from_ymd_opt(year, 10, 3).expect("valid date components"),
5 => NaiveDate::from_ymd_opt(year, 9, 22).expect("valid date components"),
6 => NaiveDate::from_ymd_opt(year, 9, 11).expect("valid date components"),
7 => NaiveDate::from_ymd_opt(year, 9, 30).expect("valid date components"),
8 => NaiveDate::from_ymd_opt(year, 9, 19).expect("valid date components"),
9 => NaiveDate::from_ymd_opt(year, 10, 9).expect("valid date components"),
10 => NaiveDate::from_ymd_opt(year, 9, 28).expect("valid date components"),
11 => NaiveDate::from_ymd_opt(year, 9, 17).expect("valid date components"),
12 => NaiveDate::from_ymd_opt(year, 10, 6).expect("valid date components"),
13 => NaiveDate::from_ymd_opt(year, 9, 25).expect("valid date components"),
14 => NaiveDate::from_ymd_opt(year, 9, 14).expect("valid date components"),
15 => NaiveDate::from_ymd_opt(year, 10, 4).expect("valid date components"),
16 => NaiveDate::from_ymd_opt(year, 9, 22).expect("valid date components"),
17 => NaiveDate::from_ymd_opt(year, 9, 12).expect("valid date components"),
_ => NaiveDate::from_ymd_opt(year, 10, 1).expect("valid date components"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CustomHolidayConfig {
pub name: String,
pub month: u8,
pub day: u8,
#[serde(default = "default_holiday_multiplier")]
pub activity_multiplier: f64,
}
fn default_holiday_multiplier() -> f64 {
0.05
}
impl CustomHolidayConfig {
pub fn to_holiday(&self, year: i32) -> Holiday {
Holiday::new(
&self.name,
NaiveDate::from_ymd_opt(year, self.month as u32, self.day as u32)
.expect("valid date components"),
self.activity_multiplier,
)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_us_holidays() {
let cal = HolidayCalendar::for_region(Region::US, 2024);
let christmas = NaiveDate::from_ymd_opt(2024, 12, 25).unwrap();
assert!(cal.is_holiday(christmas));
let independence = NaiveDate::from_ymd_opt(2024, 7, 4).unwrap();
assert!(cal.is_holiday(independence));
}
#[test]
fn test_german_holidays() {
let cal = HolidayCalendar::for_region(Region::DE, 2024);
let unity = NaiveDate::from_ymd_opt(2024, 10, 3).unwrap();
assert!(cal.is_holiday(unity));
}
#[test]
fn test_easter_calculation() {
assert_eq!(
HolidayCalendar::easter_date(2024),
NaiveDate::from_ymd_opt(2024, 3, 31).unwrap()
);
assert_eq!(
HolidayCalendar::easter_date(2025),
NaiveDate::from_ymd_opt(2025, 4, 20).unwrap()
);
}
#[test]
fn test_nth_weekday() {
let mlk = HolidayCalendar::nth_weekday_of_month(2024, 1, Weekday::Mon, 3);
assert_eq!(mlk, NaiveDate::from_ymd_opt(2024, 1, 15).unwrap());
let thanksgiving = HolidayCalendar::nth_weekday_of_month(2024, 11, Weekday::Thu, 4);
assert_eq!(thanksgiving, NaiveDate::from_ymd_opt(2024, 11, 28).unwrap());
}
#[test]
fn test_last_weekday() {
let memorial = HolidayCalendar::last_weekday_of_month(2024, 5, Weekday::Mon);
assert_eq!(memorial, NaiveDate::from_ymd_opt(2024, 5, 27).unwrap());
}
#[test]
fn test_activity_multiplier() {
let cal = HolidayCalendar::for_region(Region::US, 2024);
let christmas = NaiveDate::from_ymd_opt(2024, 12, 25).unwrap();
assert!(cal.get_multiplier(christmas) < 0.1);
let regular = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
assert!((cal.get_multiplier(regular) - 1.0).abs() < 0.01);
}
#[test]
fn test_all_regions_have_holidays() {
let regions = [
Region::US,
Region::DE,
Region::GB,
Region::CN,
Region::JP,
Region::IN,
Region::BR,
Region::MX,
Region::AU,
Region::SG,
Region::KR,
Region::FR,
Region::IT,
Region::ES,
Region::CA,
];
for region in regions {
let cal = HolidayCalendar::for_region(region, 2024);
assert!(
!cal.holidays.is_empty(),
"Region {:?} should have holidays",
region
);
}
}
#[test]
fn test_brazilian_holidays() {
let cal = HolidayCalendar::for_region(Region::BR, 2024);
let independence = NaiveDate::from_ymd_opt(2024, 9, 7).unwrap();
assert!(cal.is_holiday(independence));
let tiradentes = NaiveDate::from_ymd_opt(2024, 4, 21).unwrap();
assert!(cal.is_holiday(tiradentes));
}
#[test]
fn test_mexican_holidays() {
let cal = HolidayCalendar::for_region(Region::MX, 2024);
let independence = NaiveDate::from_ymd_opt(2024, 9, 16).unwrap();
assert!(cal.is_holiday(independence));
}
#[test]
fn test_australian_holidays() {
let cal = HolidayCalendar::for_region(Region::AU, 2024);
let anzac = NaiveDate::from_ymd_opt(2024, 4, 25).unwrap();
assert!(cal.is_holiday(anzac));
let australia_day = NaiveDate::from_ymd_opt(2024, 1, 26).unwrap();
assert!(cal.is_holiday(australia_day));
}
#[test]
fn test_singapore_holidays() {
let cal = HolidayCalendar::for_region(Region::SG, 2024);
let national = NaiveDate::from_ymd_opt(2024, 8, 9).unwrap();
assert!(cal.is_holiday(national));
}
#[test]
fn test_korean_holidays() {
let cal = HolidayCalendar::for_region(Region::KR, 2024);
let liberation = NaiveDate::from_ymd_opt(2024, 8, 15).unwrap();
assert!(cal.is_holiday(liberation));
let hangul = NaiveDate::from_ymd_opt(2024, 10, 9).unwrap();
assert!(cal.is_holiday(hangul));
}
#[test]
fn test_chinese_holidays() {
let cal = HolidayCalendar::for_region(Region::CN, 2024);
let national = NaiveDate::from_ymd_opt(2024, 10, 1).unwrap();
assert!(cal.is_holiday(national));
}
#[test]
fn test_japanese_golden_week() {
let cal = HolidayCalendar::for_region(Region::JP, 2024);
let kodomo = NaiveDate::from_ymd_opt(2024, 5, 5).unwrap();
assert!(cal.is_holiday(kodomo));
}
#[test]
fn test_french_holidays() {
let cal = HolidayCalendar::for_region(Region::FR, 2024);
let bastille = NaiveDate::from_ymd_opt(2024, 7, 14).unwrap();
assert!(cal.is_holiday(bastille));
let noel = NaiveDate::from_ymd_opt(2024, 12, 25).unwrap();
assert!(cal.is_holiday(noel));
let travail = NaiveDate::from_ymd_opt(2024, 5, 1).unwrap();
assert!(cal.is_holiday(travail));
let easter_monday = NaiveDate::from_ymd_opt(2024, 4, 1).unwrap();
assert!(cal.is_holiday(easter_monday));
assert_eq!(cal.holidays.len(), 11);
}
#[test]
fn test_french_holidays_2025() {
let cal = HolidayCalendar::for_region(Region::FR, 2025);
let easter_monday = NaiveDate::from_ymd_opt(2025, 4, 21).unwrap();
assert!(cal.is_holiday(easter_monday));
let ascension = NaiveDate::from_ymd_opt(2025, 5, 29).unwrap();
assert!(cal.is_holiday(ascension));
}
#[test]
fn test_italian_holidays() {
let cal = HolidayCalendar::for_region(Region::IT, 2024);
let ferragosto = NaiveDate::from_ymd_opt(2024, 8, 15).unwrap();
assert!(cal.is_holiday(ferragosto));
let repubblica = NaiveDate::from_ymd_opt(2024, 6, 2).unwrap();
assert!(cal.is_holiday(repubblica));
let stefano = NaiveDate::from_ymd_opt(2024, 12, 26).unwrap();
assert!(cal.is_holiday(stefano));
let epifania = NaiveDate::from_ymd_opt(2024, 1, 6).unwrap();
assert!(cal.is_holiday(epifania));
assert_eq!(cal.holidays.len(), 11);
}
#[test]
fn test_spanish_holidays() {
let cal = HolidayCalendar::for_region(Region::ES, 2024);
let national = NaiveDate::from_ymd_opt(2024, 10, 12).unwrap();
assert!(cal.is_holiday(national));
let constitution = NaiveDate::from_ymd_opt(2024, 12, 6).unwrap();
assert!(cal.is_holiday(constitution));
let good_friday = NaiveDate::from_ymd_opt(2024, 3, 29).unwrap();
assert!(cal.is_holiday(good_friday));
assert_eq!(cal.holidays.len(), 10);
}
#[test]
fn test_canadian_holidays() {
let cal = HolidayCalendar::for_region(Region::CA, 2024);
let canada_day = NaiveDate::from_ymd_opt(2024, 7, 1).unwrap();
assert!(cal.is_holiday(canada_day));
let thanksgiving = NaiveDate::from_ymd_opt(2024, 10, 14).unwrap();
assert!(cal.is_holiday(thanksgiving));
let victoria = NaiveDate::from_ymd_opt(2024, 5, 20).unwrap();
assert!(cal.is_holiday(victoria));
let labour = NaiveDate::from_ymd_opt(2024, 9, 2).unwrap();
assert!(cal.is_holiday(labour));
assert_eq!(cal.holidays.len(), 10);
}
#[test]
fn test_canadian_holidays_2025() {
let cal = HolidayCalendar::for_region(Region::CA, 2025);
let victoria = NaiveDate::from_ymd_opt(2025, 5, 19).unwrap();
assert!(cal.is_holiday(victoria));
let thanksgiving = NaiveDate::from_ymd_opt(2025, 10, 13).unwrap();
assert!(cal.is_holiday(thanksgiving));
}
fn sorted_dates(cal: &HolidayCalendar) -> Vec<NaiveDate> {
let mut dates = cal.all_dates();
dates.sort();
dates.dedup();
dates
}
#[test]
fn test_us_country_pack_parity_2024() {
let reg = crate::CountryPackRegistry::builtin_only().expect("builtin registry");
let us_pack = reg.get_by_str("US");
let legacy = HolidayCalendar::for_region(Region::US, 2024);
let pack_cal = HolidayCalendar::from_country_pack(us_pack, 2024);
let legacy_dates = sorted_dates(&legacy);
let pack_dates = sorted_dates(&pack_cal);
for date in &legacy_dates {
assert!(
pack_cal.is_holiday(*date),
"US pack calendar missing legacy holiday on {date}"
);
}
for date in &pack_dates {
assert!(
legacy.is_holiday(*date),
"Legacy US calendar missing pack holiday on {date}"
);
}
assert_eq!(
legacy_dates.len(),
pack_dates.len(),
"US holiday count mismatch: legacy={}, pack={}",
legacy_dates.len(),
pack_dates.len()
);
}
#[test]
fn test_us_country_pack_parity_2025() {
let reg = crate::CountryPackRegistry::builtin_only().expect("builtin registry");
let us_pack = reg.get_by_str("US");
let legacy = HolidayCalendar::for_region(Region::US, 2025);
let pack_cal = HolidayCalendar::from_country_pack(us_pack, 2025);
let legacy_dates = sorted_dates(&legacy);
let pack_dates = sorted_dates(&pack_cal);
for date in &legacy_dates {
assert!(
pack_cal.is_holiday(*date),
"US 2025 pack calendar missing legacy holiday on {date}"
);
}
for date in &pack_dates {
assert!(
legacy.is_holiday(*date),
"Legacy US 2025 calendar missing pack holiday on {date}"
);
}
assert_eq!(legacy_dates.len(), pack_dates.len());
}
#[test]
fn test_de_country_pack_parity_2024() {
let reg = crate::CountryPackRegistry::builtin_only().expect("builtin registry");
let de_pack = reg.get_by_str("DE");
let legacy = HolidayCalendar::for_region(Region::DE, 2024);
let pack_cal = HolidayCalendar::from_country_pack(de_pack, 2024);
let legacy_dates = sorted_dates(&legacy);
let pack_dates = sorted_dates(&pack_cal);
for date in &legacy_dates {
assert!(
pack_cal.is_holiday(*date),
"DE pack calendar missing legacy holiday on {date}"
);
}
for date in &pack_dates {
assert!(
legacy.is_holiday(*date),
"Legacy DE calendar missing pack holiday on {date}"
);
}
assert_eq!(
legacy_dates.len(),
pack_dates.len(),
"DE holiday count mismatch: legacy={}, pack={}",
legacy_dates.len(),
pack_dates.len()
);
}
#[test]
fn test_gb_country_pack_parity_2024() {
let reg = crate::CountryPackRegistry::builtin_only().expect("builtin registry");
let gb_pack = reg.get_by_str("GB");
let legacy = HolidayCalendar::for_region(Region::GB, 2024);
let pack_cal = HolidayCalendar::from_country_pack(gb_pack, 2024);
let legacy_dates = sorted_dates(&legacy);
let pack_dates = sorted_dates(&pack_cal);
for date in &legacy_dates {
assert!(
pack_cal.is_holiday(*date),
"GB pack calendar missing legacy holiday on {date}"
);
}
for date in &pack_dates {
assert!(
legacy.is_holiday(*date),
"Legacy GB calendar missing pack holiday on {date}"
);
}
assert_eq!(
legacy_dates.len(),
pack_dates.len(),
"GB holiday count mismatch: legacy={}, pack={}",
legacy_dates.len(),
pack_dates.len()
);
}
}