Skip to main content

datasynth_core/distributions/
holidays.rs

1//! Regional holiday calendars for transaction generation.
2//!
3//! Supports holidays for US, DE (Germany), GB (UK), CN (China),
4//! JP (Japan), and IN (India) with appropriate activity multipliers.
5
6use chrono::{Datelike, Duration, NaiveDate, Weekday};
7use serde::{Deserialize, Serialize};
8
9/// Supported regions for holiday calendars.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
11#[serde(rename_all = "UPPERCASE")]
12pub enum Region {
13    /// United States
14    US,
15    /// Germany
16    DE,
17    /// United Kingdom
18    GB,
19    /// China
20    CN,
21    /// Japan
22    JP,
23    /// India
24    IN,
25    /// Brazil
26    BR,
27    /// Mexico
28    MX,
29    /// Australia
30    AU,
31    /// Singapore
32    SG,
33    /// South Korea
34    KR,
35    /// France
36    FR,
37    /// Italy
38    IT,
39    /// Spain
40    ES,
41    /// Canada
42    CA,
43}
44
45impl std::fmt::Display for Region {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        match self {
48            Region::US => write!(f, "United States"),
49            Region::DE => write!(f, "Germany"),
50            Region::GB => write!(f, "United Kingdom"),
51            Region::CN => write!(f, "China"),
52            Region::JP => write!(f, "Japan"),
53            Region::IN => write!(f, "India"),
54            Region::BR => write!(f, "Brazil"),
55            Region::MX => write!(f, "Mexico"),
56            Region::AU => write!(f, "Australia"),
57            Region::SG => write!(f, "Singapore"),
58            Region::KR => write!(f, "South Korea"),
59            Region::FR => write!(f, "France"),
60            Region::IT => write!(f, "Italy"),
61            Region::ES => write!(f, "Spain"),
62            Region::CA => write!(f, "Canada"),
63        }
64    }
65}
66
67/// A holiday with its associated activity multiplier.
68#[derive(Debug, Clone)]
69pub struct Holiday {
70    /// Holiday name.
71    pub name: String,
72    /// Date of the holiday.
73    pub date: NaiveDate,
74    /// Activity multiplier (0.0 = completely closed, 1.0 = normal).
75    pub activity_multiplier: f64,
76    /// Whether this is a bank holiday (affects financial transactions).
77    pub is_bank_holiday: bool,
78}
79
80impl Holiday {
81    /// Create a new holiday.
82    pub fn new(name: impl Into<String>, date: NaiveDate, multiplier: f64) -> Self {
83        Self {
84            name: name.into(),
85            date,
86            activity_multiplier: multiplier,
87            is_bank_holiday: true,
88        }
89    }
90
91    /// Set whether this is a bank holiday.
92    pub fn with_bank_holiday(mut self, is_bank_holiday: bool) -> Self {
93        self.is_bank_holiday = is_bank_holiday;
94        self
95    }
96}
97
98/// A calendar of holidays for a specific region and year.
99#[derive(Debug, Clone)]
100pub struct HolidayCalendar {
101    /// Region for this calendar.
102    pub region: Region,
103    /// Year for this calendar.
104    pub year: i32,
105    /// List of holidays.
106    pub holidays: Vec<Holiday>,
107}
108
109impl HolidayCalendar {
110    /// Create a new empty holiday calendar.
111    pub fn new(region: Region, year: i32) -> Self {
112        Self {
113            region,
114            year,
115            holidays: Vec::new(),
116        }
117    }
118
119    /// Create a holiday calendar for a specific region and year.
120    pub fn for_region(region: Region, year: i32) -> Self {
121        match region {
122            Region::US => Self::us_holidays(year),
123            Region::DE => Self::de_holidays(year),
124            Region::GB => Self::gb_holidays(year),
125            Region::CN => Self::cn_holidays(year),
126            Region::JP => Self::jp_holidays(year),
127            Region::IN => Self::in_holidays(year),
128            Region::BR => Self::br_holidays(year),
129            Region::MX => Self::mx_holidays(year),
130            Region::AU => Self::au_holidays(year),
131            Region::SG => Self::sg_holidays(year),
132            Region::KR => Self::kr_holidays(year),
133            Region::FR => Self::fr_holidays(year),
134            Region::IT => Self::it_holidays(year),
135            Region::ES => Self::es_holidays(year),
136            Region::CA => Self::ca_holidays(year),
137        }
138    }
139
140    /// Check if a date is a holiday.
141    pub fn is_holiday(&self, date: NaiveDate) -> bool {
142        self.holidays.iter().any(|h| h.date == date)
143    }
144
145    /// Get the activity multiplier for a date.
146    pub fn get_multiplier(&self, date: NaiveDate) -> f64 {
147        self.holidays
148            .iter()
149            .find(|h| h.date == date)
150            .map(|h| h.activity_multiplier)
151            .unwrap_or(1.0)
152    }
153
154    /// Get all holidays for a date (may include multiple on same day).
155    pub fn get_holidays(&self, date: NaiveDate) -> Vec<&Holiday> {
156        self.holidays.iter().filter(|h| h.date == date).collect()
157    }
158
159    /// Add a holiday to the calendar.
160    pub fn add_holiday(&mut self, holiday: Holiday) {
161        self.holidays.push(holiday);
162    }
163
164    /// Get all dates in the calendar.
165    pub fn all_dates(&self) -> Vec<NaiveDate> {
166        self.holidays.iter().map(|h| h.date).collect()
167    }
168
169    /// Build a holiday calendar from a [`CountryPack`].
170    ///
171    /// Resolves fixed, easter-relative, nth-weekday, last-weekday, and
172    /// lunar holiday types defined in the pack's `holidays` section.
173    /// The `region` field is set to `Region::US` as a default; callers
174    /// that need a specific `Region` value should set it afterwards.
175    pub fn from_country_pack(pack: &crate::country::schema::CountryPack, year: i32) -> Self {
176        // Try to map the pack's country_code to a Region for backward compat.
177        let region = match pack.country_code.as_str() {
178            "US" => Region::US,
179            "DE" => Region::DE,
180            "GB" => Region::GB,
181            "CN" => Region::CN,
182            "JP" => Region::JP,
183            "IN" => Region::IN,
184            "BR" => Region::BR,
185            "MX" => Region::MX,
186            "AU" => Region::AU,
187            "SG" => Region::SG,
188            "KR" => Region::KR,
189            "FR" => Region::FR,
190            "IT" => Region::IT,
191            "ES" => Region::ES,
192            "CA" => Region::CA,
193            _ => Region::US,
194        };
195
196        let mut cal = Self::new(region, year);
197        let holidays = &pack.holidays;
198
199        // --- Fixed holidays ---
200        for h in &holidays.fixed {
201            if let Some(date) = NaiveDate::from_ymd_opt(year, h.month, h.day) {
202                let date = if h.observe_weekend_rule {
203                    Self::observe_weekend(date)
204                } else {
205                    date
206                };
207                cal.add_holiday(Holiday::new(&h.name, date, h.activity_multiplier));
208            }
209        }
210
211        // --- Easter-relative holidays ---
212        if let Some(easter) = crate::country::easter::compute_easter(year) {
213            for h in &holidays.easter_relative {
214                let date = easter + Duration::days(h.offset_days as i64);
215                cal.add_holiday(Holiday::new(&h.name, date, h.activity_multiplier));
216            }
217        }
218
219        // --- Nth-weekday holidays ---
220        for h in &holidays.nth_weekday {
221            if let Some(weekday) = Self::parse_weekday(&h.weekday) {
222                let date = Self::nth_weekday_of_month(year, h.month, weekday, h.occurrence);
223                let date = date + Duration::days(h.offset_days as i64);
224                cal.add_holiday(Holiday::new(&h.name, date, h.activity_multiplier));
225            }
226        }
227
228        // --- Last-weekday holidays ---
229        for h in &holidays.last_weekday {
230            if let Some(weekday) = Self::parse_weekday(&h.weekday) {
231                let date = Self::last_weekday_of_month(year, h.month, weekday);
232                cal.add_holiday(Holiday::new(&h.name, date, h.activity_multiplier));
233            }
234        }
235
236        // --- Lunar holidays ---
237        for h in &holidays.lunar {
238            if let Some(dates) =
239                crate::country::lunar::resolve_lunar_holiday(&h.algorithm, year, h.duration_days)
240            {
241                for date in dates {
242                    cal.add_holiday(Holiday::new(&h.name, date, h.activity_multiplier));
243                }
244            }
245        }
246
247        cal
248    }
249
250    /// Parse a weekday string (e.g. "monday") into a `chrono::Weekday`.
251    fn parse_weekday(s: &str) -> Option<Weekday> {
252        match s.to_lowercase().as_str() {
253            "monday" | "mon" => Some(Weekday::Mon),
254            "tuesday" | "tue" => Some(Weekday::Tue),
255            "wednesday" | "wed" => Some(Weekday::Wed),
256            "thursday" | "thu" => Some(Weekday::Thu),
257            "friday" | "fri" => Some(Weekday::Fri),
258            "saturday" | "sat" => Some(Weekday::Sat),
259            "sunday" | "sun" => Some(Weekday::Sun),
260            _ => None,
261        }
262    }
263
264    /// US Federal Holidays.
265    fn us_holidays(year: i32) -> Self {
266        let mut cal = Self::new(Region::US, year);
267
268        // New Year's Day - Jan 1 (observed)
269        let new_years = NaiveDate::from_ymd_opt(year, 1, 1).expect("valid date components");
270        cal.add_holiday(Holiday::new(
271            "New Year's Day",
272            Self::observe_weekend(new_years),
273            0.02,
274        ));
275
276        // Martin Luther King Jr. Day - 3rd Monday of January
277        let mlk = Self::nth_weekday_of_month(year, 1, Weekday::Mon, 3);
278        cal.add_holiday(Holiday::new("Martin Luther King Jr. Day", mlk, 0.1));
279
280        // Presidents' Day - 3rd Monday of February
281        let presidents = Self::nth_weekday_of_month(year, 2, Weekday::Mon, 3);
282        cal.add_holiday(Holiday::new("Presidents' Day", presidents, 0.1));
283
284        // Memorial Day - Last Monday of May
285        let memorial = Self::last_weekday_of_month(year, 5, Weekday::Mon);
286        cal.add_holiday(Holiday::new("Memorial Day", memorial, 0.05));
287
288        // Juneteenth - June 19
289        let juneteenth = NaiveDate::from_ymd_opt(year, 6, 19).expect("valid date components");
290        cal.add_holiday(Holiday::new(
291            "Juneteenth",
292            Self::observe_weekend(juneteenth),
293            0.1,
294        ));
295
296        // Independence Day - July 4
297        let independence = NaiveDate::from_ymd_opt(year, 7, 4).expect("valid date components");
298        cal.add_holiday(Holiday::new(
299            "Independence Day",
300            Self::observe_weekend(independence),
301            0.02,
302        ));
303
304        // Labor Day - 1st Monday of September
305        let labor = Self::nth_weekday_of_month(year, 9, Weekday::Mon, 1);
306        cal.add_holiday(Holiday::new("Labor Day", labor, 0.05));
307
308        // Columbus Day - 2nd Monday of October
309        let columbus = Self::nth_weekday_of_month(year, 10, Weekday::Mon, 2);
310        cal.add_holiday(Holiday::new("Columbus Day", columbus, 0.2));
311
312        // Veterans Day - November 11
313        let veterans = NaiveDate::from_ymd_opt(year, 11, 11).expect("valid date components");
314        cal.add_holiday(Holiday::new(
315            "Veterans Day",
316            Self::observe_weekend(veterans),
317            0.1,
318        ));
319
320        // Thanksgiving - 4th Thursday of November
321        let thanksgiving = Self::nth_weekday_of_month(year, 11, Weekday::Thu, 4);
322        cal.add_holiday(Holiday::new("Thanksgiving", thanksgiving, 0.02));
323
324        // Day after Thanksgiving
325        cal.add_holiday(Holiday::new(
326            "Day after Thanksgiving",
327            thanksgiving + Duration::days(1),
328            0.1,
329        ));
330
331        // Christmas Eve - December 24
332        let christmas_eve = NaiveDate::from_ymd_opt(year, 12, 24).expect("valid date components");
333        cal.add_holiday(Holiday::new("Christmas Eve", christmas_eve, 0.1));
334
335        // Christmas Day - December 25
336        let christmas = NaiveDate::from_ymd_opt(year, 12, 25).expect("valid date components");
337        cal.add_holiday(Holiday::new(
338            "Christmas Day",
339            Self::observe_weekend(christmas),
340            0.02,
341        ));
342
343        // New Year's Eve - December 31
344        let new_years_eve = NaiveDate::from_ymd_opt(year, 12, 31).expect("valid date components");
345        cal.add_holiday(Holiday::new("New Year's Eve", new_years_eve, 0.1));
346
347        cal
348    }
349
350    /// German holidays (nationwide).
351    fn de_holidays(year: i32) -> Self {
352        let mut cal = Self::new(Region::DE, year);
353
354        // Neujahr - January 1
355        cal.add_holiday(Holiday::new(
356            "Neujahr",
357            NaiveDate::from_ymd_opt(year, 1, 1).expect("valid date components"),
358            0.02,
359        ));
360
361        // Karfreitag - Good Friday (Easter - 2 days)
362        let easter = Self::easter_date(year);
363        cal.add_holiday(Holiday::new("Karfreitag", easter - Duration::days(2), 0.02));
364
365        // Ostermontag - Easter Monday
366        cal.add_holiday(Holiday::new(
367            "Ostermontag",
368            easter + Duration::days(1),
369            0.02,
370        ));
371
372        // Tag der Arbeit - May 1
373        cal.add_holiday(Holiday::new(
374            "Tag der Arbeit",
375            NaiveDate::from_ymd_opt(year, 5, 1).expect("valid date components"),
376            0.02,
377        ));
378
379        // Christi Himmelfahrt - Ascension Day (Easter + 39 days)
380        cal.add_holiday(Holiday::new(
381            "Christi Himmelfahrt",
382            easter + Duration::days(39),
383            0.02,
384        ));
385
386        // Pfingstmontag - Whit Monday (Easter + 50 days)
387        cal.add_holiday(Holiday::new(
388            "Pfingstmontag",
389            easter + Duration::days(50),
390            0.02,
391        ));
392
393        // Tag der Deutschen Einheit - October 3
394        cal.add_holiday(Holiday::new(
395            "Tag der Deutschen Einheit",
396            NaiveDate::from_ymd_opt(year, 10, 3).expect("valid date components"),
397            0.02,
398        ));
399
400        // Weihnachten - December 25-26
401        cal.add_holiday(Holiday::new(
402            "1. Weihnachtstag",
403            NaiveDate::from_ymd_opt(year, 12, 25).expect("valid date components"),
404            0.02,
405        ));
406        cal.add_holiday(Holiday::new(
407            "2. Weihnachtstag",
408            NaiveDate::from_ymd_opt(year, 12, 26).expect("valid date components"),
409            0.02,
410        ));
411
412        // Silvester - December 31
413        cal.add_holiday(Holiday::new(
414            "Silvester",
415            NaiveDate::from_ymd_opt(year, 12, 31).expect("valid date components"),
416            0.1,
417        ));
418
419        cal
420    }
421
422    /// UK bank holidays.
423    fn gb_holidays(year: i32) -> Self {
424        let mut cal = Self::new(Region::GB, year);
425
426        // New Year's Day
427        let new_years = NaiveDate::from_ymd_opt(year, 1, 1).expect("valid date components");
428        cal.add_holiday(Holiday::new(
429            "New Year's Day",
430            Self::observe_weekend(new_years),
431            0.02,
432        ));
433
434        // Good Friday
435        let easter = Self::easter_date(year);
436        cal.add_holiday(Holiday::new(
437            "Good Friday",
438            easter - Duration::days(2),
439            0.02,
440        ));
441
442        // Easter Monday
443        cal.add_holiday(Holiday::new(
444            "Easter Monday",
445            easter + Duration::days(1),
446            0.02,
447        ));
448
449        // Early May Bank Holiday - 1st Monday of May
450        let early_may = Self::nth_weekday_of_month(year, 5, Weekday::Mon, 1);
451        cal.add_holiday(Holiday::new("Early May Bank Holiday", early_may, 0.02));
452
453        // Spring Bank Holiday - Last Monday of May
454        let spring = Self::last_weekday_of_month(year, 5, Weekday::Mon);
455        cal.add_holiday(Holiday::new("Spring Bank Holiday", spring, 0.02));
456
457        // Summer Bank Holiday - Last Monday of August
458        let summer = Self::last_weekday_of_month(year, 8, Weekday::Mon);
459        cal.add_holiday(Holiday::new("Summer Bank Holiday", summer, 0.02));
460
461        // Christmas Day
462        let christmas = NaiveDate::from_ymd_opt(year, 12, 25).expect("valid date components");
463        cal.add_holiday(Holiday::new(
464            "Christmas Day",
465            Self::observe_weekend(christmas),
466            0.02,
467        ));
468
469        // Boxing Day
470        let boxing = NaiveDate::from_ymd_opt(year, 12, 26).expect("valid date components");
471        cal.add_holiday(Holiday::new(
472            "Boxing Day",
473            Self::observe_weekend(boxing),
474            0.02,
475        ));
476
477        cal
478    }
479
480    /// Chinese holidays (simplified - fixed dates only).
481    fn cn_holidays(year: i32) -> Self {
482        let mut cal = Self::new(Region::CN, year);
483
484        // New Year's Day - January 1
485        cal.add_holiday(Holiday::new(
486            "New Year",
487            NaiveDate::from_ymd_opt(year, 1, 1).expect("valid date components"),
488            0.05,
489        ));
490
491        // Spring Festival (Chinese New Year) - approximate late Jan/early Feb
492        // Using a simplified calculation - typically 7-day holiday
493        let cny = Self::approximate_chinese_new_year(year);
494        for i in 0..7 {
495            cal.add_holiday(Holiday::new(
496                if i == 0 {
497                    "Spring Festival"
498                } else {
499                    "Spring Festival Holiday"
500                },
501                cny + Duration::days(i),
502                0.02,
503            ));
504        }
505
506        // Qingming Festival - April 4-6 (approximate)
507        cal.add_holiday(Holiday::new(
508            "Qingming Festival",
509            NaiveDate::from_ymd_opt(year, 4, 5).expect("valid date components"),
510            0.05,
511        ));
512
513        // Labor Day - May 1 (3-day holiday)
514        for i in 0..3 {
515            cal.add_holiday(Holiday::new(
516                if i == 0 {
517                    "Labor Day"
518                } else {
519                    "Labor Day Holiday"
520                },
521                NaiveDate::from_ymd_opt(year, 5, 1).expect("valid date components")
522                    + Duration::days(i),
523                0.05,
524            ));
525        }
526
527        // Dragon Boat Festival - approximate early June
528        cal.add_holiday(Holiday::new(
529            "Dragon Boat Festival",
530            NaiveDate::from_ymd_opt(year, 6, 10).expect("valid date components"),
531            0.05,
532        ));
533
534        // Mid-Autumn Festival - approximate late September
535        cal.add_holiday(Holiday::new(
536            "Mid-Autumn Festival",
537            NaiveDate::from_ymd_opt(year, 9, 15).expect("valid date components"),
538            0.05,
539        ));
540
541        // National Day - October 1 (7-day holiday)
542        for i in 0..7 {
543            cal.add_holiday(Holiday::new(
544                if i == 0 {
545                    "National Day"
546                } else {
547                    "National Day Holiday"
548                },
549                NaiveDate::from_ymd_opt(year, 10, 1).expect("valid date components")
550                    + Duration::days(i),
551                0.02,
552            ));
553        }
554
555        cal
556    }
557
558    /// Japanese holidays.
559    fn jp_holidays(year: i32) -> Self {
560        let mut cal = Self::new(Region::JP, year);
561
562        // Ganjitsu - January 1
563        cal.add_holiday(Holiday::new(
564            "Ganjitsu (New Year)",
565            NaiveDate::from_ymd_opt(year, 1, 1).expect("valid date components"),
566            0.02,
567        ));
568
569        // New Year holidays - January 2-3
570        cal.add_holiday(Holiday::new(
571            "New Year Holiday",
572            NaiveDate::from_ymd_opt(year, 1, 2).expect("valid date components"),
573            0.05,
574        ));
575        cal.add_holiday(Holiday::new(
576            "New Year Holiday",
577            NaiveDate::from_ymd_opt(year, 1, 3).expect("valid date components"),
578            0.05,
579        ));
580
581        // Seijin no Hi - Coming of Age Day - 2nd Monday of January
582        let seijin = Self::nth_weekday_of_month(year, 1, Weekday::Mon, 2);
583        cal.add_holiday(Holiday::new("Seijin no Hi", seijin, 0.05));
584
585        // Kenkoku Kinen no Hi - National Foundation Day - February 11
586        cal.add_holiday(Holiday::new(
587            "Kenkoku Kinen no Hi",
588            NaiveDate::from_ymd_opt(year, 2, 11).expect("valid date components"),
589            0.02,
590        ));
591
592        // Tenno Tanjobi - Emperor's Birthday - February 23
593        cal.add_holiday(Holiday::new(
594            "Tenno Tanjobi",
595            NaiveDate::from_ymd_opt(year, 2, 23).expect("valid date components"),
596            0.02,
597        ));
598
599        // Shunbun no Hi - Vernal Equinox - around March 20-21
600        cal.add_holiday(Holiday::new(
601            "Shunbun no Hi",
602            NaiveDate::from_ymd_opt(year, 3, 20).expect("valid date components"),
603            0.02,
604        ));
605
606        // Showa no Hi - Showa Day - April 29
607        cal.add_holiday(Holiday::new(
608            "Showa no Hi",
609            NaiveDate::from_ymd_opt(year, 4, 29).expect("valid date components"),
610            0.02,
611        ));
612
613        // Golden Week - April 29 - May 5
614        cal.add_holiday(Holiday::new(
615            "Kenpo Kinenbi",
616            NaiveDate::from_ymd_opt(year, 5, 3).expect("valid date components"),
617            0.02,
618        ));
619        cal.add_holiday(Holiday::new(
620            "Midori no Hi",
621            NaiveDate::from_ymd_opt(year, 5, 4).expect("valid date components"),
622            0.02,
623        ));
624        cal.add_holiday(Holiday::new(
625            "Kodomo no Hi",
626            NaiveDate::from_ymd_opt(year, 5, 5).expect("valid date components"),
627            0.02,
628        ));
629
630        // Umi no Hi - Marine Day - 3rd Monday of July
631        let umi = Self::nth_weekday_of_month(year, 7, Weekday::Mon, 3);
632        cal.add_holiday(Holiday::new("Umi no Hi", umi, 0.05));
633
634        // Yama no Hi - Mountain Day - August 11
635        cal.add_holiday(Holiday::new(
636            "Yama no Hi",
637            NaiveDate::from_ymd_opt(year, 8, 11).expect("valid date components"),
638            0.05,
639        ));
640
641        // Keiro no Hi - Respect for the Aged Day - 3rd Monday of September
642        let keiro = Self::nth_weekday_of_month(year, 9, Weekday::Mon, 3);
643        cal.add_holiday(Holiday::new("Keiro no Hi", keiro, 0.05));
644
645        // Shubun no Hi - Autumnal Equinox - around September 22-23
646        cal.add_holiday(Holiday::new(
647            "Shubun no Hi",
648            NaiveDate::from_ymd_opt(year, 9, 23).expect("valid date components"),
649            0.02,
650        ));
651
652        // Sports Day - 2nd Monday of October
653        let sports = Self::nth_weekday_of_month(year, 10, Weekday::Mon, 2);
654        cal.add_holiday(Holiday::new("Sports Day", sports, 0.05));
655
656        // Bunka no Hi - Culture Day - November 3
657        cal.add_holiday(Holiday::new(
658            "Bunka no Hi",
659            NaiveDate::from_ymd_opt(year, 11, 3).expect("valid date components"),
660            0.02,
661        ));
662
663        // Kinro Kansha no Hi - Labor Thanksgiving Day - November 23
664        cal.add_holiday(Holiday::new(
665            "Kinro Kansha no Hi",
666            NaiveDate::from_ymd_opt(year, 11, 23).expect("valid date components"),
667            0.02,
668        ));
669
670        cal
671    }
672
673    /// Indian holidays (national holidays).
674    fn in_holidays(year: i32) -> Self {
675        let mut cal = Self::new(Region::IN, year);
676
677        // Republic Day - January 26
678        cal.add_holiday(Holiday::new(
679            "Republic Day",
680            NaiveDate::from_ymd_opt(year, 1, 26).expect("valid date components"),
681            0.02,
682        ));
683
684        // Holi - approximate March (lunar calendar)
685        cal.add_holiday(Holiday::new(
686            "Holi",
687            NaiveDate::from_ymd_opt(year, 3, 10).expect("valid date components"),
688            0.05,
689        ));
690
691        // Good Friday
692        let easter = Self::easter_date(year);
693        cal.add_holiday(Holiday::new(
694            "Good Friday",
695            easter - Duration::days(2),
696            0.05,
697        ));
698
699        // Independence Day - August 15
700        cal.add_holiday(Holiday::new(
701            "Independence Day",
702            NaiveDate::from_ymd_opt(year, 8, 15).expect("valid date components"),
703            0.02,
704        ));
705
706        // Gandhi Jayanti - October 2
707        cal.add_holiday(Holiday::new(
708            "Gandhi Jayanti",
709            NaiveDate::from_ymd_opt(year, 10, 2).expect("valid date components"),
710            0.02,
711        ));
712
713        // Dussehra - approximate October (lunar calendar)
714        cal.add_holiday(Holiday::new(
715            "Dussehra",
716            NaiveDate::from_ymd_opt(year, 10, 15).expect("valid date components"),
717            0.05,
718        ));
719
720        // Diwali - approximate October/November (5-day festival)
721        let diwali = Self::approximate_diwali(year);
722        for i in 0..5 {
723            cal.add_holiday(Holiday::new(
724                match i {
725                    0 => "Dhanteras",
726                    1 => "Naraka Chaturdashi",
727                    2 => "Diwali",
728                    3 => "Govardhan Puja",
729                    _ => "Bhai Dooj",
730                },
731                diwali + Duration::days(i),
732                if i == 2 { 0.02 } else { 0.1 },
733            ));
734        }
735
736        // Christmas - December 25
737        cal.add_holiday(Holiday::new(
738            "Christmas",
739            NaiveDate::from_ymd_opt(year, 12, 25).expect("valid date components"),
740            0.1,
741        ));
742
743        cal
744    }
745
746    /// Brazilian holidays (national holidays).
747    fn br_holidays(year: i32) -> Self {
748        let mut cal = Self::new(Region::BR, year);
749
750        // Confraternização Universal - January 1
751        cal.add_holiday(Holiday::new(
752            "Confraternização Universal",
753            NaiveDate::from_ymd_opt(year, 1, 1).expect("valid date components"),
754            0.02,
755        ));
756
757        // Carnaval - Tuesday before Ash Wednesday (47 days before Easter)
758        let easter = Self::easter_date(year);
759        let carnival_tuesday = easter - Duration::days(47);
760        let carnival_monday = carnival_tuesday - Duration::days(1);
761        cal.add_holiday(Holiday::new("Carnaval (Segunda)", carnival_monday, 0.02));
762        cal.add_holiday(Holiday::new("Carnaval (Terça)", carnival_tuesday, 0.02));
763
764        // Sexta-feira Santa - Good Friday
765        cal.add_holiday(Holiday::new(
766            "Sexta-feira Santa",
767            easter - Duration::days(2),
768            0.02,
769        ));
770
771        // Tiradentes - April 21
772        cal.add_holiday(Holiday::new(
773            "Tiradentes",
774            NaiveDate::from_ymd_opt(year, 4, 21).expect("valid date components"),
775            0.02,
776        ));
777
778        // Dia do Trabalho - May 1
779        cal.add_holiday(Holiday::new(
780            "Dia do Trabalho",
781            NaiveDate::from_ymd_opt(year, 5, 1).expect("valid date components"),
782            0.02,
783        ));
784
785        // Corpus Christi - 60 days after Easter
786        cal.add_holiday(Holiday::new(
787            "Corpus Christi",
788            easter + Duration::days(60),
789            0.05,
790        ));
791
792        // Independência do Brasil - September 7
793        cal.add_holiday(Holiday::new(
794            "Independência do Brasil",
795            NaiveDate::from_ymd_opt(year, 9, 7).expect("valid date components"),
796            0.02,
797        ));
798
799        // Nossa Senhora Aparecida - October 12
800        cal.add_holiday(Holiday::new(
801            "Nossa Senhora Aparecida",
802            NaiveDate::from_ymd_opt(year, 10, 12).expect("valid date components"),
803            0.02,
804        ));
805
806        // Finados - November 2
807        cal.add_holiday(Holiday::new(
808            "Finados",
809            NaiveDate::from_ymd_opt(year, 11, 2).expect("valid date components"),
810            0.02,
811        ));
812
813        // Proclamação da República - November 15
814        cal.add_holiday(Holiday::new(
815            "Proclamação da República",
816            NaiveDate::from_ymd_opt(year, 11, 15).expect("valid date components"),
817            0.02,
818        ));
819
820        // Natal - December 25
821        cal.add_holiday(Holiday::new(
822            "Natal",
823            NaiveDate::from_ymd_opt(year, 12, 25).expect("valid date components"),
824            0.02,
825        ));
826
827        cal
828    }
829
830    /// Mexican holidays (national holidays).
831    fn mx_holidays(year: i32) -> Self {
832        let mut cal = Self::new(Region::MX, year);
833
834        // Año Nuevo - January 1
835        cal.add_holiday(Holiday::new(
836            "Año Nuevo",
837            NaiveDate::from_ymd_opt(year, 1, 1).expect("valid date components"),
838            0.02,
839        ));
840
841        // Día de la Constitución - First Monday of February
842        let constitution = Self::nth_weekday_of_month(year, 2, Weekday::Mon, 1);
843        cal.add_holiday(Holiday::new("Día de la Constitución", constitution, 0.02));
844
845        // Natalicio de Benito Juárez - Third Monday of March
846        let juarez = Self::nth_weekday_of_month(year, 3, Weekday::Mon, 3);
847        cal.add_holiday(Holiday::new("Natalicio de Benito Juárez", juarez, 0.02));
848
849        // Semana Santa - Holy Thursday and Good Friday
850        let easter = Self::easter_date(year);
851        cal.add_holiday(Holiday::new(
852            "Jueves Santo",
853            easter - Duration::days(3),
854            0.05,
855        ));
856        cal.add_holiday(Holiday::new(
857            "Viernes Santo",
858            easter - Duration::days(2),
859            0.02,
860        ));
861
862        // Día del Trabajo - May 1
863        cal.add_holiday(Holiday::new(
864            "Día del Trabajo",
865            NaiveDate::from_ymd_opt(year, 5, 1).expect("valid date components"),
866            0.02,
867        ));
868
869        // Día de la Independencia - September 16
870        cal.add_holiday(Holiday::new(
871            "Día de la Independencia",
872            NaiveDate::from_ymd_opt(year, 9, 16).expect("valid date components"),
873            0.02,
874        ));
875
876        // Día de la Revolución - Third Monday of November
877        let revolution = Self::nth_weekday_of_month(year, 11, Weekday::Mon, 3);
878        cal.add_holiday(Holiday::new("Día de la Revolución", revolution, 0.02));
879
880        // Día de Muertos - November 1-2 (not official but widely observed)
881        cal.add_holiday(Holiday::new(
882            "Día de Muertos",
883            NaiveDate::from_ymd_opt(year, 11, 1).expect("valid date components"),
884            0.1,
885        ));
886        cal.add_holiday(Holiday::new(
887            "Día de Muertos",
888            NaiveDate::from_ymd_opt(year, 11, 2).expect("valid date components"),
889            0.1,
890        ));
891
892        // Navidad - December 25
893        cal.add_holiday(Holiday::new(
894            "Navidad",
895            NaiveDate::from_ymd_opt(year, 12, 25).expect("valid date components"),
896            0.02,
897        ));
898
899        cal
900    }
901
902    /// Australian holidays (national holidays).
903    fn au_holidays(year: i32) -> Self {
904        let mut cal = Self::new(Region::AU, year);
905
906        // New Year's Day - January 1
907        let new_years = NaiveDate::from_ymd_opt(year, 1, 1).expect("valid date components");
908        cal.add_holiday(Holiday::new(
909            "New Year's Day",
910            Self::observe_weekend(new_years),
911            0.02,
912        ));
913
914        // Australia Day - January 26 (observed)
915        let australia_day = NaiveDate::from_ymd_opt(year, 1, 26).expect("valid date components");
916        cal.add_holiday(Holiday::new(
917            "Australia Day",
918            Self::observe_weekend(australia_day),
919            0.02,
920        ));
921
922        // Good Friday
923        let easter = Self::easter_date(year);
924        cal.add_holiday(Holiday::new(
925            "Good Friday",
926            easter - Duration::days(2),
927            0.02,
928        ));
929
930        // Easter Saturday
931        cal.add_holiday(Holiday::new(
932            "Easter Saturday",
933            easter - Duration::days(1),
934            0.02,
935        ));
936
937        // Easter Monday
938        cal.add_holiday(Holiday::new(
939            "Easter Monday",
940            easter + Duration::days(1),
941            0.02,
942        ));
943
944        // ANZAC Day - April 25
945        let anzac = NaiveDate::from_ymd_opt(year, 4, 25).expect("valid date components");
946        cal.add_holiday(Holiday::new("ANZAC Day", anzac, 0.02));
947
948        // Queen's Birthday - Second Monday of June (varies by state, using NSW)
949        let queens_birthday = Self::nth_weekday_of_month(year, 6, Weekday::Mon, 2);
950        cal.add_holiday(Holiday::new("Queen's Birthday", queens_birthday, 0.02));
951
952        // Christmas Day
953        let christmas = NaiveDate::from_ymd_opt(year, 12, 25).expect("valid date components");
954        cal.add_holiday(Holiday::new(
955            "Christmas Day",
956            Self::observe_weekend(christmas),
957            0.02,
958        ));
959
960        // Boxing Day - December 26
961        let boxing = NaiveDate::from_ymd_opt(year, 12, 26).expect("valid date components");
962        cal.add_holiday(Holiday::new(
963            "Boxing Day",
964            Self::observe_weekend(boxing),
965            0.02,
966        ));
967
968        cal
969    }
970
971    /// Singaporean holidays (national holidays).
972    fn sg_holidays(year: i32) -> Self {
973        let mut cal = Self::new(Region::SG, year);
974
975        // New Year's Day - January 1
976        cal.add_holiday(Holiday::new(
977            "New Year's Day",
978            NaiveDate::from_ymd_opt(year, 1, 1).expect("valid date components"),
979            0.02,
980        ));
981
982        // Chinese New Year (2 days) - approximate
983        let cny = Self::approximate_chinese_new_year(year);
984        cal.add_holiday(Holiday::new("Chinese New Year", cny, 0.02));
985        cal.add_holiday(Holiday::new(
986            "Chinese New Year (Day 2)",
987            cny + Duration::days(1),
988            0.02,
989        ));
990
991        // Good Friday
992        let easter = Self::easter_date(year);
993        cal.add_holiday(Holiday::new(
994            "Good Friday",
995            easter - Duration::days(2),
996            0.02,
997        ));
998
999        // Labour Day - May 1
1000        cal.add_holiday(Holiday::new(
1001            "Labour Day",
1002            NaiveDate::from_ymd_opt(year, 5, 1).expect("valid date components"),
1003            0.02,
1004        ));
1005
1006        // Vesak Day - approximate (full moon in May)
1007        let vesak = Self::approximate_vesak(year);
1008        cal.add_holiday(Holiday::new("Vesak Day", vesak, 0.02));
1009
1010        // Hari Raya Puasa - approximate (end of Ramadan)
1011        let hari_raya_puasa = Self::approximate_hari_raya_puasa(year);
1012        cal.add_holiday(Holiday::new("Hari Raya Puasa", hari_raya_puasa, 0.02));
1013
1014        // Hari Raya Haji - approximate (Festival of Sacrifice)
1015        let hari_raya_haji = Self::approximate_hari_raya_haji(year);
1016        cal.add_holiday(Holiday::new("Hari Raya Haji", hari_raya_haji, 0.02));
1017
1018        // National Day - August 9
1019        cal.add_holiday(Holiday::new(
1020            "National Day",
1021            NaiveDate::from_ymd_opt(year, 8, 9).expect("valid date components"),
1022            0.02,
1023        ));
1024
1025        // Deepavali - approximate (October/November)
1026        let deepavali = Self::approximate_deepavali(year);
1027        cal.add_holiday(Holiday::new("Deepavali", deepavali, 0.02));
1028
1029        // Christmas Day
1030        cal.add_holiday(Holiday::new(
1031            "Christmas Day",
1032            NaiveDate::from_ymd_opt(year, 12, 25).expect("valid date components"),
1033            0.02,
1034        ));
1035
1036        cal
1037    }
1038
1039    /// South Korean holidays (national holidays).
1040    fn kr_holidays(year: i32) -> Self {
1041        let mut cal = Self::new(Region::KR, year);
1042
1043        // New Year's Day - January 1
1044        cal.add_holiday(Holiday::new(
1045            "Sinjeong",
1046            NaiveDate::from_ymd_opt(year, 1, 1).expect("valid date components"),
1047            0.02,
1048        ));
1049
1050        // Seollal (Korean New Year) - 3 days around lunar new year
1051        let seollal = Self::approximate_korean_new_year(year);
1052        cal.add_holiday(Holiday::new(
1053            "Seollal (Eve)",
1054            seollal - Duration::days(1),
1055            0.02,
1056        ));
1057        cal.add_holiday(Holiday::new("Seollal", seollal, 0.02));
1058        cal.add_holiday(Holiday::new(
1059            "Seollal (Day 2)",
1060            seollal + Duration::days(1),
1061            0.02,
1062        ));
1063
1064        // Independence Movement Day - March 1
1065        cal.add_holiday(Holiday::new(
1066            "Samiljeol",
1067            NaiveDate::from_ymd_opt(year, 3, 1).expect("valid date components"),
1068            0.02,
1069        ));
1070
1071        // Children's Day - May 5
1072        cal.add_holiday(Holiday::new(
1073            "Eorininal",
1074            NaiveDate::from_ymd_opt(year, 5, 5).expect("valid date components"),
1075            0.02,
1076        ));
1077
1078        // Buddha's Birthday - approximate (8th day of 4th lunar month)
1079        let buddha_birthday = Self::approximate_korean_buddha_birthday(year);
1080        cal.add_holiday(Holiday::new("Seokgatansinil", buddha_birthday, 0.02));
1081
1082        // Memorial Day - June 6
1083        cal.add_holiday(Holiday::new(
1084            "Hyeonchungil",
1085            NaiveDate::from_ymd_opt(year, 6, 6).expect("valid date components"),
1086            0.02,
1087        ));
1088
1089        // Liberation Day - August 15
1090        cal.add_holiday(Holiday::new(
1091            "Gwangbokjeol",
1092            NaiveDate::from_ymd_opt(year, 8, 15).expect("valid date components"),
1093            0.02,
1094        ));
1095
1096        // Chuseok (Korean Thanksgiving) - 3 days around harvest moon
1097        let chuseok = Self::approximate_chuseok(year);
1098        cal.add_holiday(Holiday::new(
1099            "Chuseok (Eve)",
1100            chuseok - Duration::days(1),
1101            0.02,
1102        ));
1103        cal.add_holiday(Holiday::new("Chuseok", chuseok, 0.02));
1104        cal.add_holiday(Holiday::new(
1105            "Chuseok (Day 2)",
1106            chuseok + Duration::days(1),
1107            0.02,
1108        ));
1109
1110        // National Foundation Day - October 3
1111        cal.add_holiday(Holiday::new(
1112            "Gaecheonjeol",
1113            NaiveDate::from_ymd_opt(year, 10, 3).expect("valid date components"),
1114            0.02,
1115        ));
1116
1117        // Hangul Day - October 9
1118        cal.add_holiday(Holiday::new(
1119            "Hangullal",
1120            NaiveDate::from_ymd_opt(year, 10, 9).expect("valid date components"),
1121            0.02,
1122        ));
1123
1124        // Christmas - December 25
1125        cal.add_holiday(Holiday::new(
1126            "Seongtanjeol",
1127            NaiveDate::from_ymd_opt(year, 12, 25).expect("valid date components"),
1128            0.02,
1129        ));
1130
1131        cal
1132    }
1133
1134    /// French national holidays.
1135    fn fr_holidays(year: i32) -> Self {
1136        let mut cal = Self::new(Region::FR, year);
1137
1138        // Jour de l'an - January 1
1139        cal.add_holiday(Holiday::new(
1140            "Jour de l'an",
1141            NaiveDate::from_ymd_opt(year, 1, 1).expect("valid date components"),
1142            0.02,
1143        ));
1144
1145        let easter = Self::easter_date(year);
1146
1147        // Lundi de Pâques - Easter Monday
1148        cal.add_holiday(Holiday::new(
1149            "Lundi de Pâques",
1150            easter + Duration::days(1),
1151            0.02,
1152        ));
1153
1154        // Fête du Travail - May 1
1155        cal.add_holiday(Holiday::new(
1156            "Fête du Travail",
1157            NaiveDate::from_ymd_opt(year, 5, 1).expect("valid date components"),
1158            0.02,
1159        ));
1160
1161        // Victoire 1945 - May 8
1162        cal.add_holiday(Holiday::new(
1163            "Victoire 1945",
1164            NaiveDate::from_ymd_opt(year, 5, 8).expect("valid date components"),
1165            0.02,
1166        ));
1167
1168        // Ascension - Easter + 39 days
1169        cal.add_holiday(Holiday::new("Ascension", easter + Duration::days(39), 0.02));
1170
1171        // Lundi de Pentecôte - Whit Monday (Easter + 50 days)
1172        cal.add_holiday(Holiday::new(
1173            "Lundi de Pentecôte",
1174            easter + Duration::days(50),
1175            0.05,
1176        ));
1177
1178        // Fête nationale - July 14
1179        cal.add_holiday(Holiday::new(
1180            "Fête nationale",
1181            NaiveDate::from_ymd_opt(year, 7, 14).expect("valid date components"),
1182            0.02,
1183        ));
1184
1185        // Assomption - August 15
1186        cal.add_holiday(Holiday::new(
1187            "Assomption",
1188            NaiveDate::from_ymd_opt(year, 8, 15).expect("valid date components"),
1189            0.02,
1190        ));
1191
1192        // Toussaint - November 1
1193        cal.add_holiday(Holiday::new(
1194            "Toussaint",
1195            NaiveDate::from_ymd_opt(year, 11, 1).expect("valid date components"),
1196            0.02,
1197        ));
1198
1199        // Armistice - November 11
1200        cal.add_holiday(Holiday::new(
1201            "Armistice",
1202            NaiveDate::from_ymd_opt(year, 11, 11).expect("valid date components"),
1203            0.02,
1204        ));
1205
1206        // Noël - December 25
1207        cal.add_holiday(Holiday::new(
1208            "Noël",
1209            NaiveDate::from_ymd_opt(year, 12, 25).expect("valid date components"),
1210            0.02,
1211        ));
1212
1213        cal
1214    }
1215
1216    /// Italian national holidays.
1217    fn it_holidays(year: i32) -> Self {
1218        let mut cal = Self::new(Region::IT, year);
1219
1220        // Capodanno - January 1
1221        cal.add_holiday(Holiday::new(
1222            "Capodanno",
1223            NaiveDate::from_ymd_opt(year, 1, 1).expect("valid date components"),
1224            0.02,
1225        ));
1226
1227        // Epifania - January 6
1228        cal.add_holiday(Holiday::new(
1229            "Epifania",
1230            NaiveDate::from_ymd_opt(year, 1, 6).expect("valid date components"),
1231            0.02,
1232        ));
1233
1234        let easter = Self::easter_date(year);
1235
1236        // Lunedì dell'Angelo - Easter Monday
1237        cal.add_holiday(Holiday::new(
1238            "Lunedì dell'Angelo",
1239            easter + Duration::days(1),
1240            0.02,
1241        ));
1242
1243        // Festa della Liberazione - April 25
1244        cal.add_holiday(Holiday::new(
1245            "Festa della Liberazione",
1246            NaiveDate::from_ymd_opt(year, 4, 25).expect("valid date components"),
1247            0.02,
1248        ));
1249
1250        // Festa dei Lavoratori - May 1
1251        cal.add_holiday(Holiday::new(
1252            "Festa dei Lavoratori",
1253            NaiveDate::from_ymd_opt(year, 5, 1).expect("valid date components"),
1254            0.02,
1255        ));
1256
1257        // Festa della Repubblica - June 2
1258        cal.add_holiday(Holiday::new(
1259            "Festa della Repubblica",
1260            NaiveDate::from_ymd_opt(year, 6, 2).expect("valid date components"),
1261            0.02,
1262        ));
1263
1264        // Ferragosto - August 15
1265        cal.add_holiday(Holiday::new(
1266            "Ferragosto",
1267            NaiveDate::from_ymd_opt(year, 8, 15).expect("valid date components"),
1268            0.02,
1269        ));
1270
1271        // Tutti i Santi - November 1
1272        cal.add_holiday(Holiday::new(
1273            "Tutti i Santi",
1274            NaiveDate::from_ymd_opt(year, 11, 1).expect("valid date components"),
1275            0.02,
1276        ));
1277
1278        // Immacolata Concezione - December 8
1279        cal.add_holiday(Holiday::new(
1280            "Immacolata Concezione",
1281            NaiveDate::from_ymd_opt(year, 12, 8).expect("valid date components"),
1282            0.02,
1283        ));
1284
1285        // Natale - December 25
1286        cal.add_holiday(Holiday::new(
1287            "Natale",
1288            NaiveDate::from_ymd_opt(year, 12, 25).expect("valid date components"),
1289            0.02,
1290        ));
1291
1292        // Santo Stefano - December 26
1293        cal.add_holiday(Holiday::new(
1294            "Santo Stefano",
1295            NaiveDate::from_ymd_opt(year, 12, 26).expect("valid date components"),
1296            0.02,
1297        ));
1298
1299        cal
1300    }
1301
1302    /// Spanish national holidays.
1303    fn es_holidays(year: i32) -> Self {
1304        let mut cal = Self::new(Region::ES, year);
1305
1306        // Año Nuevo - January 1
1307        cal.add_holiday(Holiday::new(
1308            "Año Nuevo",
1309            NaiveDate::from_ymd_opt(year, 1, 1).expect("valid date components"),
1310            0.02,
1311        ));
1312
1313        // Epifanía del Señor - January 6
1314        cal.add_holiday(Holiday::new(
1315            "Epifanía del Señor",
1316            NaiveDate::from_ymd_opt(year, 1, 6).expect("valid date components"),
1317            0.02,
1318        ));
1319
1320        let easter = Self::easter_date(year);
1321
1322        // Viernes Santo - Good Friday
1323        cal.add_holiday(Holiday::new(
1324            "Viernes Santo",
1325            easter - Duration::days(2),
1326            0.02,
1327        ));
1328
1329        // Fiesta del Trabajo - May 1
1330        cal.add_holiday(Holiday::new(
1331            "Fiesta del Trabajo",
1332            NaiveDate::from_ymd_opt(year, 5, 1).expect("valid date components"),
1333            0.02,
1334        ));
1335
1336        // Asunción de la Virgen - August 15
1337        cal.add_holiday(Holiday::new(
1338            "Asunción de la Virgen",
1339            NaiveDate::from_ymd_opt(year, 8, 15).expect("valid date components"),
1340            0.02,
1341        ));
1342
1343        // Fiesta Nacional de España - October 12
1344        cal.add_holiday(Holiday::new(
1345            "Fiesta Nacional de España",
1346            NaiveDate::from_ymd_opt(year, 10, 12).expect("valid date components"),
1347            0.02,
1348        ));
1349
1350        // Todos los Santos - November 1
1351        cal.add_holiday(Holiday::new(
1352            "Todos los Santos",
1353            NaiveDate::from_ymd_opt(year, 11, 1).expect("valid date components"),
1354            0.02,
1355        ));
1356
1357        // Día de la Constitución - December 6
1358        cal.add_holiday(Holiday::new(
1359            "Día de la Constitución",
1360            NaiveDate::from_ymd_opt(year, 12, 6).expect("valid date components"),
1361            0.02,
1362        ));
1363
1364        // Inmaculada Concepción - December 8
1365        cal.add_holiday(Holiday::new(
1366            "Inmaculada Concepción",
1367            NaiveDate::from_ymd_opt(year, 12, 8).expect("valid date components"),
1368            0.02,
1369        ));
1370
1371        // Navidad - December 25
1372        cal.add_holiday(Holiday::new(
1373            "Navidad",
1374            NaiveDate::from_ymd_opt(year, 12, 25).expect("valid date components"),
1375            0.02,
1376        ));
1377
1378        cal
1379    }
1380
1381    /// Canadian national holidays.
1382    fn ca_holidays(year: i32) -> Self {
1383        let mut cal = Self::new(Region::CA, year);
1384
1385        // New Year's Day - January 1 (observed)
1386        let new_years = NaiveDate::from_ymd_opt(year, 1, 1).expect("valid date components");
1387        cal.add_holiday(Holiday::new(
1388            "New Year's Day",
1389            Self::observe_weekend(new_years),
1390            0.02,
1391        ));
1392
1393        let easter = Self::easter_date(year);
1394
1395        // Good Friday
1396        cal.add_holiday(Holiday::new(
1397            "Good Friday",
1398            easter - Duration::days(2),
1399            0.02,
1400        ));
1401
1402        // Victoria Day - last Monday before May 25
1403        let may24 = NaiveDate::from_ymd_opt(year, 5, 24).expect("valid date components");
1404        let victoria_day = {
1405            let wd = may24.weekday();
1406            let days_back = (wd.num_days_from_monday() as i64 + 7) % 7;
1407            may24 - Duration::days(days_back)
1408        };
1409        cal.add_holiday(Holiday::new("Victoria Day", victoria_day, 0.02));
1410
1411        // Canada Day - July 1 (observed)
1412        let canada_day = NaiveDate::from_ymd_opt(year, 7, 1).expect("valid date components");
1413        cal.add_holiday(Holiday::new(
1414            "Canada Day",
1415            Self::observe_weekend(canada_day),
1416            0.02,
1417        ));
1418
1419        // Labour Day - 1st Monday of September
1420        let labour_day = Self::nth_weekday_of_month(year, 9, Weekday::Mon, 1);
1421        cal.add_holiday(Holiday::new("Labour Day", labour_day, 0.02));
1422
1423        // National Day for Truth and Reconciliation - September 30 (observed)
1424        let truth_recon = NaiveDate::from_ymd_opt(year, 9, 30).expect("valid date components");
1425        cal.add_holiday(Holiday::new(
1426            "National Day for Truth and Reconciliation",
1427            Self::observe_weekend(truth_recon),
1428            0.02,
1429        ));
1430
1431        // Thanksgiving - 2nd Monday of October
1432        let thanksgiving = Self::nth_weekday_of_month(year, 10, Weekday::Mon, 2);
1433        cal.add_holiday(Holiday::new("Thanksgiving", thanksgiving, 0.02));
1434
1435        // Remembrance Day - November 11 (observed)
1436        let remembrance = NaiveDate::from_ymd_opt(year, 11, 11).expect("valid date components");
1437        cal.add_holiday(Holiday::new(
1438            "Remembrance Day",
1439            Self::observe_weekend(remembrance),
1440            0.02,
1441        ));
1442
1443        // Christmas Day - December 25 (observed)
1444        let christmas = NaiveDate::from_ymd_opt(year, 12, 25).expect("valid date components");
1445        cal.add_holiday(Holiday::new(
1446            "Christmas Day",
1447            Self::observe_weekend(christmas),
1448            0.02,
1449        ));
1450
1451        // Boxing Day - December 26 (observed)
1452        let boxing = NaiveDate::from_ymd_opt(year, 12, 26).expect("valid date components");
1453        cal.add_holiday(Holiday::new(
1454            "Boxing Day",
1455            Self::observe_weekend(boxing),
1456            0.02,
1457        ));
1458
1459        cal
1460    }
1461
1462    /// Calculate Easter date using the anonymous Gregorian algorithm.
1463    fn easter_date(year: i32) -> NaiveDate {
1464        let a = year % 19;
1465        let b = year / 100;
1466        let c = year % 100;
1467        let d = b / 4;
1468        let e = b % 4;
1469        let f = (b + 8) / 25;
1470        let g = (b - f + 1) / 3;
1471        let h = (19 * a + b - d - g + 15) % 30;
1472        let i = c / 4;
1473        let k = c % 4;
1474        let l = (32 + 2 * e + 2 * i - h - k) % 7;
1475        let m = (a + 11 * h + 22 * l) / 451;
1476        let month = (h + l - 7 * m + 114) / 31;
1477        let day = ((h + l - 7 * m + 114) % 31) + 1;
1478
1479        NaiveDate::from_ymd_opt(year, month as u32, day as u32).expect("valid date components")
1480    }
1481
1482    /// Get nth weekday of a month (e.g., 3rd Monday of January).
1483    fn nth_weekday_of_month(year: i32, month: u32, weekday: Weekday, n: u32) -> NaiveDate {
1484        let first = NaiveDate::from_ymd_opt(year, month, 1).expect("valid date components");
1485        let first_weekday = first.weekday();
1486
1487        let days_until = (weekday.num_days_from_monday() as i64
1488            - first_weekday.num_days_from_monday() as i64
1489            + 7)
1490            % 7;
1491
1492        first + Duration::days(days_until + (n - 1) as i64 * 7)
1493    }
1494
1495    /// Get last weekday of a month (e.g., last Monday of May).
1496    fn last_weekday_of_month(year: i32, month: u32, weekday: Weekday) -> NaiveDate {
1497        let last = if month == 12 {
1498            NaiveDate::from_ymd_opt(year + 1, 1, 1).expect("valid date components")
1499                - Duration::days(1)
1500        } else {
1501            NaiveDate::from_ymd_opt(year, month + 1, 1).expect("valid date components")
1502                - Duration::days(1)
1503        };
1504
1505        let last_weekday = last.weekday();
1506        let days_back = (last_weekday.num_days_from_monday() as i64
1507            - weekday.num_days_from_monday() as i64
1508            + 7)
1509            % 7;
1510
1511        last - Duration::days(days_back)
1512    }
1513
1514    /// Observe weekend holidays on nearest weekday.
1515    fn observe_weekend(date: NaiveDate) -> NaiveDate {
1516        match date.weekday() {
1517            Weekday::Sat => date - Duration::days(1), // Friday
1518            Weekday::Sun => date + Duration::days(1), // Monday
1519            _ => date,
1520        }
1521    }
1522
1523    /// Approximate Chinese New Year date (simplified calculation).
1524    fn approximate_chinese_new_year(year: i32) -> NaiveDate {
1525        // Chinese New Year falls between Jan 21 and Feb 20
1526        // This is a simplified approximation
1527        let base_year = 2000;
1528        let cny_2000 = NaiveDate::from_ymd_opt(2000, 2, 5).expect("valid date components");
1529
1530        let years_diff = year - base_year;
1531        let lunar_cycle = 29.5306; // days per lunar month
1532        let days_offset = (years_diff as f64 * 12.0 * lunar_cycle) % 365.25;
1533
1534        let mut result = cny_2000 + Duration::days(days_offset as i64);
1535
1536        // Ensure it falls in Jan-Feb range
1537        while result.month() > 2 || (result.month() == 2 && result.day() > 20) {
1538            result -= Duration::days(29);
1539        }
1540        while result.month() < 1 || (result.month() == 1 && result.day() < 21) {
1541            result += Duration::days(29);
1542        }
1543
1544        // Adjust year if needed
1545        if result.year() != year {
1546            result = NaiveDate::from_ymd_opt(year, result.month(), result.day().min(28))
1547                .unwrap_or_else(|| {
1548                    NaiveDate::from_ymd_opt(year, result.month(), 28)
1549                        .expect("valid date components")
1550                });
1551        }
1552
1553        result
1554    }
1555
1556    /// Approximate Diwali date (simplified calculation).
1557    fn approximate_diwali(year: i32) -> NaiveDate {
1558        // Diwali typically falls in October-November
1559        // This is a simplified approximation
1560        match year % 4 {
1561            0 => NaiveDate::from_ymd_opt(year, 11, 1).expect("valid date components"),
1562            1 => NaiveDate::from_ymd_opt(year, 10, 24).expect("valid date components"),
1563            2 => NaiveDate::from_ymd_opt(year, 11, 12).expect("valid date components"),
1564            _ => NaiveDate::from_ymd_opt(year, 11, 4).expect("valid date components"),
1565        }
1566    }
1567
1568    /// Approximate Vesak Day (Buddha's Birthday in Theravada tradition).
1569    /// Falls on the full moon of the 4th lunar month (usually May).
1570    fn approximate_vesak(year: i32) -> NaiveDate {
1571        // Vesak is typically in May
1572        // Using approximate lunar cycle calculation
1573        let base = match year % 19 {
1574            0 => 18,
1575            1 => 7,
1576            2 => 26,
1577            3 => 15,
1578            4 => 5,
1579            5 => 24,
1580            6 => 13,
1581            7 => 2,
1582            8 => 22,
1583            9 => 11,
1584            10 => 30,
1585            11 => 19,
1586            12 => 8,
1587            13 => 27,
1588            14 => 17,
1589            15 => 6,
1590            16 => 25,
1591            17 => 14,
1592            _ => 3,
1593        };
1594        let month = if base > 20 { 4 } else { 5 };
1595        let day = if base > 20 { base - 10 } else { base };
1596        NaiveDate::from_ymd_opt(year, month, day.clamp(1, 28) as u32)
1597            .expect("valid date components")
1598    }
1599
1600    /// Approximate Hari Raya Puasa (Eid al-Fitr).
1601    /// Based on Islamic lunar calendar (moves ~11 days earlier each year).
1602    fn approximate_hari_raya_puasa(year: i32) -> NaiveDate {
1603        // Islamic calendar moves about 11 days earlier each year
1604        // Base: 2024 Eid al-Fitr was approximately April 10
1605        let base_year = 2024;
1606        let base_date = NaiveDate::from_ymd_opt(2024, 4, 10).expect("valid date components");
1607        let years_diff = year - base_year;
1608        let days_shift = (years_diff as f64 * -10.63) as i64;
1609        let mut result = base_date + Duration::days(days_shift);
1610
1611        // Wrap around to stay in valid range
1612        while result.year() != year {
1613            if result.year() > year {
1614                result -= Duration::days(354); // Islamic lunar year
1615            } else {
1616                result += Duration::days(354);
1617            }
1618        }
1619        result
1620    }
1621
1622    /// Approximate Hari Raya Haji (Eid al-Adha).
1623    /// Approximately 70 days after Hari Raya Puasa.
1624    fn approximate_hari_raya_haji(year: i32) -> NaiveDate {
1625        Self::approximate_hari_raya_puasa(year) + Duration::days(70)
1626    }
1627
1628    /// Approximate Deepavali date (same as Diwali).
1629    fn approximate_deepavali(year: i32) -> NaiveDate {
1630        Self::approximate_diwali(year)
1631    }
1632
1633    /// Approximate Korean New Year (Seollal).
1634    /// Similar to Chinese New Year but may differ by a day.
1635    fn approximate_korean_new_year(year: i32) -> NaiveDate {
1636        Self::approximate_chinese_new_year(year)
1637    }
1638
1639    /// Approximate Korean Buddha's Birthday.
1640    /// 8th day of the 4th lunar month.
1641    fn approximate_korean_buddha_birthday(year: i32) -> NaiveDate {
1642        // Typically falls in late April to late May
1643        match year % 19 {
1644            0 => NaiveDate::from_ymd_opt(year, 5, 15).expect("valid date components"),
1645            1 => NaiveDate::from_ymd_opt(year, 5, 4).expect("valid date components"),
1646            2 => NaiveDate::from_ymd_opt(year, 5, 23).expect("valid date components"),
1647            3 => NaiveDate::from_ymd_opt(year, 5, 12).expect("valid date components"),
1648            4 => NaiveDate::from_ymd_opt(year, 5, 1).expect("valid date components"),
1649            5 => NaiveDate::from_ymd_opt(year, 5, 20).expect("valid date components"),
1650            6 => NaiveDate::from_ymd_opt(year, 5, 10).expect("valid date components"),
1651            7 => NaiveDate::from_ymd_opt(year, 4, 29).expect("valid date components"),
1652            8 => NaiveDate::from_ymd_opt(year, 5, 18).expect("valid date components"),
1653            9 => NaiveDate::from_ymd_opt(year, 5, 7).expect("valid date components"),
1654            10 => NaiveDate::from_ymd_opt(year, 5, 26).expect("valid date components"),
1655            11 => NaiveDate::from_ymd_opt(year, 5, 15).expect("valid date components"),
1656            12 => NaiveDate::from_ymd_opt(year, 5, 4).expect("valid date components"),
1657            13 => NaiveDate::from_ymd_opt(year, 5, 24).expect("valid date components"),
1658            14 => NaiveDate::from_ymd_opt(year, 5, 13).expect("valid date components"),
1659            15 => NaiveDate::from_ymd_opt(year, 5, 2).expect("valid date components"),
1660            16 => NaiveDate::from_ymd_opt(year, 5, 21).expect("valid date components"),
1661            17 => NaiveDate::from_ymd_opt(year, 5, 10).expect("valid date components"),
1662            _ => NaiveDate::from_ymd_opt(year, 4, 30).expect("valid date components"),
1663        }
1664    }
1665
1666    /// Approximate Chuseok (Korean Thanksgiving).
1667    /// 15th day of the 8th lunar month (harvest moon).
1668    fn approximate_chuseok(year: i32) -> NaiveDate {
1669        // Chuseok typically falls in September or early October
1670        match year % 19 {
1671            0 => NaiveDate::from_ymd_opt(year, 9, 17).expect("valid date components"),
1672            1 => NaiveDate::from_ymd_opt(year, 10, 6).expect("valid date components"),
1673            2 => NaiveDate::from_ymd_opt(year, 9, 25).expect("valid date components"),
1674            3 => NaiveDate::from_ymd_opt(year, 9, 14).expect("valid date components"),
1675            4 => NaiveDate::from_ymd_opt(year, 10, 3).expect("valid date components"),
1676            5 => NaiveDate::from_ymd_opt(year, 9, 22).expect("valid date components"),
1677            6 => NaiveDate::from_ymd_opt(year, 9, 11).expect("valid date components"),
1678            7 => NaiveDate::from_ymd_opt(year, 9, 30).expect("valid date components"),
1679            8 => NaiveDate::from_ymd_opt(year, 9, 19).expect("valid date components"),
1680            9 => NaiveDate::from_ymd_opt(year, 10, 9).expect("valid date components"),
1681            10 => NaiveDate::from_ymd_opt(year, 9, 28).expect("valid date components"),
1682            11 => NaiveDate::from_ymd_opt(year, 9, 17).expect("valid date components"),
1683            12 => NaiveDate::from_ymd_opt(year, 10, 6).expect("valid date components"),
1684            13 => NaiveDate::from_ymd_opt(year, 9, 25).expect("valid date components"),
1685            14 => NaiveDate::from_ymd_opt(year, 9, 14).expect("valid date components"),
1686            15 => NaiveDate::from_ymd_opt(year, 10, 4).expect("valid date components"),
1687            16 => NaiveDate::from_ymd_opt(year, 9, 22).expect("valid date components"),
1688            17 => NaiveDate::from_ymd_opt(year, 9, 12).expect("valid date components"),
1689            _ => NaiveDate::from_ymd_opt(year, 10, 1).expect("valid date components"),
1690        }
1691    }
1692}
1693
1694/// Custom holiday configuration for YAML/JSON input.
1695#[derive(Debug, Clone, Serialize, Deserialize)]
1696pub struct CustomHolidayConfig {
1697    /// Holiday name.
1698    pub name: String,
1699    /// Month (1-12).
1700    pub month: u8,
1701    /// Day of month.
1702    pub day: u8,
1703    /// Activity multiplier (optional, defaults to 0.05).
1704    #[serde(default = "default_holiday_multiplier")]
1705    pub activity_multiplier: f64,
1706}
1707
1708fn default_holiday_multiplier() -> f64 {
1709    0.05
1710}
1711
1712impl CustomHolidayConfig {
1713    /// Convert to a Holiday for a specific year.
1714    pub fn to_holiday(&self, year: i32) -> Holiday {
1715        Holiday::new(
1716            &self.name,
1717            NaiveDate::from_ymd_opt(year, self.month as u32, self.day as u32)
1718                .expect("valid date components"),
1719            self.activity_multiplier,
1720        )
1721    }
1722}
1723
1724#[cfg(test)]
1725#[allow(clippy::unwrap_used)]
1726mod tests {
1727    use super::*;
1728
1729    #[test]
1730    fn test_us_holidays() {
1731        let cal = HolidayCalendar::for_region(Region::US, 2024);
1732
1733        // Check some specific holidays exist
1734        let christmas = NaiveDate::from_ymd_opt(2024, 12, 25).unwrap();
1735        assert!(cal.is_holiday(christmas));
1736
1737        // Independence Day (observed on Friday since July 4 is Thursday in 2024)
1738        let independence = NaiveDate::from_ymd_opt(2024, 7, 4).unwrap();
1739        assert!(cal.is_holiday(independence));
1740    }
1741
1742    #[test]
1743    fn test_german_holidays() {
1744        let cal = HolidayCalendar::for_region(Region::DE, 2024);
1745
1746        // Tag der Deutschen Einheit - October 3
1747        let unity = NaiveDate::from_ymd_opt(2024, 10, 3).unwrap();
1748        assert!(cal.is_holiday(unity));
1749    }
1750
1751    #[test]
1752    fn test_easter_calculation() {
1753        // Known Easter dates
1754        assert_eq!(
1755            HolidayCalendar::easter_date(2024),
1756            NaiveDate::from_ymd_opt(2024, 3, 31).unwrap()
1757        );
1758        assert_eq!(
1759            HolidayCalendar::easter_date(2025),
1760            NaiveDate::from_ymd_opt(2025, 4, 20).unwrap()
1761        );
1762    }
1763
1764    #[test]
1765    fn test_nth_weekday() {
1766        // 3rd Monday of January 2024
1767        let mlk = HolidayCalendar::nth_weekday_of_month(2024, 1, Weekday::Mon, 3);
1768        assert_eq!(mlk, NaiveDate::from_ymd_opt(2024, 1, 15).unwrap());
1769
1770        // 4th Thursday of November 2024 (Thanksgiving)
1771        let thanksgiving = HolidayCalendar::nth_weekday_of_month(2024, 11, Weekday::Thu, 4);
1772        assert_eq!(thanksgiving, NaiveDate::from_ymd_opt(2024, 11, 28).unwrap());
1773    }
1774
1775    #[test]
1776    fn test_last_weekday() {
1777        // Last Monday of May 2024 (Memorial Day)
1778        let memorial = HolidayCalendar::last_weekday_of_month(2024, 5, Weekday::Mon);
1779        assert_eq!(memorial, NaiveDate::from_ymd_opt(2024, 5, 27).unwrap());
1780    }
1781
1782    #[test]
1783    fn test_activity_multiplier() {
1784        let cal = HolidayCalendar::for_region(Region::US, 2024);
1785
1786        // Holiday should have low multiplier
1787        let christmas = NaiveDate::from_ymd_opt(2024, 12, 25).unwrap();
1788        assert!(cal.get_multiplier(christmas) < 0.1);
1789
1790        // Regular day should be 1.0
1791        let regular = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
1792        assert!((cal.get_multiplier(regular) - 1.0).abs() < 0.01);
1793    }
1794
1795    #[test]
1796    fn test_all_regions_have_holidays() {
1797        let regions = [
1798            Region::US,
1799            Region::DE,
1800            Region::GB,
1801            Region::CN,
1802            Region::JP,
1803            Region::IN,
1804            Region::BR,
1805            Region::MX,
1806            Region::AU,
1807            Region::SG,
1808            Region::KR,
1809            Region::FR,
1810            Region::IT,
1811            Region::ES,
1812            Region::CA,
1813        ];
1814
1815        for region in regions {
1816            let cal = HolidayCalendar::for_region(region, 2024);
1817            assert!(
1818                !cal.holidays.is_empty(),
1819                "Region {:?} should have holidays",
1820                region
1821            );
1822        }
1823    }
1824
1825    #[test]
1826    fn test_brazilian_holidays() {
1827        let cal = HolidayCalendar::for_region(Region::BR, 2024);
1828
1829        // Independência do Brasil - September 7
1830        let independence = NaiveDate::from_ymd_opt(2024, 9, 7).unwrap();
1831        assert!(cal.is_holiday(independence));
1832
1833        // Tiradentes - April 21
1834        let tiradentes = NaiveDate::from_ymd_opt(2024, 4, 21).unwrap();
1835        assert!(cal.is_holiday(tiradentes));
1836    }
1837
1838    #[test]
1839    fn test_mexican_holidays() {
1840        let cal = HolidayCalendar::for_region(Region::MX, 2024);
1841
1842        // Día de la Independencia - September 16
1843        let independence = NaiveDate::from_ymd_opt(2024, 9, 16).unwrap();
1844        assert!(cal.is_holiday(independence));
1845    }
1846
1847    #[test]
1848    fn test_australian_holidays() {
1849        let cal = HolidayCalendar::for_region(Region::AU, 2024);
1850
1851        // ANZAC Day - April 25
1852        let anzac = NaiveDate::from_ymd_opt(2024, 4, 25).unwrap();
1853        assert!(cal.is_holiday(anzac));
1854
1855        // Australia Day - January 26
1856        let australia_day = NaiveDate::from_ymd_opt(2024, 1, 26).unwrap();
1857        assert!(cal.is_holiday(australia_day));
1858    }
1859
1860    #[test]
1861    fn test_singapore_holidays() {
1862        let cal = HolidayCalendar::for_region(Region::SG, 2024);
1863
1864        // National Day - August 9
1865        let national = NaiveDate::from_ymd_opt(2024, 8, 9).unwrap();
1866        assert!(cal.is_holiday(national));
1867    }
1868
1869    #[test]
1870    fn test_korean_holidays() {
1871        let cal = HolidayCalendar::for_region(Region::KR, 2024);
1872
1873        // Liberation Day - August 15
1874        let liberation = NaiveDate::from_ymd_opt(2024, 8, 15).unwrap();
1875        assert!(cal.is_holiday(liberation));
1876
1877        // Hangul Day - October 9
1878        let hangul = NaiveDate::from_ymd_opt(2024, 10, 9).unwrap();
1879        assert!(cal.is_holiday(hangul));
1880    }
1881
1882    #[test]
1883    fn test_chinese_holidays() {
1884        let cal = HolidayCalendar::for_region(Region::CN, 2024);
1885
1886        // National Day - October 1
1887        let national = NaiveDate::from_ymd_opt(2024, 10, 1).unwrap();
1888        assert!(cal.is_holiday(national));
1889    }
1890
1891    #[test]
1892    fn test_japanese_golden_week() {
1893        let cal = HolidayCalendar::for_region(Region::JP, 2024);
1894
1895        // Check Golden Week holidays
1896        let kodomo = NaiveDate::from_ymd_opt(2024, 5, 5).unwrap();
1897        assert!(cal.is_holiday(kodomo));
1898    }
1899
1900    #[test]
1901    fn test_french_holidays() {
1902        let cal = HolidayCalendar::for_region(Region::FR, 2024);
1903
1904        // Fête nationale - July 14
1905        let bastille = NaiveDate::from_ymd_opt(2024, 7, 14).unwrap();
1906        assert!(cal.is_holiday(bastille));
1907
1908        // Noël - December 25
1909        let noel = NaiveDate::from_ymd_opt(2024, 12, 25).unwrap();
1910        assert!(cal.is_holiday(noel));
1911
1912        // Fête du Travail - May 1
1913        let travail = NaiveDate::from_ymd_opt(2024, 5, 1).unwrap();
1914        assert!(cal.is_holiday(travail));
1915
1916        // Easter Monday 2024: April 1 (Easter is March 31, 2024)
1917        let easter_monday = NaiveDate::from_ymd_opt(2024, 4, 1).unwrap();
1918        assert!(cal.is_holiday(easter_monday));
1919
1920        // 11 holidays total
1921        assert_eq!(cal.holidays.len(), 11);
1922    }
1923
1924    #[test]
1925    fn test_french_holidays_2025() {
1926        let cal = HolidayCalendar::for_region(Region::FR, 2025);
1927
1928        // Easter Monday 2025: April 21 (Easter is April 20, 2025)
1929        let easter_monday = NaiveDate::from_ymd_opt(2025, 4, 21).unwrap();
1930        assert!(cal.is_holiday(easter_monday));
1931
1932        // Ascension 2025: May 29 (Easter + 39)
1933        let ascension = NaiveDate::from_ymd_opt(2025, 5, 29).unwrap();
1934        assert!(cal.is_holiday(ascension));
1935    }
1936
1937    #[test]
1938    fn test_italian_holidays() {
1939        let cal = HolidayCalendar::for_region(Region::IT, 2024);
1940
1941        // Ferragosto - August 15
1942        let ferragosto = NaiveDate::from_ymd_opt(2024, 8, 15).unwrap();
1943        assert!(cal.is_holiday(ferragosto));
1944
1945        // Festa della Repubblica - June 2
1946        let repubblica = NaiveDate::from_ymd_opt(2024, 6, 2).unwrap();
1947        assert!(cal.is_holiday(repubblica));
1948
1949        // Santo Stefano - December 26
1950        let stefano = NaiveDate::from_ymd_opt(2024, 12, 26).unwrap();
1951        assert!(cal.is_holiday(stefano));
1952
1953        // Epifania - January 6
1954        let epifania = NaiveDate::from_ymd_opt(2024, 1, 6).unwrap();
1955        assert!(cal.is_holiday(epifania));
1956
1957        // 11 holidays total (including Easter Monday)
1958        assert_eq!(cal.holidays.len(), 11);
1959    }
1960
1961    #[test]
1962    fn test_spanish_holidays() {
1963        let cal = HolidayCalendar::for_region(Region::ES, 2024);
1964
1965        // Fiesta Nacional - October 12
1966        let national = NaiveDate::from_ymd_opt(2024, 10, 12).unwrap();
1967        assert!(cal.is_holiday(national));
1968
1969        // Día de la Constitución - December 6
1970        let constitution = NaiveDate::from_ymd_opt(2024, 12, 6).unwrap();
1971        assert!(cal.is_holiday(constitution));
1972
1973        // Viernes Santo 2024: March 29 (Easter March 31 - 2)
1974        let good_friday = NaiveDate::from_ymd_opt(2024, 3, 29).unwrap();
1975        assert!(cal.is_holiday(good_friday));
1976
1977        // 10 holidays total
1978        assert_eq!(cal.holidays.len(), 10);
1979    }
1980
1981    #[test]
1982    fn test_canadian_holidays() {
1983        let cal = HolidayCalendar::for_region(Region::CA, 2024);
1984
1985        // Canada Day - July 1
1986        let canada_day = NaiveDate::from_ymd_opt(2024, 7, 1).unwrap();
1987        assert!(cal.is_holiday(canada_day));
1988
1989        // Thanksgiving - 2nd Monday of October 2024 = October 14
1990        let thanksgiving = NaiveDate::from_ymd_opt(2024, 10, 14).unwrap();
1991        assert!(cal.is_holiday(thanksgiving));
1992
1993        // Victoria Day 2024 - Monday before May 25 = May 20
1994        let victoria = NaiveDate::from_ymd_opt(2024, 5, 20).unwrap();
1995        assert!(cal.is_holiday(victoria));
1996
1997        // Labour Day 2024 - 1st Monday of September = September 2
1998        let labour = NaiveDate::from_ymd_opt(2024, 9, 2).unwrap();
1999        assert!(cal.is_holiday(labour));
2000
2001        // 10 holidays total
2002        assert_eq!(cal.holidays.len(), 10);
2003    }
2004
2005    #[test]
2006    fn test_canadian_holidays_2025() {
2007        let cal = HolidayCalendar::for_region(Region::CA, 2025);
2008
2009        // Victoria Day 2025 - Monday before May 25 = May 19
2010        let victoria = NaiveDate::from_ymd_opt(2025, 5, 19).unwrap();
2011        assert!(cal.is_holiday(victoria));
2012
2013        // Thanksgiving 2025 - 2nd Monday of October = October 13
2014        let thanksgiving = NaiveDate::from_ymd_opt(2025, 10, 13).unwrap();
2015        assert!(cal.is_holiday(thanksgiving));
2016    }
2017
2018    // -----------------------------------------------------------------
2019    // Parity tests: for_region() vs from_country_pack()
2020    // -----------------------------------------------------------------
2021
2022    /// Extract sorted unique dates from a holiday calendar.
2023    fn sorted_dates(cal: &HolidayCalendar) -> Vec<NaiveDate> {
2024        let mut dates = cal.all_dates();
2025        dates.sort();
2026        dates.dedup();
2027        dates
2028    }
2029
2030    #[test]
2031    fn test_us_country_pack_parity_2024() {
2032        let reg = crate::CountryPackRegistry::builtin_only().expect("builtin registry");
2033        let us_pack = reg.get_by_str("US");
2034
2035        let legacy = HolidayCalendar::for_region(Region::US, 2024);
2036        let pack_cal = HolidayCalendar::from_country_pack(us_pack, 2024);
2037
2038        let legacy_dates = sorted_dates(&legacy);
2039        let pack_dates = sorted_dates(&pack_cal);
2040
2041        // Every legacy date must appear in the pack-derived calendar.
2042        for date in &legacy_dates {
2043            assert!(
2044                pack_cal.is_holiday(*date),
2045                "US pack calendar missing legacy holiday on {date}"
2046            );
2047        }
2048
2049        // Every pack date must appear in the legacy calendar.
2050        for date in &pack_dates {
2051            assert!(
2052                legacy.is_holiday(*date),
2053                "Legacy US calendar missing pack holiday on {date}"
2054            );
2055        }
2056
2057        assert_eq!(
2058            legacy_dates.len(),
2059            pack_dates.len(),
2060            "US holiday count mismatch: legacy={}, pack={}",
2061            legacy_dates.len(),
2062            pack_dates.len()
2063        );
2064    }
2065
2066    #[test]
2067    fn test_us_country_pack_parity_2025() {
2068        let reg = crate::CountryPackRegistry::builtin_only().expect("builtin registry");
2069        let us_pack = reg.get_by_str("US");
2070
2071        let legacy = HolidayCalendar::for_region(Region::US, 2025);
2072        let pack_cal = HolidayCalendar::from_country_pack(us_pack, 2025);
2073
2074        let legacy_dates = sorted_dates(&legacy);
2075        let pack_dates = sorted_dates(&pack_cal);
2076
2077        for date in &legacy_dates {
2078            assert!(
2079                pack_cal.is_holiday(*date),
2080                "US 2025 pack calendar missing legacy holiday on {date}"
2081            );
2082        }
2083        for date in &pack_dates {
2084            assert!(
2085                legacy.is_holiday(*date),
2086                "Legacy US 2025 calendar missing pack holiday on {date}"
2087            );
2088        }
2089        assert_eq!(legacy_dates.len(), pack_dates.len());
2090    }
2091
2092    #[test]
2093    fn test_de_country_pack_parity_2024() {
2094        let reg = crate::CountryPackRegistry::builtin_only().expect("builtin registry");
2095        let de_pack = reg.get_by_str("DE");
2096
2097        let legacy = HolidayCalendar::for_region(Region::DE, 2024);
2098        let pack_cal = HolidayCalendar::from_country_pack(de_pack, 2024);
2099
2100        let legacy_dates = sorted_dates(&legacy);
2101        let pack_dates = sorted_dates(&pack_cal);
2102
2103        for date in &legacy_dates {
2104            assert!(
2105                pack_cal.is_holiday(*date),
2106                "DE pack calendar missing legacy holiday on {date}"
2107            );
2108        }
2109        for date in &pack_dates {
2110            assert!(
2111                legacy.is_holiday(*date),
2112                "Legacy DE calendar missing pack holiday on {date}"
2113            );
2114        }
2115        assert_eq!(
2116            legacy_dates.len(),
2117            pack_dates.len(),
2118            "DE holiday count mismatch: legacy={}, pack={}",
2119            legacy_dates.len(),
2120            pack_dates.len()
2121        );
2122    }
2123
2124    #[test]
2125    fn test_gb_country_pack_parity_2024() {
2126        let reg = crate::CountryPackRegistry::builtin_only().expect("builtin registry");
2127        let gb_pack = reg.get_by_str("GB");
2128
2129        let legacy = HolidayCalendar::for_region(Region::GB, 2024);
2130        let pack_cal = HolidayCalendar::from_country_pack(gb_pack, 2024);
2131
2132        let legacy_dates = sorted_dates(&legacy);
2133        let pack_dates = sorted_dates(&pack_cal);
2134
2135        for date in &legacy_dates {
2136            assert!(
2137                pack_cal.is_holiday(*date),
2138                "GB pack calendar missing legacy holiday on {date}"
2139            );
2140        }
2141        for date in &pack_dates {
2142            assert!(
2143                legacy.is_holiday(*date),
2144                "Legacy GB calendar missing pack holiday on {date}"
2145            );
2146        }
2147        assert_eq!(
2148            legacy_dates.len(),
2149            pack_dates.len(),
2150            "GB holiday count mismatch: legacy={}, pack={}",
2151            legacy_dates.len(),
2152            pack_dates.len()
2153        );
2154    }
2155}