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}
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#[derive(Debug, Clone)]
42pub struct Holiday {
43 pub name: String,
45 pub date: NaiveDate,
47 pub activity_multiplier: f64,
49 pub is_bank_holiday: bool,
51}
52
53impl Holiday {
54 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 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#[derive(Debug, Clone)]
73pub struct HolidayCalendar {
74 pub region: Region,
76 pub year: i32,
78 pub holidays: Vec<Holiday>,
80}
81
82impl HolidayCalendar {
83 pub fn new(region: Region, year: i32) -> Self {
85 Self {
86 region,
87 year,
88 holidays: Vec::new(),
89 }
90 }
91
92 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 pub fn is_holiday(&self, date: NaiveDate) -> bool {
106 self.holidays.iter().any(|h| h.date == date)
107 }
108
109 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 pub fn get_holidays(&self, date: NaiveDate) -> Vec<&Holiday> {
120 self.holidays.iter().filter(|h| h.date == date).collect()
121 }
122
123 pub fn add_holiday(&mut self, holiday: Holiday) {
125 self.holidays.push(holiday);
126 }
127
128 pub fn all_dates(&self) -> Vec<NaiveDate> {
130 self.holidays.iter().map(|h| h.date).collect()
131 }
132
133 fn us_holidays(year: i32) -> Self {
135 let mut cal = Self::new(Region::US, year);
136
137 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 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 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 let memorial = Self::last_weekday_of_month(year, 5, Weekday::Mon);
155 cal.add_holiday(Holiday::new("Memorial Day", memorial, 0.05));
156
157 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 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 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 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 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 let thanksgiving = Self::nth_weekday_of_month(year, 11, Weekday::Thu, 4);
191 cal.add_holiday(Holiday::new("Thanksgiving", thanksgiving, 0.02));
192
193 cal.add_holiday(Holiday::new(
195 "Day after Thanksgiving",
196 thanksgiving + Duration::days(1),
197 0.1,
198 ));
199
200 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 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 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 fn de_holidays(year: i32) -> Self {
221 let mut cal = Self::new(Region::DE, year);
222
223 cal.add_holiday(Holiday::new(
225 "Neujahr",
226 NaiveDate::from_ymd_opt(year, 1, 1).unwrap(),
227 0.02,
228 ));
229
230 let easter = Self::easter_date(year);
232 cal.add_holiday(Holiday::new("Karfreitag", easter - Duration::days(2), 0.02));
233
234 cal.add_holiday(Holiday::new(
236 "Ostermontag",
237 easter + Duration::days(1),
238 0.02,
239 ));
240
241 cal.add_holiday(Holiday::new(
243 "Tag der Arbeit",
244 NaiveDate::from_ymd_opt(year, 5, 1).unwrap(),
245 0.02,
246 ));
247
248 cal.add_holiday(Holiday::new(
250 "Christi Himmelfahrt",
251 easter + Duration::days(39),
252 0.02,
253 ));
254
255 cal.add_holiday(Holiday::new(
257 "Pfingstmontag",
258 easter + Duration::days(50),
259 0.02,
260 ));
261
262 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 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 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 fn gb_holidays(year: i32) -> Self {
293 let mut cal = Self::new(Region::GB, year);
294
295 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 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 cal.add_holiday(Holiday::new(
313 "Easter Monday",
314 easter + Duration::days(1),
315 0.02,
316 ));
317
318 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 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 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 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 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 fn cn_holidays(year: i32) -> Self {
351 let mut cal = Self::new(Region::CN, year);
352
353 cal.add_holiday(Holiday::new(
355 "New Year",
356 NaiveDate::from_ymd_opt(year, 1, 1).unwrap(),
357 0.05,
358 ));
359
360 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 cal.add_holiday(Holiday::new(
377 "Qingming Festival",
378 NaiveDate::from_ymd_opt(year, 4, 5).unwrap(),
379 0.05,
380 ));
381
382 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 cal.add_holiday(Holiday::new(
397 "Dragon Boat Festival",
398 NaiveDate::from_ymd_opt(year, 6, 10).unwrap(),
399 0.05,
400 ));
401
402 cal.add_holiday(Holiday::new(
404 "Mid-Autumn Festival",
405 NaiveDate::from_ymd_opt(year, 9, 15).unwrap(),
406 0.05,
407 ));
408
409 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 fn jp_holidays(year: i32) -> Self {
427 let mut cal = Self::new(Region::JP, year);
428
429 cal.add_holiday(Holiday::new(
431 "Ganjitsu (New Year)",
432 NaiveDate::from_ymd_opt(year, 1, 1).unwrap(),
433 0.02,
434 ));
435
436 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 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 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 cal.add_holiday(Holiday::new(
461 "Tenno Tanjobi",
462 NaiveDate::from_ymd_opt(year, 2, 23).unwrap(),
463 0.02,
464 ));
465
466 cal.add_holiday(Holiday::new(
468 "Shunbun no Hi",
469 NaiveDate::from_ymd_opt(year, 3, 20).unwrap(),
470 0.02,
471 ));
472
473 cal.add_holiday(Holiday::new(
475 "Showa no Hi",
476 NaiveDate::from_ymd_opt(year, 4, 29).unwrap(),
477 0.02,
478 ));
479
480 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 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 cal.add_holiday(Holiday::new(
503 "Yama no Hi",
504 NaiveDate::from_ymd_opt(year, 8, 11).unwrap(),
505 0.05,
506 ));
507
508 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 cal.add_holiday(Holiday::new(
514 "Shubun no Hi",
515 NaiveDate::from_ymd_opt(year, 9, 23).unwrap(),
516 0.02,
517 ));
518
519 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 cal.add_holiday(Holiday::new(
525 "Bunka no Hi",
526 NaiveDate::from_ymd_opt(year, 11, 3).unwrap(),
527 0.02,
528 ));
529
530 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 fn in_holidays(year: i32) -> Self {
542 let mut cal = Self::new(Region::IN, year);
543
544 cal.add_holiday(Holiday::new(
546 "Republic Day",
547 NaiveDate::from_ymd_opt(year, 1, 26).unwrap(),
548 0.02,
549 ));
550
551 cal.add_holiday(Holiday::new(
553 "Holi",
554 NaiveDate::from_ymd_opt(year, 3, 10).unwrap(),
555 0.05,
556 ));
557
558 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 cal.add_holiday(Holiday::new(
568 "Independence Day",
569 NaiveDate::from_ymd_opt(year, 8, 15).unwrap(),
570 0.02,
571 ));
572
573 cal.add_holiday(Holiday::new(
575 "Gandhi Jayanti",
576 NaiveDate::from_ymd_opt(year, 10, 2).unwrap(),
577 0.02,
578 ));
579
580 cal.add_holiday(Holiday::new(
582 "Dussehra",
583 NaiveDate::from_ymd_opt(year, 10, 15).unwrap(),
584 0.05,
585 ));
586
587 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 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 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 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 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 fn observe_weekend(date: NaiveDate) -> NaiveDate {
665 match date.weekday() {
666 Weekday::Sat => date - Duration::days(1), Weekday::Sun => date + Duration::days(1), _ => date,
669 }
670 }
671
672 fn approximate_chinese_new_year(year: i32) -> NaiveDate {
674 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; 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 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 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 fn approximate_diwali(year: i32) -> NaiveDate {
704 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#[derive(Debug, Clone, Serialize, Deserialize)]
717pub struct CustomHolidayConfig {
718 pub name: String,
720 pub month: u8,
722 pub day: u8,
724 #[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 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 let christmas = NaiveDate::from_ymd_opt(2024, 12, 25).unwrap();
754 assert!(cal.is_holiday(christmas));
755
756 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 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 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 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 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 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 let christmas = NaiveDate::from_ymd_opt(2024, 12, 25).unwrap();
807 assert!(cal.get_multiplier(christmas) < 0.1);
808
809 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 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 let kodomo = NaiveDate::from_ymd_opt(2024, 5, 5).unwrap();
850 assert!(cal.is_holiday(kodomo));
851 }
852}