1use chrono::{Datelike, Duration, NaiveDate, Weekday};
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
11#[serde(rename_all = "UPPERCASE")]
12pub enum Region {
13 US,
15 DE,
17 GB,
19 CN,
21 JP,
23 IN,
25 BR,
27 MX,
29 AU,
31 SG,
33 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#[derive(Debug, Clone)]
57pub struct Holiday {
58 pub name: String,
60 pub date: NaiveDate,
62 pub activity_multiplier: f64,
64 pub is_bank_holiday: bool,
66}
67
68impl Holiday {
69 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 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#[derive(Debug, Clone)]
88pub struct HolidayCalendar {
89 pub region: Region,
91 pub year: i32,
93 pub holidays: Vec<Holiday>,
95}
96
97impl HolidayCalendar {
98 pub fn new(region: Region, year: i32) -> Self {
100 Self {
101 region,
102 year,
103 holidays: Vec::new(),
104 }
105 }
106
107 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 pub fn is_holiday(&self, date: NaiveDate) -> bool {
126 self.holidays.iter().any(|h| h.date == date)
127 }
128
129 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 pub fn get_holidays(&self, date: NaiveDate) -> Vec<&Holiday> {
140 self.holidays.iter().filter(|h| h.date == date).collect()
141 }
142
143 pub fn add_holiday(&mut self, holiday: Holiday) {
145 self.holidays.push(holiday);
146 }
147
148 pub fn all_dates(&self) -> Vec<NaiveDate> {
150 self.holidays.iter().map(|h| h.date).collect()
151 }
152
153 pub fn from_country_pack(pack: &crate::country::schema::CountryPack, year: i32) -> Self {
160 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 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 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 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 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 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 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 fn us_holidays(year: i32) -> Self {
246 let mut cal = Self::new(Region::US, year);
247
248 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 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 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 let memorial = Self::last_weekday_of_month(year, 5, Weekday::Mon);
266 cal.add_holiday(Holiday::new("Memorial Day", memorial, 0.05));
267
268 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 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 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 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 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 let thanksgiving = Self::nth_weekday_of_month(year, 11, Weekday::Thu, 4);
302 cal.add_holiday(Holiday::new("Thanksgiving", thanksgiving, 0.02));
303
304 cal.add_holiday(Holiday::new(
306 "Day after Thanksgiving",
307 thanksgiving + Duration::days(1),
308 0.1,
309 ));
310
311 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 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 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 fn de_holidays(year: i32) -> Self {
332 let mut cal = Self::new(Region::DE, year);
333
334 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 let easter = Self::easter_date(year);
343 cal.add_holiday(Holiday::new("Karfreitag", easter - Duration::days(2), 0.02));
344
345 cal.add_holiday(Holiday::new(
347 "Ostermontag",
348 easter + Duration::days(1),
349 0.02,
350 ));
351
352 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 cal.add_holiday(Holiday::new(
361 "Christi Himmelfahrt",
362 easter + Duration::days(39),
363 0.02,
364 ));
365
366 cal.add_holiday(Holiday::new(
368 "Pfingstmontag",
369 easter + Duration::days(50),
370 0.02,
371 ));
372
373 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 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 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 fn gb_holidays(year: i32) -> Self {
404 let mut cal = Self::new(Region::GB, year);
405
406 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 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 cal.add_holiday(Holiday::new(
424 "Easter Monday",
425 easter + Duration::days(1),
426 0.02,
427 ));
428
429 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 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 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 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 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 fn cn_holidays(year: i32) -> Self {
462 let mut cal = Self::new(Region::CN, year);
463
464 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 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 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 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 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 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 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 fn jp_holidays(year: i32) -> Self {
540 let mut cal = Self::new(Region::JP, year);
541
542 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 fn in_holidays(year: i32) -> Self {
655 let mut cal = Self::new(Region::IN, year);
656
657 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 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 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 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 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 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 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 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 fn br_holidays(year: i32) -> Self {
728 let mut cal = Self::new(Region::BR, year);
729
730 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 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 cal.add_holiday(Holiday::new(
746 "Sexta-feira Santa",
747 easter - Duration::days(2),
748 0.02,
749 ));
750
751 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 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 cal.add_holiday(Holiday::new(
767 "Corpus Christi",
768 easter + Duration::days(60),
769 0.05,
770 ));
771
772 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 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 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 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 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 fn mx_holidays(year: i32) -> Self {
812 let mut cal = Self::new(Region::MX, year);
813
814 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 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 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 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 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 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 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 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 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 fn au_holidays(year: i32) -> Self {
884 let mut cal = Self::new(Region::AU, year);
885
886 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 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 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 cal.add_holiday(Holiday::new(
912 "Easter Saturday",
913 easter - Duration::days(1),
914 0.02,
915 ));
916
917 cal.add_holiday(Holiday::new(
919 "Easter Monday",
920 easter + Duration::days(1),
921 0.02,
922 ));
923
924 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 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 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 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 fn sg_holidays(year: i32) -> Self {
953 let mut cal = Self::new(Region::SG, year);
954
955 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 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 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 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 let vesak = Self::approximate_vesak(year);
988 cal.add_holiday(Holiday::new("Vesak Day", vesak, 0.02));
989
990 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 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 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 let deepavali = Self::approximate_deepavali(year);
1007 cal.add_holiday(Holiday::new("Deepavali", deepavali, 0.02));
1008
1009 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 fn kr_holidays(year: i32) -> Self {
1021 let mut cal = Self::new(Region::KR, year);
1022
1023 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 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 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 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 let buddha_birthday = Self::approximate_korean_buddha_birthday(year);
1060 cal.add_holiday(Holiday::new("Seokgatansinil", buddha_birthday, 0.02));
1061
1062 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 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 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 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 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 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 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 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 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 fn observe_weekend(date: NaiveDate) -> NaiveDate {
1168 match date.weekday() {
1169 Weekday::Sat => date - Duration::days(1), Weekday::Sun => date + Duration::days(1), _ => date,
1172 }
1173 }
1174
1175 fn approximate_chinese_new_year(year: i32) -> NaiveDate {
1177 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; 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 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 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 fn approximate_diwali(year: i32) -> NaiveDate {
1210 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 fn approximate_vesak(year: i32) -> NaiveDate {
1223 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 fn approximate_hari_raya_puasa(year: i32) -> NaiveDate {
1255 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 while result.year() != year {
1265 if result.year() > year {
1266 result -= Duration::days(354); } else {
1268 result += Duration::days(354);
1269 }
1270 }
1271 result
1272 }
1273
1274 fn approximate_hari_raya_haji(year: i32) -> NaiveDate {
1277 Self::approximate_hari_raya_puasa(year) + Duration::days(70)
1278 }
1279
1280 fn approximate_deepavali(year: i32) -> NaiveDate {
1282 Self::approximate_diwali(year)
1283 }
1284
1285 fn approximate_korean_new_year(year: i32) -> NaiveDate {
1288 Self::approximate_chinese_new_year(year)
1289 }
1290
1291 fn approximate_korean_buddha_birthday(year: i32) -> NaiveDate {
1294 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 fn approximate_chuseok(year: i32) -> NaiveDate {
1321 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#[derive(Debug, Clone, Serialize, Deserialize)]
1348pub struct CustomHolidayConfig {
1349 pub name: String,
1351 pub month: u8,
1353 pub day: u8,
1355 #[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 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 let christmas = NaiveDate::from_ymd_opt(2024, 12, 25).unwrap();
1387 assert!(cal.is_holiday(christmas));
1388
1389 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 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 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 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 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 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 let christmas = NaiveDate::from_ymd_opt(2024, 12, 25).unwrap();
1440 assert!(cal.get_multiplier(christmas) < 0.1);
1441
1442 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 let independence = NaiveDate::from_ymd_opt(2024, 9, 7).unwrap();
1479 assert!(cal.is_holiday(independence));
1480
1481 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 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 let anzac = NaiveDate::from_ymd_opt(2024, 4, 25).unwrap();
1501 assert!(cal.is_holiday(anzac));
1502
1503 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 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 let liberation = NaiveDate::from_ymd_opt(2024, 8, 15).unwrap();
1523 assert!(cal.is_holiday(liberation));
1524
1525 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 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 let kodomo = NaiveDate::from_ymd_opt(2024, 5, 5).unwrap();
1545 assert!(cal.is_holiday(kodomo));
1546 }
1547
1548 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 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 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}