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 fn us_holidays(year: i32) -> Self {
155 let mut cal = Self::new(Region::US, year);
156
157 let new_years = NaiveDate::from_ymd_opt(year, 1, 1).unwrap();
159 cal.add_holiday(Holiday::new(
160 "New Year's Day",
161 Self::observe_weekend(new_years),
162 0.02,
163 ));
164
165 let mlk = Self::nth_weekday_of_month(year, 1, Weekday::Mon, 3);
167 cal.add_holiday(Holiday::new("Martin Luther King Jr. Day", mlk, 0.1));
168
169 let presidents = Self::nth_weekday_of_month(year, 2, Weekday::Mon, 3);
171 cal.add_holiday(Holiday::new("Presidents' Day", presidents, 0.1));
172
173 let memorial = Self::last_weekday_of_month(year, 5, Weekday::Mon);
175 cal.add_holiday(Holiday::new("Memorial Day", memorial, 0.05));
176
177 let juneteenth = NaiveDate::from_ymd_opt(year, 6, 19).unwrap();
179 cal.add_holiday(Holiday::new(
180 "Juneteenth",
181 Self::observe_weekend(juneteenth),
182 0.1,
183 ));
184
185 let independence = NaiveDate::from_ymd_opt(year, 7, 4).unwrap();
187 cal.add_holiday(Holiday::new(
188 "Independence Day",
189 Self::observe_weekend(independence),
190 0.02,
191 ));
192
193 let labor = Self::nth_weekday_of_month(year, 9, Weekday::Mon, 1);
195 cal.add_holiday(Holiday::new("Labor Day", labor, 0.05));
196
197 let columbus = Self::nth_weekday_of_month(year, 10, Weekday::Mon, 2);
199 cal.add_holiday(Holiday::new("Columbus Day", columbus, 0.2));
200
201 let veterans = NaiveDate::from_ymd_opt(year, 11, 11).unwrap();
203 cal.add_holiday(Holiday::new(
204 "Veterans Day",
205 Self::observe_weekend(veterans),
206 0.1,
207 ));
208
209 let thanksgiving = Self::nth_weekday_of_month(year, 11, Weekday::Thu, 4);
211 cal.add_holiday(Holiday::new("Thanksgiving", thanksgiving, 0.02));
212
213 cal.add_holiday(Holiday::new(
215 "Day after Thanksgiving",
216 thanksgiving + Duration::days(1),
217 0.1,
218 ));
219
220 let christmas_eve = NaiveDate::from_ymd_opt(year, 12, 24).unwrap();
222 cal.add_holiday(Holiday::new("Christmas Eve", christmas_eve, 0.1));
223
224 let christmas = NaiveDate::from_ymd_opt(year, 12, 25).unwrap();
226 cal.add_holiday(Holiday::new(
227 "Christmas Day",
228 Self::observe_weekend(christmas),
229 0.02,
230 ));
231
232 let new_years_eve = NaiveDate::from_ymd_opt(year, 12, 31).unwrap();
234 cal.add_holiday(Holiday::new("New Year's Eve", new_years_eve, 0.1));
235
236 cal
237 }
238
239 fn de_holidays(year: i32) -> Self {
241 let mut cal = Self::new(Region::DE, year);
242
243 cal.add_holiday(Holiday::new(
245 "Neujahr",
246 NaiveDate::from_ymd_opt(year, 1, 1).unwrap(),
247 0.02,
248 ));
249
250 let easter = Self::easter_date(year);
252 cal.add_holiday(Holiday::new("Karfreitag", easter - Duration::days(2), 0.02));
253
254 cal.add_holiday(Holiday::new(
256 "Ostermontag",
257 easter + Duration::days(1),
258 0.02,
259 ));
260
261 cal.add_holiday(Holiday::new(
263 "Tag der Arbeit",
264 NaiveDate::from_ymd_opt(year, 5, 1).unwrap(),
265 0.02,
266 ));
267
268 cal.add_holiday(Holiday::new(
270 "Christi Himmelfahrt",
271 easter + Duration::days(39),
272 0.02,
273 ));
274
275 cal.add_holiday(Holiday::new(
277 "Pfingstmontag",
278 easter + Duration::days(50),
279 0.02,
280 ));
281
282 cal.add_holiday(Holiday::new(
284 "Tag der Deutschen Einheit",
285 NaiveDate::from_ymd_opt(year, 10, 3).unwrap(),
286 0.02,
287 ));
288
289 cal.add_holiday(Holiday::new(
291 "1. Weihnachtstag",
292 NaiveDate::from_ymd_opt(year, 12, 25).unwrap(),
293 0.02,
294 ));
295 cal.add_holiday(Holiday::new(
296 "2. Weihnachtstag",
297 NaiveDate::from_ymd_opt(year, 12, 26).unwrap(),
298 0.02,
299 ));
300
301 cal.add_holiday(Holiday::new(
303 "Silvester",
304 NaiveDate::from_ymd_opt(year, 12, 31).unwrap(),
305 0.1,
306 ));
307
308 cal
309 }
310
311 fn gb_holidays(year: i32) -> Self {
313 let mut cal = Self::new(Region::GB, year);
314
315 let new_years = NaiveDate::from_ymd_opt(year, 1, 1).unwrap();
317 cal.add_holiday(Holiday::new(
318 "New Year's Day",
319 Self::observe_weekend(new_years),
320 0.02,
321 ));
322
323 let easter = Self::easter_date(year);
325 cal.add_holiday(Holiday::new(
326 "Good Friday",
327 easter - Duration::days(2),
328 0.02,
329 ));
330
331 cal.add_holiday(Holiday::new(
333 "Easter Monday",
334 easter + Duration::days(1),
335 0.02,
336 ));
337
338 let early_may = Self::nth_weekday_of_month(year, 5, Weekday::Mon, 1);
340 cal.add_holiday(Holiday::new("Early May Bank Holiday", early_may, 0.02));
341
342 let spring = Self::last_weekday_of_month(year, 5, Weekday::Mon);
344 cal.add_holiday(Holiday::new("Spring Bank Holiday", spring, 0.02));
345
346 let summer = Self::last_weekday_of_month(year, 8, Weekday::Mon);
348 cal.add_holiday(Holiday::new("Summer Bank Holiday", summer, 0.02));
349
350 let christmas = NaiveDate::from_ymd_opt(year, 12, 25).unwrap();
352 cal.add_holiday(Holiday::new(
353 "Christmas Day",
354 Self::observe_weekend(christmas),
355 0.02,
356 ));
357
358 let boxing = NaiveDate::from_ymd_opt(year, 12, 26).unwrap();
360 cal.add_holiday(Holiday::new(
361 "Boxing Day",
362 Self::observe_weekend(boxing),
363 0.02,
364 ));
365
366 cal
367 }
368
369 fn cn_holidays(year: i32) -> Self {
371 let mut cal = Self::new(Region::CN, year);
372
373 cal.add_holiday(Holiday::new(
375 "New Year",
376 NaiveDate::from_ymd_opt(year, 1, 1).unwrap(),
377 0.05,
378 ));
379
380 let cny = Self::approximate_chinese_new_year(year);
383 for i in 0..7 {
384 cal.add_holiday(Holiday::new(
385 if i == 0 {
386 "Spring Festival"
387 } else {
388 "Spring Festival Holiday"
389 },
390 cny + Duration::days(i),
391 0.02,
392 ));
393 }
394
395 cal.add_holiday(Holiday::new(
397 "Qingming Festival",
398 NaiveDate::from_ymd_opt(year, 4, 5).unwrap(),
399 0.05,
400 ));
401
402 for i in 0..3 {
404 cal.add_holiday(Holiday::new(
405 if i == 0 {
406 "Labor Day"
407 } else {
408 "Labor Day Holiday"
409 },
410 NaiveDate::from_ymd_opt(year, 5, 1).unwrap() + Duration::days(i),
411 0.05,
412 ));
413 }
414
415 cal.add_holiday(Holiday::new(
417 "Dragon Boat Festival",
418 NaiveDate::from_ymd_opt(year, 6, 10).unwrap(),
419 0.05,
420 ));
421
422 cal.add_holiday(Holiday::new(
424 "Mid-Autumn Festival",
425 NaiveDate::from_ymd_opt(year, 9, 15).unwrap(),
426 0.05,
427 ));
428
429 for i in 0..7 {
431 cal.add_holiday(Holiday::new(
432 if i == 0 {
433 "National Day"
434 } else {
435 "National Day Holiday"
436 },
437 NaiveDate::from_ymd_opt(year, 10, 1).unwrap() + Duration::days(i),
438 0.02,
439 ));
440 }
441
442 cal
443 }
444
445 fn jp_holidays(year: i32) -> Self {
447 let mut cal = Self::new(Region::JP, year);
448
449 cal.add_holiday(Holiday::new(
451 "Ganjitsu (New Year)",
452 NaiveDate::from_ymd_opt(year, 1, 1).unwrap(),
453 0.02,
454 ));
455
456 cal.add_holiday(Holiday::new(
458 "New Year Holiday",
459 NaiveDate::from_ymd_opt(year, 1, 2).unwrap(),
460 0.05,
461 ));
462 cal.add_holiday(Holiday::new(
463 "New Year Holiday",
464 NaiveDate::from_ymd_opt(year, 1, 3).unwrap(),
465 0.05,
466 ));
467
468 let seijin = Self::nth_weekday_of_month(year, 1, Weekday::Mon, 2);
470 cal.add_holiday(Holiday::new("Seijin no Hi", seijin, 0.05));
471
472 cal.add_holiday(Holiday::new(
474 "Kenkoku Kinen no Hi",
475 NaiveDate::from_ymd_opt(year, 2, 11).unwrap(),
476 0.02,
477 ));
478
479 cal.add_holiday(Holiday::new(
481 "Tenno Tanjobi",
482 NaiveDate::from_ymd_opt(year, 2, 23).unwrap(),
483 0.02,
484 ));
485
486 cal.add_holiday(Holiday::new(
488 "Shunbun no Hi",
489 NaiveDate::from_ymd_opt(year, 3, 20).unwrap(),
490 0.02,
491 ));
492
493 cal.add_holiday(Holiday::new(
495 "Showa no Hi",
496 NaiveDate::from_ymd_opt(year, 4, 29).unwrap(),
497 0.02,
498 ));
499
500 cal.add_holiday(Holiday::new(
502 "Kenpo Kinenbi",
503 NaiveDate::from_ymd_opt(year, 5, 3).unwrap(),
504 0.02,
505 ));
506 cal.add_holiday(Holiday::new(
507 "Midori no Hi",
508 NaiveDate::from_ymd_opt(year, 5, 4).unwrap(),
509 0.02,
510 ));
511 cal.add_holiday(Holiday::new(
512 "Kodomo no Hi",
513 NaiveDate::from_ymd_opt(year, 5, 5).unwrap(),
514 0.02,
515 ));
516
517 let umi = Self::nth_weekday_of_month(year, 7, Weekday::Mon, 3);
519 cal.add_holiday(Holiday::new("Umi no Hi", umi, 0.05));
520
521 cal.add_holiday(Holiday::new(
523 "Yama no Hi",
524 NaiveDate::from_ymd_opt(year, 8, 11).unwrap(),
525 0.05,
526 ));
527
528 let keiro = Self::nth_weekday_of_month(year, 9, Weekday::Mon, 3);
530 cal.add_holiday(Holiday::new("Keiro no Hi", keiro, 0.05));
531
532 cal.add_holiday(Holiday::new(
534 "Shubun no Hi",
535 NaiveDate::from_ymd_opt(year, 9, 23).unwrap(),
536 0.02,
537 ));
538
539 let sports = Self::nth_weekday_of_month(year, 10, Weekday::Mon, 2);
541 cal.add_holiday(Holiday::new("Sports Day", sports, 0.05));
542
543 cal.add_holiday(Holiday::new(
545 "Bunka no Hi",
546 NaiveDate::from_ymd_opt(year, 11, 3).unwrap(),
547 0.02,
548 ));
549
550 cal.add_holiday(Holiday::new(
552 "Kinro Kansha no Hi",
553 NaiveDate::from_ymd_opt(year, 11, 23).unwrap(),
554 0.02,
555 ));
556
557 cal
558 }
559
560 fn in_holidays(year: i32) -> Self {
562 let mut cal = Self::new(Region::IN, year);
563
564 cal.add_holiday(Holiday::new(
566 "Republic Day",
567 NaiveDate::from_ymd_opt(year, 1, 26).unwrap(),
568 0.02,
569 ));
570
571 cal.add_holiday(Holiday::new(
573 "Holi",
574 NaiveDate::from_ymd_opt(year, 3, 10).unwrap(),
575 0.05,
576 ));
577
578 let easter = Self::easter_date(year);
580 cal.add_holiday(Holiday::new(
581 "Good Friday",
582 easter - Duration::days(2),
583 0.05,
584 ));
585
586 cal.add_holiday(Holiday::new(
588 "Independence Day",
589 NaiveDate::from_ymd_opt(year, 8, 15).unwrap(),
590 0.02,
591 ));
592
593 cal.add_holiday(Holiday::new(
595 "Gandhi Jayanti",
596 NaiveDate::from_ymd_opt(year, 10, 2).unwrap(),
597 0.02,
598 ));
599
600 cal.add_holiday(Holiday::new(
602 "Dussehra",
603 NaiveDate::from_ymd_opt(year, 10, 15).unwrap(),
604 0.05,
605 ));
606
607 let diwali = Self::approximate_diwali(year);
609 for i in 0..5 {
610 cal.add_holiday(Holiday::new(
611 match i {
612 0 => "Dhanteras",
613 1 => "Naraka Chaturdashi",
614 2 => "Diwali",
615 3 => "Govardhan Puja",
616 _ => "Bhai Dooj",
617 },
618 diwali + Duration::days(i),
619 if i == 2 { 0.02 } else { 0.1 },
620 ));
621 }
622
623 cal.add_holiday(Holiday::new(
625 "Christmas",
626 NaiveDate::from_ymd_opt(year, 12, 25).unwrap(),
627 0.1,
628 ));
629
630 cal
631 }
632
633 fn br_holidays(year: i32) -> Self {
635 let mut cal = Self::new(Region::BR, year);
636
637 cal.add_holiday(Holiday::new(
639 "Confraternização Universal",
640 NaiveDate::from_ymd_opt(year, 1, 1).unwrap(),
641 0.02,
642 ));
643
644 let easter = Self::easter_date(year);
646 let carnival_tuesday = easter - Duration::days(47);
647 let carnival_monday = carnival_tuesday - Duration::days(1);
648 cal.add_holiday(Holiday::new("Carnaval (Segunda)", carnival_monday, 0.02));
649 cal.add_holiday(Holiday::new("Carnaval (Terça)", carnival_tuesday, 0.02));
650
651 cal.add_holiday(Holiday::new(
653 "Sexta-feira Santa",
654 easter - Duration::days(2),
655 0.02,
656 ));
657
658 cal.add_holiday(Holiday::new(
660 "Tiradentes",
661 NaiveDate::from_ymd_opt(year, 4, 21).unwrap(),
662 0.02,
663 ));
664
665 cal.add_holiday(Holiday::new(
667 "Dia do Trabalho",
668 NaiveDate::from_ymd_opt(year, 5, 1).unwrap(),
669 0.02,
670 ));
671
672 cal.add_holiday(Holiday::new(
674 "Corpus Christi",
675 easter + Duration::days(60),
676 0.05,
677 ));
678
679 cal.add_holiday(Holiday::new(
681 "IndependĂȘncia do Brasil",
682 NaiveDate::from_ymd_opt(year, 9, 7).unwrap(),
683 0.02,
684 ));
685
686 cal.add_holiday(Holiday::new(
688 "Nossa Senhora Aparecida",
689 NaiveDate::from_ymd_opt(year, 10, 12).unwrap(),
690 0.02,
691 ));
692
693 cal.add_holiday(Holiday::new(
695 "Finados",
696 NaiveDate::from_ymd_opt(year, 11, 2).unwrap(),
697 0.02,
698 ));
699
700 cal.add_holiday(Holiday::new(
702 "Proclamação da RepĂșblica",
703 NaiveDate::from_ymd_opt(year, 11, 15).unwrap(),
704 0.02,
705 ));
706
707 cal.add_holiday(Holiday::new(
709 "Natal",
710 NaiveDate::from_ymd_opt(year, 12, 25).unwrap(),
711 0.02,
712 ));
713
714 cal
715 }
716
717 fn mx_holidays(year: i32) -> Self {
719 let mut cal = Self::new(Region::MX, year);
720
721 cal.add_holiday(Holiday::new(
723 "Año Nuevo",
724 NaiveDate::from_ymd_opt(year, 1, 1).unwrap(),
725 0.02,
726 ));
727
728 let constitution = Self::nth_weekday_of_month(year, 2, Weekday::Mon, 1);
730 cal.add_holiday(Holiday::new("DĂa de la ConstituciĂłn", constitution, 0.02));
731
732 let juarez = Self::nth_weekday_of_month(year, 3, Weekday::Mon, 3);
734 cal.add_holiday(Holiday::new("Natalicio de Benito JuĂĄrez", juarez, 0.02));
735
736 let easter = Self::easter_date(year);
738 cal.add_holiday(Holiday::new(
739 "Jueves Santo",
740 easter - Duration::days(3),
741 0.05,
742 ));
743 cal.add_holiday(Holiday::new(
744 "Viernes Santo",
745 easter - Duration::days(2),
746 0.02,
747 ));
748
749 cal.add_holiday(Holiday::new(
751 "DĂa del Trabajo",
752 NaiveDate::from_ymd_opt(year, 5, 1).unwrap(),
753 0.02,
754 ));
755
756 cal.add_holiday(Holiday::new(
758 "DĂa de la Independencia",
759 NaiveDate::from_ymd_opt(year, 9, 16).unwrap(),
760 0.02,
761 ));
762
763 let revolution = Self::nth_weekday_of_month(year, 11, Weekday::Mon, 3);
765 cal.add_holiday(Holiday::new("DĂa de la RevoluciĂłn", revolution, 0.02));
766
767 cal.add_holiday(Holiday::new(
769 "DĂa de Muertos",
770 NaiveDate::from_ymd_opt(year, 11, 1).unwrap(),
771 0.1,
772 ));
773 cal.add_holiday(Holiday::new(
774 "DĂa de Muertos",
775 NaiveDate::from_ymd_opt(year, 11, 2).unwrap(),
776 0.1,
777 ));
778
779 cal.add_holiday(Holiday::new(
781 "Navidad",
782 NaiveDate::from_ymd_opt(year, 12, 25).unwrap(),
783 0.02,
784 ));
785
786 cal
787 }
788
789 fn au_holidays(year: i32) -> Self {
791 let mut cal = Self::new(Region::AU, year);
792
793 let new_years = NaiveDate::from_ymd_opt(year, 1, 1).unwrap();
795 cal.add_holiday(Holiday::new(
796 "New Year's Day",
797 Self::observe_weekend(new_years),
798 0.02,
799 ));
800
801 let australia_day = NaiveDate::from_ymd_opt(year, 1, 26).unwrap();
803 cal.add_holiday(Holiday::new(
804 "Australia Day",
805 Self::observe_weekend(australia_day),
806 0.02,
807 ));
808
809 let easter = Self::easter_date(year);
811 cal.add_holiday(Holiday::new(
812 "Good Friday",
813 easter - Duration::days(2),
814 0.02,
815 ));
816
817 cal.add_holiday(Holiday::new(
819 "Easter Saturday",
820 easter - Duration::days(1),
821 0.02,
822 ));
823
824 cal.add_holiday(Holiday::new(
826 "Easter Monday",
827 easter + Duration::days(1),
828 0.02,
829 ));
830
831 let anzac = NaiveDate::from_ymd_opt(year, 4, 25).unwrap();
833 cal.add_holiday(Holiday::new("ANZAC Day", anzac, 0.02));
834
835 let queens_birthday = Self::nth_weekday_of_month(year, 6, Weekday::Mon, 2);
837 cal.add_holiday(Holiday::new("Queen's Birthday", queens_birthday, 0.02));
838
839 let christmas = NaiveDate::from_ymd_opt(year, 12, 25).unwrap();
841 cal.add_holiday(Holiday::new(
842 "Christmas Day",
843 Self::observe_weekend(christmas),
844 0.02,
845 ));
846
847 let boxing = NaiveDate::from_ymd_opt(year, 12, 26).unwrap();
849 cal.add_holiday(Holiday::new(
850 "Boxing Day",
851 Self::observe_weekend(boxing),
852 0.02,
853 ));
854
855 cal
856 }
857
858 fn sg_holidays(year: i32) -> Self {
860 let mut cal = Self::new(Region::SG, year);
861
862 cal.add_holiday(Holiday::new(
864 "New Year's Day",
865 NaiveDate::from_ymd_opt(year, 1, 1).unwrap(),
866 0.02,
867 ));
868
869 let cny = Self::approximate_chinese_new_year(year);
871 cal.add_holiday(Holiday::new("Chinese New Year", cny, 0.02));
872 cal.add_holiday(Holiday::new(
873 "Chinese New Year (Day 2)",
874 cny + Duration::days(1),
875 0.02,
876 ));
877
878 let easter = Self::easter_date(year);
880 cal.add_holiday(Holiday::new(
881 "Good Friday",
882 easter - Duration::days(2),
883 0.02,
884 ));
885
886 cal.add_holiday(Holiday::new(
888 "Labour Day",
889 NaiveDate::from_ymd_opt(year, 5, 1).unwrap(),
890 0.02,
891 ));
892
893 let vesak = Self::approximate_vesak(year);
895 cal.add_holiday(Holiday::new("Vesak Day", vesak, 0.02));
896
897 let hari_raya_puasa = Self::approximate_hari_raya_puasa(year);
899 cal.add_holiday(Holiday::new("Hari Raya Puasa", hari_raya_puasa, 0.02));
900
901 let hari_raya_haji = Self::approximate_hari_raya_haji(year);
903 cal.add_holiday(Holiday::new("Hari Raya Haji", hari_raya_haji, 0.02));
904
905 cal.add_holiday(Holiday::new(
907 "National Day",
908 NaiveDate::from_ymd_opt(year, 8, 9).unwrap(),
909 0.02,
910 ));
911
912 let deepavali = Self::approximate_deepavali(year);
914 cal.add_holiday(Holiday::new("Deepavali", deepavali, 0.02));
915
916 cal.add_holiday(Holiday::new(
918 "Christmas Day",
919 NaiveDate::from_ymd_opt(year, 12, 25).unwrap(),
920 0.02,
921 ));
922
923 cal
924 }
925
926 fn kr_holidays(year: i32) -> Self {
928 let mut cal = Self::new(Region::KR, year);
929
930 cal.add_holiday(Holiday::new(
932 "Sinjeong",
933 NaiveDate::from_ymd_opt(year, 1, 1).unwrap(),
934 0.02,
935 ));
936
937 let seollal = Self::approximate_korean_new_year(year);
939 cal.add_holiday(Holiday::new(
940 "Seollal (Eve)",
941 seollal - Duration::days(1),
942 0.02,
943 ));
944 cal.add_holiday(Holiday::new("Seollal", seollal, 0.02));
945 cal.add_holiday(Holiday::new(
946 "Seollal (Day 2)",
947 seollal + Duration::days(1),
948 0.02,
949 ));
950
951 cal.add_holiday(Holiday::new(
953 "Samiljeol",
954 NaiveDate::from_ymd_opt(year, 3, 1).unwrap(),
955 0.02,
956 ));
957
958 cal.add_holiday(Holiday::new(
960 "Eorininal",
961 NaiveDate::from_ymd_opt(year, 5, 5).unwrap(),
962 0.02,
963 ));
964
965 let buddha_birthday = Self::approximate_korean_buddha_birthday(year);
967 cal.add_holiday(Holiday::new("Seokgatansinil", buddha_birthday, 0.02));
968
969 cal.add_holiday(Holiday::new(
971 "Hyeonchungil",
972 NaiveDate::from_ymd_opt(year, 6, 6).unwrap(),
973 0.02,
974 ));
975
976 cal.add_holiday(Holiday::new(
978 "Gwangbokjeol",
979 NaiveDate::from_ymd_opt(year, 8, 15).unwrap(),
980 0.02,
981 ));
982
983 let chuseok = Self::approximate_chuseok(year);
985 cal.add_holiday(Holiday::new(
986 "Chuseok (Eve)",
987 chuseok - Duration::days(1),
988 0.02,
989 ));
990 cal.add_holiday(Holiday::new("Chuseok", chuseok, 0.02));
991 cal.add_holiday(Holiday::new(
992 "Chuseok (Day 2)",
993 chuseok + Duration::days(1),
994 0.02,
995 ));
996
997 cal.add_holiday(Holiday::new(
999 "Gaecheonjeol",
1000 NaiveDate::from_ymd_opt(year, 10, 3).unwrap(),
1001 0.02,
1002 ));
1003
1004 cal.add_holiday(Holiday::new(
1006 "Hangullal",
1007 NaiveDate::from_ymd_opt(year, 10, 9).unwrap(),
1008 0.02,
1009 ));
1010
1011 cal.add_holiday(Holiday::new(
1013 "Seongtanjeol",
1014 NaiveDate::from_ymd_opt(year, 12, 25).unwrap(),
1015 0.02,
1016 ));
1017
1018 cal
1019 }
1020
1021 fn easter_date(year: i32) -> NaiveDate {
1023 let a = year % 19;
1024 let b = year / 100;
1025 let c = year % 100;
1026 let d = b / 4;
1027 let e = b % 4;
1028 let f = (b + 8) / 25;
1029 let g = (b - f + 1) / 3;
1030 let h = (19 * a + b - d - g + 15) % 30;
1031 let i = c / 4;
1032 let k = c % 4;
1033 let l = (32 + 2 * e + 2 * i - h - k) % 7;
1034 let m = (a + 11 * h + 22 * l) / 451;
1035 let month = (h + l - 7 * m + 114) / 31;
1036 let day = ((h + l - 7 * m + 114) % 31) + 1;
1037
1038 NaiveDate::from_ymd_opt(year, month as u32, day as u32).unwrap()
1039 }
1040
1041 fn nth_weekday_of_month(year: i32, month: u32, weekday: Weekday, n: u32) -> NaiveDate {
1043 let first = NaiveDate::from_ymd_opt(year, month, 1).unwrap();
1044 let first_weekday = first.weekday();
1045
1046 let days_until = (weekday.num_days_from_monday() as i64
1047 - first_weekday.num_days_from_monday() as i64
1048 + 7)
1049 % 7;
1050
1051 first + Duration::days(days_until + (n - 1) as i64 * 7)
1052 }
1053
1054 fn last_weekday_of_month(year: i32, month: u32, weekday: Weekday) -> NaiveDate {
1056 let last = if month == 12 {
1057 NaiveDate::from_ymd_opt(year + 1, 1, 1).unwrap() - Duration::days(1)
1058 } else {
1059 NaiveDate::from_ymd_opt(year, month + 1, 1).unwrap() - Duration::days(1)
1060 };
1061
1062 let last_weekday = last.weekday();
1063 let days_back = (last_weekday.num_days_from_monday() as i64
1064 - weekday.num_days_from_monday() as i64
1065 + 7)
1066 % 7;
1067
1068 last - Duration::days(days_back)
1069 }
1070
1071 fn observe_weekend(date: NaiveDate) -> NaiveDate {
1073 match date.weekday() {
1074 Weekday::Sat => date - Duration::days(1), Weekday::Sun => date + Duration::days(1), _ => date,
1077 }
1078 }
1079
1080 fn approximate_chinese_new_year(year: i32) -> NaiveDate {
1082 let base_year = 2000;
1085 let cny_2000 = NaiveDate::from_ymd_opt(2000, 2, 5).unwrap();
1086
1087 let years_diff = year - base_year;
1088 let lunar_cycle = 29.5306; let days_offset = (years_diff as f64 * 12.0 * lunar_cycle) % 365.25;
1090
1091 let mut result = cny_2000 + Duration::days(days_offset as i64);
1092
1093 while result.month() > 2 || (result.month() == 2 && result.day() > 20) {
1095 result -= Duration::days(29);
1096 }
1097 while result.month() < 1 || (result.month() == 1 && result.day() < 21) {
1098 result += Duration::days(29);
1099 }
1100
1101 if result.year() != year {
1103 result = NaiveDate::from_ymd_opt(year, result.month(), result.day().min(28))
1104 .unwrap_or_else(|| NaiveDate::from_ymd_opt(year, result.month(), 28).unwrap());
1105 }
1106
1107 result
1108 }
1109
1110 fn approximate_diwali(year: i32) -> NaiveDate {
1112 match year % 4 {
1115 0 => NaiveDate::from_ymd_opt(year, 11, 1).unwrap(),
1116 1 => NaiveDate::from_ymd_opt(year, 10, 24).unwrap(),
1117 2 => NaiveDate::from_ymd_opt(year, 11, 12).unwrap(),
1118 _ => NaiveDate::from_ymd_opt(year, 11, 4).unwrap(),
1119 }
1120 }
1121
1122 fn approximate_vesak(year: i32) -> NaiveDate {
1125 let base = match year % 19 {
1128 0 => 18,
1129 1 => 7,
1130 2 => 26,
1131 3 => 15,
1132 4 => 5,
1133 5 => 24,
1134 6 => 13,
1135 7 => 2,
1136 8 => 22,
1137 9 => 11,
1138 10 => 30,
1139 11 => 19,
1140 12 => 8,
1141 13 => 27,
1142 14 => 17,
1143 15 => 6,
1144 16 => 25,
1145 17 => 14,
1146 _ => 3,
1147 };
1148 let month = if base > 20 { 4 } else { 5 };
1149 let day = if base > 20 { base - 10 } else { base };
1150 NaiveDate::from_ymd_opt(year, month, day.clamp(1, 28) as u32).unwrap()
1151 }
1152
1153 fn approximate_hari_raya_puasa(year: i32) -> NaiveDate {
1156 let base_year = 2024;
1159 let base_date = NaiveDate::from_ymd_opt(2024, 4, 10).unwrap();
1160 let years_diff = year - base_year;
1161 let days_shift = (years_diff as f64 * -10.63) as i64;
1162 let mut result = base_date + Duration::days(days_shift);
1163
1164 while result.year() != year {
1166 if result.year() > year {
1167 result -= Duration::days(354); } else {
1169 result += Duration::days(354);
1170 }
1171 }
1172 result
1173 }
1174
1175 fn approximate_hari_raya_haji(year: i32) -> NaiveDate {
1178 Self::approximate_hari_raya_puasa(year) + Duration::days(70)
1179 }
1180
1181 fn approximate_deepavali(year: i32) -> NaiveDate {
1183 Self::approximate_diwali(year)
1184 }
1185
1186 fn approximate_korean_new_year(year: i32) -> NaiveDate {
1189 Self::approximate_chinese_new_year(year)
1190 }
1191
1192 fn approximate_korean_buddha_birthday(year: i32) -> NaiveDate {
1195 match year % 19 {
1197 0 => NaiveDate::from_ymd_opt(year, 5, 15).unwrap(),
1198 1 => NaiveDate::from_ymd_opt(year, 5, 4).unwrap(),
1199 2 => NaiveDate::from_ymd_opt(year, 5, 23).unwrap(),
1200 3 => NaiveDate::from_ymd_opt(year, 5, 12).unwrap(),
1201 4 => NaiveDate::from_ymd_opt(year, 5, 1).unwrap(),
1202 5 => NaiveDate::from_ymd_opt(year, 5, 20).unwrap(),
1203 6 => NaiveDate::from_ymd_opt(year, 5, 10).unwrap(),
1204 7 => NaiveDate::from_ymd_opt(year, 4, 29).unwrap(),
1205 8 => NaiveDate::from_ymd_opt(year, 5, 18).unwrap(),
1206 9 => NaiveDate::from_ymd_opt(year, 5, 7).unwrap(),
1207 10 => NaiveDate::from_ymd_opt(year, 5, 26).unwrap(),
1208 11 => NaiveDate::from_ymd_opt(year, 5, 15).unwrap(),
1209 12 => NaiveDate::from_ymd_opt(year, 5, 4).unwrap(),
1210 13 => NaiveDate::from_ymd_opt(year, 5, 24).unwrap(),
1211 14 => NaiveDate::from_ymd_opt(year, 5, 13).unwrap(),
1212 15 => NaiveDate::from_ymd_opt(year, 5, 2).unwrap(),
1213 16 => NaiveDate::from_ymd_opt(year, 5, 21).unwrap(),
1214 17 => NaiveDate::from_ymd_opt(year, 5, 10).unwrap(),
1215 _ => NaiveDate::from_ymd_opt(year, 4, 30).unwrap(),
1216 }
1217 }
1218
1219 fn approximate_chuseok(year: i32) -> NaiveDate {
1222 match year % 19 {
1224 0 => NaiveDate::from_ymd_opt(year, 9, 17).unwrap(),
1225 1 => NaiveDate::from_ymd_opt(year, 10, 6).unwrap(),
1226 2 => NaiveDate::from_ymd_opt(year, 9, 25).unwrap(),
1227 3 => NaiveDate::from_ymd_opt(year, 9, 14).unwrap(),
1228 4 => NaiveDate::from_ymd_opt(year, 10, 3).unwrap(),
1229 5 => NaiveDate::from_ymd_opt(year, 9, 22).unwrap(),
1230 6 => NaiveDate::from_ymd_opt(year, 9, 11).unwrap(),
1231 7 => NaiveDate::from_ymd_opt(year, 9, 30).unwrap(),
1232 8 => NaiveDate::from_ymd_opt(year, 9, 19).unwrap(),
1233 9 => NaiveDate::from_ymd_opt(year, 10, 9).unwrap(),
1234 10 => NaiveDate::from_ymd_opt(year, 9, 28).unwrap(),
1235 11 => NaiveDate::from_ymd_opt(year, 9, 17).unwrap(),
1236 12 => NaiveDate::from_ymd_opt(year, 10, 6).unwrap(),
1237 13 => NaiveDate::from_ymd_opt(year, 9, 25).unwrap(),
1238 14 => NaiveDate::from_ymd_opt(year, 9, 14).unwrap(),
1239 15 => NaiveDate::from_ymd_opt(year, 10, 4).unwrap(),
1240 16 => NaiveDate::from_ymd_opt(year, 9, 22).unwrap(),
1241 17 => NaiveDate::from_ymd_opt(year, 9, 12).unwrap(),
1242 _ => NaiveDate::from_ymd_opt(year, 10, 1).unwrap(),
1243 }
1244 }
1245}
1246
1247#[derive(Debug, Clone, Serialize, Deserialize)]
1249pub struct CustomHolidayConfig {
1250 pub name: String,
1252 pub month: u8,
1254 pub day: u8,
1256 #[serde(default = "default_holiday_multiplier")]
1258 pub activity_multiplier: f64,
1259}
1260
1261fn default_holiday_multiplier() -> f64 {
1262 0.05
1263}
1264
1265impl CustomHolidayConfig {
1266 pub fn to_holiday(&self, year: i32) -> Holiday {
1268 Holiday::new(
1269 &self.name,
1270 NaiveDate::from_ymd_opt(year, self.month as u32, self.day as u32).unwrap(),
1271 self.activity_multiplier,
1272 )
1273 }
1274}
1275
1276#[cfg(test)]
1277mod tests {
1278 use super::*;
1279
1280 #[test]
1281 fn test_us_holidays() {
1282 let cal = HolidayCalendar::for_region(Region::US, 2024);
1283
1284 let christmas = NaiveDate::from_ymd_opt(2024, 12, 25).unwrap();
1286 assert!(cal.is_holiday(christmas));
1287
1288 let independence = NaiveDate::from_ymd_opt(2024, 7, 4).unwrap();
1290 assert!(cal.is_holiday(independence));
1291 }
1292
1293 #[test]
1294 fn test_german_holidays() {
1295 let cal = HolidayCalendar::for_region(Region::DE, 2024);
1296
1297 let unity = NaiveDate::from_ymd_opt(2024, 10, 3).unwrap();
1299 assert!(cal.is_holiday(unity));
1300 }
1301
1302 #[test]
1303 fn test_easter_calculation() {
1304 assert_eq!(
1306 HolidayCalendar::easter_date(2024),
1307 NaiveDate::from_ymd_opt(2024, 3, 31).unwrap()
1308 );
1309 assert_eq!(
1310 HolidayCalendar::easter_date(2025),
1311 NaiveDate::from_ymd_opt(2025, 4, 20).unwrap()
1312 );
1313 }
1314
1315 #[test]
1316 fn test_nth_weekday() {
1317 let mlk = HolidayCalendar::nth_weekday_of_month(2024, 1, Weekday::Mon, 3);
1319 assert_eq!(mlk, NaiveDate::from_ymd_opt(2024, 1, 15).unwrap());
1320
1321 let thanksgiving = HolidayCalendar::nth_weekday_of_month(2024, 11, Weekday::Thu, 4);
1323 assert_eq!(thanksgiving, NaiveDate::from_ymd_opt(2024, 11, 28).unwrap());
1324 }
1325
1326 #[test]
1327 fn test_last_weekday() {
1328 let memorial = HolidayCalendar::last_weekday_of_month(2024, 5, Weekday::Mon);
1330 assert_eq!(memorial, NaiveDate::from_ymd_opt(2024, 5, 27).unwrap());
1331 }
1332
1333 #[test]
1334 fn test_activity_multiplier() {
1335 let cal = HolidayCalendar::for_region(Region::US, 2024);
1336
1337 let christmas = NaiveDate::from_ymd_opt(2024, 12, 25).unwrap();
1339 assert!(cal.get_multiplier(christmas) < 0.1);
1340
1341 let regular = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
1343 assert!((cal.get_multiplier(regular) - 1.0).abs() < 0.01);
1344 }
1345
1346 #[test]
1347 fn test_all_regions_have_holidays() {
1348 let regions = [
1349 Region::US,
1350 Region::DE,
1351 Region::GB,
1352 Region::CN,
1353 Region::JP,
1354 Region::IN,
1355 Region::BR,
1356 Region::MX,
1357 Region::AU,
1358 Region::SG,
1359 Region::KR,
1360 ];
1361
1362 for region in regions {
1363 let cal = HolidayCalendar::for_region(region, 2024);
1364 assert!(
1365 !cal.holidays.is_empty(),
1366 "Region {:?} should have holidays",
1367 region
1368 );
1369 }
1370 }
1371
1372 #[test]
1373 fn test_brazilian_holidays() {
1374 let cal = HolidayCalendar::for_region(Region::BR, 2024);
1375
1376 let independence = NaiveDate::from_ymd_opt(2024, 9, 7).unwrap();
1378 assert!(cal.is_holiday(independence));
1379
1380 let tiradentes = NaiveDate::from_ymd_opt(2024, 4, 21).unwrap();
1382 assert!(cal.is_holiday(tiradentes));
1383 }
1384
1385 #[test]
1386 fn test_mexican_holidays() {
1387 let cal = HolidayCalendar::for_region(Region::MX, 2024);
1388
1389 let independence = NaiveDate::from_ymd_opt(2024, 9, 16).unwrap();
1391 assert!(cal.is_holiday(independence));
1392 }
1393
1394 #[test]
1395 fn test_australian_holidays() {
1396 let cal = HolidayCalendar::for_region(Region::AU, 2024);
1397
1398 let anzac = NaiveDate::from_ymd_opt(2024, 4, 25).unwrap();
1400 assert!(cal.is_holiday(anzac));
1401
1402 let australia_day = NaiveDate::from_ymd_opt(2024, 1, 26).unwrap();
1404 assert!(cal.is_holiday(australia_day));
1405 }
1406
1407 #[test]
1408 fn test_singapore_holidays() {
1409 let cal = HolidayCalendar::for_region(Region::SG, 2024);
1410
1411 let national = NaiveDate::from_ymd_opt(2024, 8, 9).unwrap();
1413 assert!(cal.is_holiday(national));
1414 }
1415
1416 #[test]
1417 fn test_korean_holidays() {
1418 let cal = HolidayCalendar::for_region(Region::KR, 2024);
1419
1420 let liberation = NaiveDate::from_ymd_opt(2024, 8, 15).unwrap();
1422 assert!(cal.is_holiday(liberation));
1423
1424 let hangul = NaiveDate::from_ymd_opt(2024, 10, 9).unwrap();
1426 assert!(cal.is_holiday(hangul));
1427 }
1428
1429 #[test]
1430 fn test_chinese_holidays() {
1431 let cal = HolidayCalendar::for_region(Region::CN, 2024);
1432
1433 let national = NaiveDate::from_ymd_opt(2024, 10, 1).unwrap();
1435 assert!(cal.is_holiday(national));
1436 }
1437
1438 #[test]
1439 fn test_japanese_golden_week() {
1440 let cal = HolidayCalendar::for_region(Region::JP, 2024);
1441
1442 let kodomo = NaiveDate::from_ymd_opt(2024, 5, 5).unwrap();
1444 assert!(cal.is_holiday(kodomo));
1445 }
1446}