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