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.
1524    ///
1525    /// TODO: This is a simplified approximation that propagates the 2000 anchor
1526    /// date using an average lunar month length (29.5306 days). The true date
1527    /// requires a full lunisolar calendar computation (Metonic cycle, leap-month
1528    /// intercalation). The approximation can be off by a day or two and should
1529    /// not be used for applications requiring exact public-holiday compliance.
1530    /// Use a dedicated calendar library (e.g. `chinese-lunisolar-calendar`) for
1531    /// precision. For activity-pattern simulation the error is acceptable.
1532    fn approximate_chinese_new_year(year: i32) -> NaiveDate {
1533        // Chinese New Year falls between Jan 21 and Feb 20
1534        let base_year = 2000;
1535        let cny_2000 = NaiveDate::from_ymd_opt(2000, 2, 5).expect("valid date components");
1536
1537        let years_diff = year - base_year;
1538        let lunar_cycle = 29.5306; // days per lunar month
1539        let days_offset = (years_diff as f64 * 12.0 * lunar_cycle) % 365.25;
1540
1541        let mut result = cny_2000 + Duration::days(days_offset as i64);
1542
1543        // Ensure it falls in Jan-Feb range
1544        while result.month() > 2 || (result.month() == 2 && result.day() > 20) {
1545            result -= Duration::days(29);
1546        }
1547        while result.month() < 1 || (result.month() == 1 && result.day() < 21) {
1548            result += Duration::days(29);
1549        }
1550
1551        // Adjust year if needed
1552        if result.year() != year {
1553            result = NaiveDate::from_ymd_opt(year, result.month(), result.day().min(28))
1554                .unwrap_or_else(|| {
1555                    NaiveDate::from_ymd_opt(year, result.month(), 28)
1556                        .expect("valid date components")
1557                });
1558        }
1559
1560        result
1561    }
1562
1563    /// Approximate Diwali date (simplified calculation).
1564    fn approximate_diwali(year: i32) -> NaiveDate {
1565        // Diwali typically falls in October-November
1566        // This is a simplified approximation
1567        match year % 4 {
1568            0 => NaiveDate::from_ymd_opt(year, 11, 1).expect("valid date components"),
1569            1 => NaiveDate::from_ymd_opt(year, 10, 24).expect("valid date components"),
1570            2 => NaiveDate::from_ymd_opt(year, 11, 12).expect("valid date components"),
1571            _ => NaiveDate::from_ymd_opt(year, 11, 4).expect("valid date components"),
1572        }
1573    }
1574
1575    /// Approximate Vesak Day (Buddha's Birthday in Theravada tradition).
1576    /// Falls on the full moon of the 4th lunar month (usually May).
1577    fn approximate_vesak(year: i32) -> NaiveDate {
1578        // Vesak is typically in May
1579        // Using approximate lunar cycle calculation
1580        let base = match year % 19 {
1581            0 => 18,
1582            1 => 7,
1583            2 => 26,
1584            3 => 15,
1585            4 => 5,
1586            5 => 24,
1587            6 => 13,
1588            7 => 2,
1589            8 => 22,
1590            9 => 11,
1591            10 => 30,
1592            11 => 19,
1593            12 => 8,
1594            13 => 27,
1595            14 => 17,
1596            15 => 6,
1597            16 => 25,
1598            17 => 14,
1599            _ => 3,
1600        };
1601        let month = if base > 20 { 4 } else { 5 };
1602        let day = if base > 20 { base - 10 } else { base };
1603        NaiveDate::from_ymd_opt(year, month, day.clamp(1, 28) as u32)
1604            .expect("valid date components")
1605    }
1606
1607    /// Approximate Hari Raya Puasa (Eid al-Fitr).
1608    /// Based on Islamic lunar calendar (moves ~11 days earlier each year).
1609    fn approximate_hari_raya_puasa(year: i32) -> NaiveDate {
1610        // Islamic calendar moves about 11 days earlier each year
1611        // Base: 2024 Eid al-Fitr was approximately April 10
1612        let base_year = 2024;
1613        let base_date = NaiveDate::from_ymd_opt(2024, 4, 10).expect("valid date components");
1614        let years_diff = year - base_year;
1615        let days_shift = (years_diff as f64 * -10.63) as i64;
1616        let mut result = base_date + Duration::days(days_shift);
1617
1618        // Wrap around to stay in valid range
1619        while result.year() != year {
1620            if result.year() > year {
1621                result -= Duration::days(354); // Islamic lunar year
1622            } else {
1623                result += Duration::days(354);
1624            }
1625        }
1626        result
1627    }
1628
1629    /// Approximate Hari Raya Haji (Eid al-Adha).
1630    /// Approximately 70 days after Hari Raya Puasa.
1631    fn approximate_hari_raya_haji(year: i32) -> NaiveDate {
1632        Self::approximate_hari_raya_puasa(year) + Duration::days(70)
1633    }
1634
1635    /// Approximate Deepavali date (same as Diwali).
1636    fn approximate_deepavali(year: i32) -> NaiveDate {
1637        Self::approximate_diwali(year)
1638    }
1639
1640    /// Approximate Korean New Year (Seollal).
1641    /// Similar to Chinese New Year but may differ by a day.
1642    fn approximate_korean_new_year(year: i32) -> NaiveDate {
1643        Self::approximate_chinese_new_year(year)
1644    }
1645
1646    /// Approximate Korean Buddha's Birthday.
1647    /// 8th day of the 4th lunar month.
1648    fn approximate_korean_buddha_birthday(year: i32) -> NaiveDate {
1649        // Typically falls in late April to late May
1650        match year % 19 {
1651            0 => NaiveDate::from_ymd_opt(year, 5, 15).expect("valid date components"),
1652            1 => NaiveDate::from_ymd_opt(year, 5, 4).expect("valid date components"),
1653            2 => NaiveDate::from_ymd_opt(year, 5, 23).expect("valid date components"),
1654            3 => NaiveDate::from_ymd_opt(year, 5, 12).expect("valid date components"),
1655            4 => NaiveDate::from_ymd_opt(year, 5, 1).expect("valid date components"),
1656            5 => NaiveDate::from_ymd_opt(year, 5, 20).expect("valid date components"),
1657            6 => NaiveDate::from_ymd_opt(year, 5, 10).expect("valid date components"),
1658            7 => NaiveDate::from_ymd_opt(year, 4, 29).expect("valid date components"),
1659            8 => NaiveDate::from_ymd_opt(year, 5, 18).expect("valid date components"),
1660            9 => NaiveDate::from_ymd_opt(year, 5, 7).expect("valid date components"),
1661            10 => NaiveDate::from_ymd_opt(year, 5, 26).expect("valid date components"),
1662            11 => NaiveDate::from_ymd_opt(year, 5, 15).expect("valid date components"),
1663            12 => NaiveDate::from_ymd_opt(year, 5, 4).expect("valid date components"),
1664            13 => NaiveDate::from_ymd_opt(year, 5, 24).expect("valid date components"),
1665            14 => NaiveDate::from_ymd_opt(year, 5, 13).expect("valid date components"),
1666            15 => NaiveDate::from_ymd_opt(year, 5, 2).expect("valid date components"),
1667            16 => NaiveDate::from_ymd_opt(year, 5, 21).expect("valid date components"),
1668            17 => NaiveDate::from_ymd_opt(year, 5, 10).expect("valid date components"),
1669            _ => NaiveDate::from_ymd_opt(year, 4, 30).expect("valid date components"),
1670        }
1671    }
1672
1673    /// Approximate Chuseok (Korean Thanksgiving).
1674    /// 15th day of the 8th lunar month (harvest moon).
1675    fn approximate_chuseok(year: i32) -> NaiveDate {
1676        // Chuseok typically falls in September or early October
1677        match year % 19 {
1678            0 => NaiveDate::from_ymd_opt(year, 9, 17).expect("valid date components"),
1679            1 => NaiveDate::from_ymd_opt(year, 10, 6).expect("valid date components"),
1680            2 => NaiveDate::from_ymd_opt(year, 9, 25).expect("valid date components"),
1681            3 => NaiveDate::from_ymd_opt(year, 9, 14).expect("valid date components"),
1682            4 => NaiveDate::from_ymd_opt(year, 10, 3).expect("valid date components"),
1683            5 => NaiveDate::from_ymd_opt(year, 9, 22).expect("valid date components"),
1684            6 => NaiveDate::from_ymd_opt(year, 9, 11).expect("valid date components"),
1685            7 => NaiveDate::from_ymd_opt(year, 9, 30).expect("valid date components"),
1686            8 => NaiveDate::from_ymd_opt(year, 9, 19).expect("valid date components"),
1687            9 => NaiveDate::from_ymd_opt(year, 10, 9).expect("valid date components"),
1688            10 => NaiveDate::from_ymd_opt(year, 9, 28).expect("valid date components"),
1689            11 => NaiveDate::from_ymd_opt(year, 9, 17).expect("valid date components"),
1690            12 => NaiveDate::from_ymd_opt(year, 10, 6).expect("valid date components"),
1691            13 => NaiveDate::from_ymd_opt(year, 9, 25).expect("valid date components"),
1692            14 => NaiveDate::from_ymd_opt(year, 9, 14).expect("valid date components"),
1693            15 => NaiveDate::from_ymd_opt(year, 10, 4).expect("valid date components"),
1694            16 => NaiveDate::from_ymd_opt(year, 9, 22).expect("valid date components"),
1695            17 => NaiveDate::from_ymd_opt(year, 9, 12).expect("valid date components"),
1696            _ => NaiveDate::from_ymd_opt(year, 10, 1).expect("valid date components"),
1697        }
1698    }
1699}
1700
1701/// Custom holiday configuration for YAML/JSON input.
1702#[derive(Debug, Clone, Serialize, Deserialize)]
1703pub struct CustomHolidayConfig {
1704    /// Holiday name.
1705    pub name: String,
1706    /// Month (1-12).
1707    pub month: u8,
1708    /// Day of month.
1709    pub day: u8,
1710    /// Activity multiplier (optional, defaults to 0.05).
1711    #[serde(default = "default_holiday_multiplier")]
1712    pub activity_multiplier: f64,
1713}
1714
1715fn default_holiday_multiplier() -> f64 {
1716    0.05
1717}
1718
1719impl CustomHolidayConfig {
1720    /// Convert to a Holiday for a specific year.
1721    pub fn to_holiday(&self, year: i32) -> Holiday {
1722        Holiday::new(
1723            &self.name,
1724            NaiveDate::from_ymd_opt(year, self.month as u32, self.day as u32)
1725                .expect("valid date components"),
1726            self.activity_multiplier,
1727        )
1728    }
1729}
1730
1731#[cfg(test)]
1732#[allow(clippy::unwrap_used)]
1733mod tests {
1734    use super::*;
1735
1736    #[test]
1737    fn test_us_holidays() {
1738        let cal = HolidayCalendar::for_region(Region::US, 2024);
1739
1740        // Check some specific holidays exist
1741        let christmas = NaiveDate::from_ymd_opt(2024, 12, 25).unwrap();
1742        assert!(cal.is_holiday(christmas));
1743
1744        // Independence Day (observed on Friday since July 4 is Thursday in 2024)
1745        let independence = NaiveDate::from_ymd_opt(2024, 7, 4).unwrap();
1746        assert!(cal.is_holiday(independence));
1747    }
1748
1749    #[test]
1750    fn test_german_holidays() {
1751        let cal = HolidayCalendar::for_region(Region::DE, 2024);
1752
1753        // Tag der Deutschen Einheit - October 3
1754        let unity = NaiveDate::from_ymd_opt(2024, 10, 3).unwrap();
1755        assert!(cal.is_holiday(unity));
1756    }
1757
1758    #[test]
1759    fn test_easter_calculation() {
1760        // Known Easter dates
1761        assert_eq!(
1762            HolidayCalendar::easter_date(2024),
1763            NaiveDate::from_ymd_opt(2024, 3, 31).unwrap()
1764        );
1765        assert_eq!(
1766            HolidayCalendar::easter_date(2025),
1767            NaiveDate::from_ymd_opt(2025, 4, 20).unwrap()
1768        );
1769    }
1770
1771    #[test]
1772    fn test_nth_weekday() {
1773        // 3rd Monday of January 2024
1774        let mlk = HolidayCalendar::nth_weekday_of_month(2024, 1, Weekday::Mon, 3);
1775        assert_eq!(mlk, NaiveDate::from_ymd_opt(2024, 1, 15).unwrap());
1776
1777        // 4th Thursday of November 2024 (Thanksgiving)
1778        let thanksgiving = HolidayCalendar::nth_weekday_of_month(2024, 11, Weekday::Thu, 4);
1779        assert_eq!(thanksgiving, NaiveDate::from_ymd_opt(2024, 11, 28).unwrap());
1780    }
1781
1782    #[test]
1783    fn test_last_weekday() {
1784        // Last Monday of May 2024 (Memorial Day)
1785        let memorial = HolidayCalendar::last_weekday_of_month(2024, 5, Weekday::Mon);
1786        assert_eq!(memorial, NaiveDate::from_ymd_opt(2024, 5, 27).unwrap());
1787    }
1788
1789    #[test]
1790    fn test_activity_multiplier() {
1791        let cal = HolidayCalendar::for_region(Region::US, 2024);
1792
1793        // Holiday should have low multiplier
1794        let christmas = NaiveDate::from_ymd_opt(2024, 12, 25).unwrap();
1795        assert!(cal.get_multiplier(christmas) < 0.1);
1796
1797        // Regular day should be 1.0
1798        let regular = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
1799        assert!((cal.get_multiplier(regular) - 1.0).abs() < 0.01);
1800    }
1801
1802    #[test]
1803    fn test_all_regions_have_holidays() {
1804        let regions = [
1805            Region::US,
1806            Region::DE,
1807            Region::GB,
1808            Region::CN,
1809            Region::JP,
1810            Region::IN,
1811            Region::BR,
1812            Region::MX,
1813            Region::AU,
1814            Region::SG,
1815            Region::KR,
1816            Region::FR,
1817            Region::IT,
1818            Region::ES,
1819            Region::CA,
1820        ];
1821
1822        for region in regions {
1823            let cal = HolidayCalendar::for_region(region, 2024);
1824            assert!(
1825                !cal.holidays.is_empty(),
1826                "Region {:?} should have holidays",
1827                region
1828            );
1829        }
1830    }
1831
1832    #[test]
1833    fn test_brazilian_holidays() {
1834        let cal = HolidayCalendar::for_region(Region::BR, 2024);
1835
1836        // Independência do Brasil - September 7
1837        let independence = NaiveDate::from_ymd_opt(2024, 9, 7).unwrap();
1838        assert!(cal.is_holiday(independence));
1839
1840        // Tiradentes - April 21
1841        let tiradentes = NaiveDate::from_ymd_opt(2024, 4, 21).unwrap();
1842        assert!(cal.is_holiday(tiradentes));
1843    }
1844
1845    #[test]
1846    fn test_mexican_holidays() {
1847        let cal = HolidayCalendar::for_region(Region::MX, 2024);
1848
1849        // Día de la Independencia - September 16
1850        let independence = NaiveDate::from_ymd_opt(2024, 9, 16).unwrap();
1851        assert!(cal.is_holiday(independence));
1852    }
1853
1854    #[test]
1855    fn test_australian_holidays() {
1856        let cal = HolidayCalendar::for_region(Region::AU, 2024);
1857
1858        // ANZAC Day - April 25
1859        let anzac = NaiveDate::from_ymd_opt(2024, 4, 25).unwrap();
1860        assert!(cal.is_holiday(anzac));
1861
1862        // Australia Day - January 26
1863        let australia_day = NaiveDate::from_ymd_opt(2024, 1, 26).unwrap();
1864        assert!(cal.is_holiday(australia_day));
1865    }
1866
1867    #[test]
1868    fn test_singapore_holidays() {
1869        let cal = HolidayCalendar::for_region(Region::SG, 2024);
1870
1871        // National Day - August 9
1872        let national = NaiveDate::from_ymd_opt(2024, 8, 9).unwrap();
1873        assert!(cal.is_holiday(national));
1874    }
1875
1876    #[test]
1877    fn test_korean_holidays() {
1878        let cal = HolidayCalendar::for_region(Region::KR, 2024);
1879
1880        // Liberation Day - August 15
1881        let liberation = NaiveDate::from_ymd_opt(2024, 8, 15).unwrap();
1882        assert!(cal.is_holiday(liberation));
1883
1884        // Hangul Day - October 9
1885        let hangul = NaiveDate::from_ymd_opt(2024, 10, 9).unwrap();
1886        assert!(cal.is_holiday(hangul));
1887    }
1888
1889    #[test]
1890    fn test_chinese_holidays() {
1891        let cal = HolidayCalendar::for_region(Region::CN, 2024);
1892
1893        // National Day - October 1
1894        let national = NaiveDate::from_ymd_opt(2024, 10, 1).unwrap();
1895        assert!(cal.is_holiday(national));
1896    }
1897
1898    #[test]
1899    fn test_japanese_golden_week() {
1900        let cal = HolidayCalendar::for_region(Region::JP, 2024);
1901
1902        // Check Golden Week holidays
1903        let kodomo = NaiveDate::from_ymd_opt(2024, 5, 5).unwrap();
1904        assert!(cal.is_holiday(kodomo));
1905    }
1906
1907    #[test]
1908    fn test_french_holidays() {
1909        let cal = HolidayCalendar::for_region(Region::FR, 2024);
1910
1911        // Fête nationale - July 14
1912        let bastille = NaiveDate::from_ymd_opt(2024, 7, 14).unwrap();
1913        assert!(cal.is_holiday(bastille));
1914
1915        // Noël - December 25
1916        let noel = NaiveDate::from_ymd_opt(2024, 12, 25).unwrap();
1917        assert!(cal.is_holiday(noel));
1918
1919        // Fête du Travail - May 1
1920        let travail = NaiveDate::from_ymd_opt(2024, 5, 1).unwrap();
1921        assert!(cal.is_holiday(travail));
1922
1923        // Easter Monday 2024: April 1 (Easter is March 31, 2024)
1924        let easter_monday = NaiveDate::from_ymd_opt(2024, 4, 1).unwrap();
1925        assert!(cal.is_holiday(easter_monday));
1926
1927        // 11 holidays total
1928        assert_eq!(cal.holidays.len(), 11);
1929    }
1930
1931    #[test]
1932    fn test_french_holidays_2025() {
1933        let cal = HolidayCalendar::for_region(Region::FR, 2025);
1934
1935        // Easter Monday 2025: April 21 (Easter is April 20, 2025)
1936        let easter_monday = NaiveDate::from_ymd_opt(2025, 4, 21).unwrap();
1937        assert!(cal.is_holiday(easter_monday));
1938
1939        // Ascension 2025: May 29 (Easter + 39)
1940        let ascension = NaiveDate::from_ymd_opt(2025, 5, 29).unwrap();
1941        assert!(cal.is_holiday(ascension));
1942    }
1943
1944    #[test]
1945    fn test_italian_holidays() {
1946        let cal = HolidayCalendar::for_region(Region::IT, 2024);
1947
1948        // Ferragosto - August 15
1949        let ferragosto = NaiveDate::from_ymd_opt(2024, 8, 15).unwrap();
1950        assert!(cal.is_holiday(ferragosto));
1951
1952        // Festa della Repubblica - June 2
1953        let repubblica = NaiveDate::from_ymd_opt(2024, 6, 2).unwrap();
1954        assert!(cal.is_holiday(repubblica));
1955
1956        // Santo Stefano - December 26
1957        let stefano = NaiveDate::from_ymd_opt(2024, 12, 26).unwrap();
1958        assert!(cal.is_holiday(stefano));
1959
1960        // Epifania - January 6
1961        let epifania = NaiveDate::from_ymd_opt(2024, 1, 6).unwrap();
1962        assert!(cal.is_holiday(epifania));
1963
1964        // 11 holidays total (including Easter Monday)
1965        assert_eq!(cal.holidays.len(), 11);
1966    }
1967
1968    #[test]
1969    fn test_spanish_holidays() {
1970        let cal = HolidayCalendar::for_region(Region::ES, 2024);
1971
1972        // Fiesta Nacional - October 12
1973        let national = NaiveDate::from_ymd_opt(2024, 10, 12).unwrap();
1974        assert!(cal.is_holiday(national));
1975
1976        // Día de la Constitución - December 6
1977        let constitution = NaiveDate::from_ymd_opt(2024, 12, 6).unwrap();
1978        assert!(cal.is_holiday(constitution));
1979
1980        // Viernes Santo 2024: March 29 (Easter March 31 - 2)
1981        let good_friday = NaiveDate::from_ymd_opt(2024, 3, 29).unwrap();
1982        assert!(cal.is_holiday(good_friday));
1983
1984        // 10 holidays total
1985        assert_eq!(cal.holidays.len(), 10);
1986    }
1987
1988    #[test]
1989    fn test_canadian_holidays() {
1990        let cal = HolidayCalendar::for_region(Region::CA, 2024);
1991
1992        // Canada Day - July 1
1993        let canada_day = NaiveDate::from_ymd_opt(2024, 7, 1).unwrap();
1994        assert!(cal.is_holiday(canada_day));
1995
1996        // Thanksgiving - 2nd Monday of October 2024 = October 14
1997        let thanksgiving = NaiveDate::from_ymd_opt(2024, 10, 14).unwrap();
1998        assert!(cal.is_holiday(thanksgiving));
1999
2000        // Victoria Day 2024 - Monday before May 25 = May 20
2001        let victoria = NaiveDate::from_ymd_opt(2024, 5, 20).unwrap();
2002        assert!(cal.is_holiday(victoria));
2003
2004        // Labour Day 2024 - 1st Monday of September = September 2
2005        let labour = NaiveDate::from_ymd_opt(2024, 9, 2).unwrap();
2006        assert!(cal.is_holiday(labour));
2007
2008        // 10 holidays total
2009        assert_eq!(cal.holidays.len(), 10);
2010    }
2011
2012    #[test]
2013    fn test_canadian_holidays_2025() {
2014        let cal = HolidayCalendar::for_region(Region::CA, 2025);
2015
2016        // Victoria Day 2025 - Monday before May 25 = May 19
2017        let victoria = NaiveDate::from_ymd_opt(2025, 5, 19).unwrap();
2018        assert!(cal.is_holiday(victoria));
2019
2020        // Thanksgiving 2025 - 2nd Monday of October = October 13
2021        let thanksgiving = NaiveDate::from_ymd_opt(2025, 10, 13).unwrap();
2022        assert!(cal.is_holiday(thanksgiving));
2023    }
2024
2025    // -----------------------------------------------------------------
2026    // Parity tests: for_region() vs from_country_pack()
2027    // -----------------------------------------------------------------
2028
2029    /// Extract sorted unique dates from a holiday calendar.
2030    fn sorted_dates(cal: &HolidayCalendar) -> Vec<NaiveDate> {
2031        let mut dates = cal.all_dates();
2032        dates.sort();
2033        dates.dedup();
2034        dates
2035    }
2036
2037    #[test]
2038    fn test_us_country_pack_parity_2024() {
2039        let reg = crate::CountryPackRegistry::builtin_only().expect("builtin registry");
2040        let us_pack = reg.get_by_str("US");
2041
2042        let legacy = HolidayCalendar::for_region(Region::US, 2024);
2043        let pack_cal = HolidayCalendar::from_country_pack(us_pack, 2024);
2044
2045        let legacy_dates = sorted_dates(&legacy);
2046        let pack_dates = sorted_dates(&pack_cal);
2047
2048        // Every legacy date must appear in the pack-derived calendar.
2049        for date in &legacy_dates {
2050            assert!(
2051                pack_cal.is_holiday(*date),
2052                "US pack calendar missing legacy holiday on {date}"
2053            );
2054        }
2055
2056        // Every pack date must appear in the legacy calendar.
2057        for date in &pack_dates {
2058            assert!(
2059                legacy.is_holiday(*date),
2060                "Legacy US calendar missing pack holiday on {date}"
2061            );
2062        }
2063
2064        assert_eq!(
2065            legacy_dates.len(),
2066            pack_dates.len(),
2067            "US holiday count mismatch: legacy={}, pack={}",
2068            legacy_dates.len(),
2069            pack_dates.len()
2070        );
2071    }
2072
2073    #[test]
2074    fn test_us_country_pack_parity_2025() {
2075        let reg = crate::CountryPackRegistry::builtin_only().expect("builtin registry");
2076        let us_pack = reg.get_by_str("US");
2077
2078        let legacy = HolidayCalendar::for_region(Region::US, 2025);
2079        let pack_cal = HolidayCalendar::from_country_pack(us_pack, 2025);
2080
2081        let legacy_dates = sorted_dates(&legacy);
2082        let pack_dates = sorted_dates(&pack_cal);
2083
2084        for date in &legacy_dates {
2085            assert!(
2086                pack_cal.is_holiday(*date),
2087                "US 2025 pack calendar missing legacy holiday on {date}"
2088            );
2089        }
2090        for date in &pack_dates {
2091            assert!(
2092                legacy.is_holiday(*date),
2093                "Legacy US 2025 calendar missing pack holiday on {date}"
2094            );
2095        }
2096        assert_eq!(legacy_dates.len(), pack_dates.len());
2097    }
2098
2099    #[test]
2100    fn test_de_country_pack_parity_2024() {
2101        let reg = crate::CountryPackRegistry::builtin_only().expect("builtin registry");
2102        let de_pack = reg.get_by_str("DE");
2103
2104        let legacy = HolidayCalendar::for_region(Region::DE, 2024);
2105        let pack_cal = HolidayCalendar::from_country_pack(de_pack, 2024);
2106
2107        let legacy_dates = sorted_dates(&legacy);
2108        let pack_dates = sorted_dates(&pack_cal);
2109
2110        for date in &legacy_dates {
2111            assert!(
2112                pack_cal.is_holiday(*date),
2113                "DE pack calendar missing legacy holiday on {date}"
2114            );
2115        }
2116        for date in &pack_dates {
2117            assert!(
2118                legacy.is_holiday(*date),
2119                "Legacy DE calendar missing pack holiday on {date}"
2120            );
2121        }
2122        assert_eq!(
2123            legacy_dates.len(),
2124            pack_dates.len(),
2125            "DE holiday count mismatch: legacy={}, pack={}",
2126            legacy_dates.len(),
2127            pack_dates.len()
2128        );
2129    }
2130
2131    #[test]
2132    fn test_gb_country_pack_parity_2024() {
2133        let reg = crate::CountryPackRegistry::builtin_only().expect("builtin registry");
2134        let gb_pack = reg.get_by_str("GB");
2135
2136        let legacy = HolidayCalendar::for_region(Region::GB, 2024);
2137        let pack_cal = HolidayCalendar::from_country_pack(gb_pack, 2024);
2138
2139        let legacy_dates = sorted_dates(&legacy);
2140        let pack_dates = sorted_dates(&pack_cal);
2141
2142        for date in &legacy_dates {
2143            assert!(
2144                pack_cal.is_holiday(*date),
2145                "GB pack calendar missing legacy holiday on {date}"
2146            );
2147        }
2148        for date in &pack_dates {
2149            assert!(
2150                legacy.is_holiday(*date),
2151                "Legacy GB calendar missing pack holiday on {date}"
2152            );
2153        }
2154        assert_eq!(
2155            legacy_dates.len(),
2156            pack_dates.len(),
2157            "GB holiday count mismatch: legacy={}, pack={}",
2158            legacy_dates.len(),
2159            pack_dates.len()
2160        );
2161    }
2162}