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}
26
27impl std::fmt::Display for Region {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        match self {
30            Region::US => write!(f, "United States"),
31            Region::DE => write!(f, "Germany"),
32            Region::GB => write!(f, "United Kingdom"),
33            Region::CN => write!(f, "China"),
34            Region::JP => write!(f, "Japan"),
35            Region::IN => write!(f, "India"),
36        }
37    }
38}
39
40/// A holiday with its associated activity multiplier.
41#[derive(Debug, Clone)]
42pub struct Holiday {
43    /// Holiday name.
44    pub name: String,
45    /// Date of the holiday.
46    pub date: NaiveDate,
47    /// Activity multiplier (0.0 = completely closed, 1.0 = normal).
48    pub activity_multiplier: f64,
49    /// Whether this is a bank holiday (affects financial transactions).
50    pub is_bank_holiday: bool,
51}
52
53impl Holiday {
54    /// Create a new holiday.
55    pub fn new(name: impl Into<String>, date: NaiveDate, multiplier: f64) -> Self {
56        Self {
57            name: name.into(),
58            date,
59            activity_multiplier: multiplier,
60            is_bank_holiday: true,
61        }
62    }
63
64    /// Set whether this is a bank holiday.
65    pub fn with_bank_holiday(mut self, is_bank_holiday: bool) -> Self {
66        self.is_bank_holiday = is_bank_holiday;
67        self
68    }
69}
70
71/// A calendar of holidays for a specific region and year.
72#[derive(Debug, Clone)]
73pub struct HolidayCalendar {
74    /// Region for this calendar.
75    pub region: Region,
76    /// Year for this calendar.
77    pub year: i32,
78    /// List of holidays.
79    pub holidays: Vec<Holiday>,
80}
81
82impl HolidayCalendar {
83    /// Create a new empty holiday calendar.
84    pub fn new(region: Region, year: i32) -> Self {
85        Self {
86            region,
87            year,
88            holidays: Vec::new(),
89        }
90    }
91
92    /// Create a holiday calendar for a specific region and year.
93    pub fn for_region(region: Region, year: i32) -> Self {
94        match region {
95            Region::US => Self::us_holidays(year),
96            Region::DE => Self::de_holidays(year),
97            Region::GB => Self::gb_holidays(year),
98            Region::CN => Self::cn_holidays(year),
99            Region::JP => Self::jp_holidays(year),
100            Region::IN => Self::in_holidays(year),
101        }
102    }
103
104    /// Check if a date is a holiday.
105    pub fn is_holiday(&self, date: NaiveDate) -> bool {
106        self.holidays.iter().any(|h| h.date == date)
107    }
108
109    /// Get the activity multiplier for a date.
110    pub fn get_multiplier(&self, date: NaiveDate) -> f64 {
111        self.holidays
112            .iter()
113            .find(|h| h.date == date)
114            .map(|h| h.activity_multiplier)
115            .unwrap_or(1.0)
116    }
117
118    /// Get all holidays for a date (may include multiple on same day).
119    pub fn get_holidays(&self, date: NaiveDate) -> Vec<&Holiday> {
120        self.holidays.iter().filter(|h| h.date == date).collect()
121    }
122
123    /// Add a holiday to the calendar.
124    pub fn add_holiday(&mut self, holiday: Holiday) {
125        self.holidays.push(holiday);
126    }
127
128    /// Get all dates in the calendar.
129    pub fn all_dates(&self) -> Vec<NaiveDate> {
130        self.holidays.iter().map(|h| h.date).collect()
131    }
132
133    /// US Federal Holidays.
134    fn us_holidays(year: i32) -> Self {
135        let mut cal = Self::new(Region::US, year);
136
137        // New Year's Day - Jan 1 (observed)
138        let new_years = NaiveDate::from_ymd_opt(year, 1, 1).unwrap();
139        cal.add_holiday(Holiday::new(
140            "New Year's Day",
141            Self::observe_weekend(new_years),
142            0.02,
143        ));
144
145        // Martin Luther King Jr. Day - 3rd Monday of January
146        let mlk = Self::nth_weekday_of_month(year, 1, Weekday::Mon, 3);
147        cal.add_holiday(Holiday::new("Martin Luther King Jr. Day", mlk, 0.1));
148
149        // Presidents' Day - 3rd Monday of February
150        let presidents = Self::nth_weekday_of_month(year, 2, Weekday::Mon, 3);
151        cal.add_holiday(Holiday::new("Presidents' Day", presidents, 0.1));
152
153        // Memorial Day - Last Monday of May
154        let memorial = Self::last_weekday_of_month(year, 5, Weekday::Mon);
155        cal.add_holiday(Holiday::new("Memorial Day", memorial, 0.05));
156
157        // Juneteenth - June 19
158        let juneteenth = NaiveDate::from_ymd_opt(year, 6, 19).unwrap();
159        cal.add_holiday(Holiday::new(
160            "Juneteenth",
161            Self::observe_weekend(juneteenth),
162            0.1,
163        ));
164
165        // Independence Day - July 4
166        let independence = NaiveDate::from_ymd_opt(year, 7, 4).unwrap();
167        cal.add_holiday(Holiday::new(
168            "Independence Day",
169            Self::observe_weekend(independence),
170            0.02,
171        ));
172
173        // Labor Day - 1st Monday of September
174        let labor = Self::nth_weekday_of_month(year, 9, Weekday::Mon, 1);
175        cal.add_holiday(Holiday::new("Labor Day", labor, 0.05));
176
177        // Columbus Day - 2nd Monday of October
178        let columbus = Self::nth_weekday_of_month(year, 10, Weekday::Mon, 2);
179        cal.add_holiday(Holiday::new("Columbus Day", columbus, 0.2));
180
181        // Veterans Day - November 11
182        let veterans = NaiveDate::from_ymd_opt(year, 11, 11).unwrap();
183        cal.add_holiday(Holiday::new(
184            "Veterans Day",
185            Self::observe_weekend(veterans),
186            0.1,
187        ));
188
189        // Thanksgiving - 4th Thursday of November
190        let thanksgiving = Self::nth_weekday_of_month(year, 11, Weekday::Thu, 4);
191        cal.add_holiday(Holiday::new("Thanksgiving", thanksgiving, 0.02));
192
193        // Day after Thanksgiving
194        cal.add_holiday(Holiday::new(
195            "Day after Thanksgiving",
196            thanksgiving + Duration::days(1),
197            0.1,
198        ));
199
200        // Christmas Eve - December 24
201        let christmas_eve = NaiveDate::from_ymd_opt(year, 12, 24).unwrap();
202        cal.add_holiday(Holiday::new("Christmas Eve", christmas_eve, 0.1));
203
204        // Christmas Day - December 25
205        let christmas = NaiveDate::from_ymd_opt(year, 12, 25).unwrap();
206        cal.add_holiday(Holiday::new(
207            "Christmas Day",
208            Self::observe_weekend(christmas),
209            0.02,
210        ));
211
212        // New Year's Eve - December 31
213        let new_years_eve = NaiveDate::from_ymd_opt(year, 12, 31).unwrap();
214        cal.add_holiday(Holiday::new("New Year's Eve", new_years_eve, 0.1));
215
216        cal
217    }
218
219    /// German holidays (nationwide).
220    fn de_holidays(year: i32) -> Self {
221        let mut cal = Self::new(Region::DE, year);
222
223        // Neujahr - January 1
224        cal.add_holiday(Holiday::new(
225            "Neujahr",
226            NaiveDate::from_ymd_opt(year, 1, 1).unwrap(),
227            0.02,
228        ));
229
230        // Karfreitag - Good Friday (Easter - 2 days)
231        let easter = Self::easter_date(year);
232        cal.add_holiday(Holiday::new("Karfreitag", easter - Duration::days(2), 0.02));
233
234        // Ostermontag - Easter Monday
235        cal.add_holiday(Holiday::new(
236            "Ostermontag",
237            easter + Duration::days(1),
238            0.02,
239        ));
240
241        // Tag der Arbeit - May 1
242        cal.add_holiday(Holiday::new(
243            "Tag der Arbeit",
244            NaiveDate::from_ymd_opt(year, 5, 1).unwrap(),
245            0.02,
246        ));
247
248        // Christi Himmelfahrt - Ascension Day (Easter + 39 days)
249        cal.add_holiday(Holiday::new(
250            "Christi Himmelfahrt",
251            easter + Duration::days(39),
252            0.02,
253        ));
254
255        // Pfingstmontag - Whit Monday (Easter + 50 days)
256        cal.add_holiday(Holiday::new(
257            "Pfingstmontag",
258            easter + Duration::days(50),
259            0.02,
260        ));
261
262        // Tag der Deutschen Einheit - October 3
263        cal.add_holiday(Holiday::new(
264            "Tag der Deutschen Einheit",
265            NaiveDate::from_ymd_opt(year, 10, 3).unwrap(),
266            0.02,
267        ));
268
269        // Weihnachten - December 25-26
270        cal.add_holiday(Holiday::new(
271            "1. Weihnachtstag",
272            NaiveDate::from_ymd_opt(year, 12, 25).unwrap(),
273            0.02,
274        ));
275        cal.add_holiday(Holiday::new(
276            "2. Weihnachtstag",
277            NaiveDate::from_ymd_opt(year, 12, 26).unwrap(),
278            0.02,
279        ));
280
281        // Silvester - December 31
282        cal.add_holiday(Holiday::new(
283            "Silvester",
284            NaiveDate::from_ymd_opt(year, 12, 31).unwrap(),
285            0.1,
286        ));
287
288        cal
289    }
290
291    /// UK bank holidays.
292    fn gb_holidays(year: i32) -> Self {
293        let mut cal = Self::new(Region::GB, year);
294
295        // New Year's Day
296        let new_years = NaiveDate::from_ymd_opt(year, 1, 1).unwrap();
297        cal.add_holiday(Holiday::new(
298            "New Year's Day",
299            Self::observe_weekend(new_years),
300            0.02,
301        ));
302
303        // Good Friday
304        let easter = Self::easter_date(year);
305        cal.add_holiday(Holiday::new(
306            "Good Friday",
307            easter - Duration::days(2),
308            0.02,
309        ));
310
311        // Easter Monday
312        cal.add_holiday(Holiday::new(
313            "Easter Monday",
314            easter + Duration::days(1),
315            0.02,
316        ));
317
318        // Early May Bank Holiday - 1st Monday of May
319        let early_may = Self::nth_weekday_of_month(year, 5, Weekday::Mon, 1);
320        cal.add_holiday(Holiday::new("Early May Bank Holiday", early_may, 0.02));
321
322        // Spring Bank Holiday - Last Monday of May
323        let spring = Self::last_weekday_of_month(year, 5, Weekday::Mon);
324        cal.add_holiday(Holiday::new("Spring Bank Holiday", spring, 0.02));
325
326        // Summer Bank Holiday - Last Monday of August
327        let summer = Self::last_weekday_of_month(year, 8, Weekday::Mon);
328        cal.add_holiday(Holiday::new("Summer Bank Holiday", summer, 0.02));
329
330        // Christmas Day
331        let christmas = NaiveDate::from_ymd_opt(year, 12, 25).unwrap();
332        cal.add_holiday(Holiday::new(
333            "Christmas Day",
334            Self::observe_weekend(christmas),
335            0.02,
336        ));
337
338        // Boxing Day
339        let boxing = NaiveDate::from_ymd_opt(year, 12, 26).unwrap();
340        cal.add_holiday(Holiday::new(
341            "Boxing Day",
342            Self::observe_weekend(boxing),
343            0.02,
344        ));
345
346        cal
347    }
348
349    /// Chinese holidays (simplified - fixed dates only).
350    fn cn_holidays(year: i32) -> Self {
351        let mut cal = Self::new(Region::CN, year);
352
353        // New Year's Day - January 1
354        cal.add_holiday(Holiday::new(
355            "New Year",
356            NaiveDate::from_ymd_opt(year, 1, 1).unwrap(),
357            0.05,
358        ));
359
360        // Spring Festival (Chinese New Year) - approximate late Jan/early Feb
361        // Using a simplified calculation - typically 7-day holiday
362        let cny = Self::approximate_chinese_new_year(year);
363        for i in 0..7 {
364            cal.add_holiday(Holiday::new(
365                if i == 0 {
366                    "Spring Festival"
367                } else {
368                    "Spring Festival Holiday"
369                },
370                cny + Duration::days(i),
371                0.02,
372            ));
373        }
374
375        // Qingming Festival - April 4-6 (approximate)
376        cal.add_holiday(Holiday::new(
377            "Qingming Festival",
378            NaiveDate::from_ymd_opt(year, 4, 5).unwrap(),
379            0.05,
380        ));
381
382        // Labor Day - May 1 (3-day holiday)
383        for i in 0..3 {
384            cal.add_holiday(Holiday::new(
385                if i == 0 {
386                    "Labor Day"
387                } else {
388                    "Labor Day Holiday"
389                },
390                NaiveDate::from_ymd_opt(year, 5, 1).unwrap() + Duration::days(i),
391                0.05,
392            ));
393        }
394
395        // Dragon Boat Festival - approximate early June
396        cal.add_holiday(Holiday::new(
397            "Dragon Boat Festival",
398            NaiveDate::from_ymd_opt(year, 6, 10).unwrap(),
399            0.05,
400        ));
401
402        // Mid-Autumn Festival - approximate late September
403        cal.add_holiday(Holiday::new(
404            "Mid-Autumn Festival",
405            NaiveDate::from_ymd_opt(year, 9, 15).unwrap(),
406            0.05,
407        ));
408
409        // National Day - October 1 (7-day holiday)
410        for i in 0..7 {
411            cal.add_holiday(Holiday::new(
412                if i == 0 {
413                    "National Day"
414                } else {
415                    "National Day Holiday"
416                },
417                NaiveDate::from_ymd_opt(year, 10, 1).unwrap() + Duration::days(i),
418                0.02,
419            ));
420        }
421
422        cal
423    }
424
425    /// Japanese holidays.
426    fn jp_holidays(year: i32) -> Self {
427        let mut cal = Self::new(Region::JP, year);
428
429        // Ganjitsu - January 1
430        cal.add_holiday(Holiday::new(
431            "Ganjitsu (New Year)",
432            NaiveDate::from_ymd_opt(year, 1, 1).unwrap(),
433            0.02,
434        ));
435
436        // New Year holidays - January 2-3
437        cal.add_holiday(Holiday::new(
438            "New Year Holiday",
439            NaiveDate::from_ymd_opt(year, 1, 2).unwrap(),
440            0.05,
441        ));
442        cal.add_holiday(Holiday::new(
443            "New Year Holiday",
444            NaiveDate::from_ymd_opt(year, 1, 3).unwrap(),
445            0.05,
446        ));
447
448        // Seijin no Hi - Coming of Age Day - 2nd Monday of January
449        let seijin = Self::nth_weekday_of_month(year, 1, Weekday::Mon, 2);
450        cal.add_holiday(Holiday::new("Seijin no Hi", seijin, 0.05));
451
452        // Kenkoku Kinen no Hi - National Foundation Day - February 11
453        cal.add_holiday(Holiday::new(
454            "Kenkoku Kinen no Hi",
455            NaiveDate::from_ymd_opt(year, 2, 11).unwrap(),
456            0.02,
457        ));
458
459        // Tenno Tanjobi - Emperor's Birthday - February 23
460        cal.add_holiday(Holiday::new(
461            "Tenno Tanjobi",
462            NaiveDate::from_ymd_opt(year, 2, 23).unwrap(),
463            0.02,
464        ));
465
466        // Shunbun no Hi - Vernal Equinox - around March 20-21
467        cal.add_holiday(Holiday::new(
468            "Shunbun no Hi",
469            NaiveDate::from_ymd_opt(year, 3, 20).unwrap(),
470            0.02,
471        ));
472
473        // Showa no Hi - Showa Day - April 29
474        cal.add_holiday(Holiday::new(
475            "Showa no Hi",
476            NaiveDate::from_ymd_opt(year, 4, 29).unwrap(),
477            0.02,
478        ));
479
480        // Golden Week - April 29 - May 5
481        cal.add_holiday(Holiday::new(
482            "Kenpo Kinenbi",
483            NaiveDate::from_ymd_opt(year, 5, 3).unwrap(),
484            0.02,
485        ));
486        cal.add_holiday(Holiday::new(
487            "Midori no Hi",
488            NaiveDate::from_ymd_opt(year, 5, 4).unwrap(),
489            0.02,
490        ));
491        cal.add_holiday(Holiday::new(
492            "Kodomo no Hi",
493            NaiveDate::from_ymd_opt(year, 5, 5).unwrap(),
494            0.02,
495        ));
496
497        // Umi no Hi - Marine Day - 3rd Monday of July
498        let umi = Self::nth_weekday_of_month(year, 7, Weekday::Mon, 3);
499        cal.add_holiday(Holiday::new("Umi no Hi", umi, 0.05));
500
501        // Yama no Hi - Mountain Day - August 11
502        cal.add_holiday(Holiday::new(
503            "Yama no Hi",
504            NaiveDate::from_ymd_opt(year, 8, 11).unwrap(),
505            0.05,
506        ));
507
508        // Keiro no Hi - Respect for the Aged Day - 3rd Monday of September
509        let keiro = Self::nth_weekday_of_month(year, 9, Weekday::Mon, 3);
510        cal.add_holiday(Holiday::new("Keiro no Hi", keiro, 0.05));
511
512        // Shubun no Hi - Autumnal Equinox - around September 22-23
513        cal.add_holiday(Holiday::new(
514            "Shubun no Hi",
515            NaiveDate::from_ymd_opt(year, 9, 23).unwrap(),
516            0.02,
517        ));
518
519        // Sports Day - 2nd Monday of October
520        let sports = Self::nth_weekday_of_month(year, 10, Weekday::Mon, 2);
521        cal.add_holiday(Holiday::new("Sports Day", sports, 0.05));
522
523        // Bunka no Hi - Culture Day - November 3
524        cal.add_holiday(Holiday::new(
525            "Bunka no Hi",
526            NaiveDate::from_ymd_opt(year, 11, 3).unwrap(),
527            0.02,
528        ));
529
530        // Kinro Kansha no Hi - Labor Thanksgiving Day - November 23
531        cal.add_holiday(Holiday::new(
532            "Kinro Kansha no Hi",
533            NaiveDate::from_ymd_opt(year, 11, 23).unwrap(),
534            0.02,
535        ));
536
537        cal
538    }
539
540    /// Indian holidays (national holidays).
541    fn in_holidays(year: i32) -> Self {
542        let mut cal = Self::new(Region::IN, year);
543
544        // Republic Day - January 26
545        cal.add_holiday(Holiday::new(
546            "Republic Day",
547            NaiveDate::from_ymd_opt(year, 1, 26).unwrap(),
548            0.02,
549        ));
550
551        // Holi - approximate March (lunar calendar)
552        cal.add_holiday(Holiday::new(
553            "Holi",
554            NaiveDate::from_ymd_opt(year, 3, 10).unwrap(),
555            0.05,
556        ));
557
558        // Good Friday
559        let easter = Self::easter_date(year);
560        cal.add_holiday(Holiday::new(
561            "Good Friday",
562            easter - Duration::days(2),
563            0.05,
564        ));
565
566        // Independence Day - August 15
567        cal.add_holiday(Holiday::new(
568            "Independence Day",
569            NaiveDate::from_ymd_opt(year, 8, 15).unwrap(),
570            0.02,
571        ));
572
573        // Gandhi Jayanti - October 2
574        cal.add_holiday(Holiday::new(
575            "Gandhi Jayanti",
576            NaiveDate::from_ymd_opt(year, 10, 2).unwrap(),
577            0.02,
578        ));
579
580        // Dussehra - approximate October (lunar calendar)
581        cal.add_holiday(Holiday::new(
582            "Dussehra",
583            NaiveDate::from_ymd_opt(year, 10, 15).unwrap(),
584            0.05,
585        ));
586
587        // Diwali - approximate October/November (5-day festival)
588        let diwali = Self::approximate_diwali(year);
589        for i in 0..5 {
590            cal.add_holiday(Holiday::new(
591                match i {
592                    0 => "Dhanteras",
593                    1 => "Naraka Chaturdashi",
594                    2 => "Diwali",
595                    3 => "Govardhan Puja",
596                    _ => "Bhai Dooj",
597                },
598                diwali + Duration::days(i),
599                if i == 2 { 0.02 } else { 0.1 },
600            ));
601        }
602
603        // Christmas - December 25
604        cal.add_holiday(Holiday::new(
605            "Christmas",
606            NaiveDate::from_ymd_opt(year, 12, 25).unwrap(),
607            0.1,
608        ));
609
610        cal
611    }
612
613    /// Calculate Easter date using the anonymous Gregorian algorithm.
614    fn easter_date(year: i32) -> NaiveDate {
615        let a = year % 19;
616        let b = year / 100;
617        let c = year % 100;
618        let d = b / 4;
619        let e = b % 4;
620        let f = (b + 8) / 25;
621        let g = (b - f + 1) / 3;
622        let h = (19 * a + b - d - g + 15) % 30;
623        let i = c / 4;
624        let k = c % 4;
625        let l = (32 + 2 * e + 2 * i - h - k) % 7;
626        let m = (a + 11 * h + 22 * l) / 451;
627        let month = (h + l - 7 * m + 114) / 31;
628        let day = ((h + l - 7 * m + 114) % 31) + 1;
629
630        NaiveDate::from_ymd_opt(year, month as u32, day as u32).unwrap()
631    }
632
633    /// Get nth weekday of a month (e.g., 3rd Monday of January).
634    fn nth_weekday_of_month(year: i32, month: u32, weekday: Weekday, n: u32) -> NaiveDate {
635        let first = NaiveDate::from_ymd_opt(year, month, 1).unwrap();
636        let first_weekday = first.weekday();
637
638        let days_until = (weekday.num_days_from_monday() as i64
639            - first_weekday.num_days_from_monday() as i64
640            + 7)
641            % 7;
642
643        first + Duration::days(days_until + (n - 1) as i64 * 7)
644    }
645
646    /// Get last weekday of a month (e.g., last Monday of May).
647    fn last_weekday_of_month(year: i32, month: u32, weekday: Weekday) -> NaiveDate {
648        let last = if month == 12 {
649            NaiveDate::from_ymd_opt(year + 1, 1, 1).unwrap() - Duration::days(1)
650        } else {
651            NaiveDate::from_ymd_opt(year, month + 1, 1).unwrap() - Duration::days(1)
652        };
653
654        let last_weekday = last.weekday();
655        let days_back = (last_weekday.num_days_from_monday() as i64
656            - weekday.num_days_from_monday() as i64
657            + 7)
658            % 7;
659
660        last - Duration::days(days_back)
661    }
662
663    /// Observe weekend holidays on nearest weekday.
664    fn observe_weekend(date: NaiveDate) -> NaiveDate {
665        match date.weekday() {
666            Weekday::Sat => date - Duration::days(1), // Friday
667            Weekday::Sun => date + Duration::days(1), // Monday
668            _ => date,
669        }
670    }
671
672    /// Approximate Chinese New Year date (simplified calculation).
673    fn approximate_chinese_new_year(year: i32) -> NaiveDate {
674        // Chinese New Year falls between Jan 21 and Feb 20
675        // This is a simplified approximation
676        let base_year = 2000;
677        let cny_2000 = NaiveDate::from_ymd_opt(2000, 2, 5).unwrap();
678
679        let years_diff = year - base_year;
680        let lunar_cycle = 29.5306; // days per lunar month
681        let days_offset = (years_diff as f64 * 12.0 * lunar_cycle) % 365.25;
682
683        let mut result = cny_2000 + Duration::days(days_offset as i64);
684
685        // Ensure it falls in Jan-Feb range
686        while result.month() > 2 || (result.month() == 2 && result.day() > 20) {
687            result -= Duration::days(29);
688        }
689        while result.month() < 1 || (result.month() == 1 && result.day() < 21) {
690            result += Duration::days(29);
691        }
692
693        // Adjust year if needed
694        if result.year() != year {
695            result = NaiveDate::from_ymd_opt(year, result.month(), result.day().min(28))
696                .unwrap_or_else(|| NaiveDate::from_ymd_opt(year, result.month(), 28).unwrap());
697        }
698
699        result
700    }
701
702    /// Approximate Diwali date (simplified calculation).
703    fn approximate_diwali(year: i32) -> NaiveDate {
704        // Diwali typically falls in October-November
705        // This is a simplified approximation
706        match year % 4 {
707            0 => NaiveDate::from_ymd_opt(year, 11, 1).unwrap(),
708            1 => NaiveDate::from_ymd_opt(year, 10, 24).unwrap(),
709            2 => NaiveDate::from_ymd_opt(year, 11, 12).unwrap(),
710            _ => NaiveDate::from_ymd_opt(year, 11, 4).unwrap(),
711        }
712    }
713}
714
715/// Custom holiday configuration for YAML/JSON input.
716#[derive(Debug, Clone, Serialize, Deserialize)]
717pub struct CustomHolidayConfig {
718    /// Holiday name.
719    pub name: String,
720    /// Month (1-12).
721    pub month: u8,
722    /// Day of month.
723    pub day: u8,
724    /// Activity multiplier (optional, defaults to 0.05).
725    #[serde(default = "default_holiday_multiplier")]
726    pub activity_multiplier: f64,
727}
728
729fn default_holiday_multiplier() -> f64 {
730    0.05
731}
732
733impl CustomHolidayConfig {
734    /// Convert to a Holiday for a specific year.
735    pub fn to_holiday(&self, year: i32) -> Holiday {
736        Holiday::new(
737            &self.name,
738            NaiveDate::from_ymd_opt(year, self.month as u32, self.day as u32).unwrap(),
739            self.activity_multiplier,
740        )
741    }
742}
743
744#[cfg(test)]
745mod tests {
746    use super::*;
747
748    #[test]
749    fn test_us_holidays() {
750        let cal = HolidayCalendar::for_region(Region::US, 2024);
751
752        // Check some specific holidays exist
753        let christmas = NaiveDate::from_ymd_opt(2024, 12, 25).unwrap();
754        assert!(cal.is_holiday(christmas));
755
756        // Independence Day (observed on Friday since July 4 is Thursday in 2024)
757        let independence = NaiveDate::from_ymd_opt(2024, 7, 4).unwrap();
758        assert!(cal.is_holiday(independence));
759    }
760
761    #[test]
762    fn test_german_holidays() {
763        let cal = HolidayCalendar::for_region(Region::DE, 2024);
764
765        // Tag der Deutschen Einheit - October 3
766        let unity = NaiveDate::from_ymd_opt(2024, 10, 3).unwrap();
767        assert!(cal.is_holiday(unity));
768    }
769
770    #[test]
771    fn test_easter_calculation() {
772        // Known Easter dates
773        assert_eq!(
774            HolidayCalendar::easter_date(2024),
775            NaiveDate::from_ymd_opt(2024, 3, 31).unwrap()
776        );
777        assert_eq!(
778            HolidayCalendar::easter_date(2025),
779            NaiveDate::from_ymd_opt(2025, 4, 20).unwrap()
780        );
781    }
782
783    #[test]
784    fn test_nth_weekday() {
785        // 3rd Monday of January 2024
786        let mlk = HolidayCalendar::nth_weekday_of_month(2024, 1, Weekday::Mon, 3);
787        assert_eq!(mlk, NaiveDate::from_ymd_opt(2024, 1, 15).unwrap());
788
789        // 4th Thursday of November 2024 (Thanksgiving)
790        let thanksgiving = HolidayCalendar::nth_weekday_of_month(2024, 11, Weekday::Thu, 4);
791        assert_eq!(thanksgiving, NaiveDate::from_ymd_opt(2024, 11, 28).unwrap());
792    }
793
794    #[test]
795    fn test_last_weekday() {
796        // Last Monday of May 2024 (Memorial Day)
797        let memorial = HolidayCalendar::last_weekday_of_month(2024, 5, Weekday::Mon);
798        assert_eq!(memorial, NaiveDate::from_ymd_opt(2024, 5, 27).unwrap());
799    }
800
801    #[test]
802    fn test_activity_multiplier() {
803        let cal = HolidayCalendar::for_region(Region::US, 2024);
804
805        // Holiday should have low multiplier
806        let christmas = NaiveDate::from_ymd_opt(2024, 12, 25).unwrap();
807        assert!(cal.get_multiplier(christmas) < 0.1);
808
809        // Regular day should be 1.0
810        let regular = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
811        assert!((cal.get_multiplier(regular) - 1.0).abs() < 0.01);
812    }
813
814    #[test]
815    fn test_all_regions_have_holidays() {
816        let regions = [
817            Region::US,
818            Region::DE,
819            Region::GB,
820            Region::CN,
821            Region::JP,
822            Region::IN,
823        ];
824
825        for region in regions {
826            let cal = HolidayCalendar::for_region(region, 2024);
827            assert!(
828                !cal.holidays.is_empty(),
829                "Region {:?} should have holidays",
830                region
831            );
832        }
833    }
834
835    #[test]
836    fn test_chinese_holidays() {
837        let cal = HolidayCalendar::for_region(Region::CN, 2024);
838
839        // National Day - October 1
840        let national = NaiveDate::from_ymd_opt(2024, 10, 1).unwrap();
841        assert!(cal.is_holiday(national));
842    }
843
844    #[test]
845    fn test_japanese_golden_week() {
846        let cal = HolidayCalendar::for_region(Region::JP, 2024);
847
848        // Check Golden Week holidays
849        let kodomo = NaiveDate::from_ymd_opt(2024, 5, 5).unwrap();
850        assert!(cal.is_holiday(kodomo));
851    }
852}