1use chrono::{Datelike, Duration, NaiveDate, Weekday};
6use serde::{Deserialize, Serialize};
7use std::collections::BTreeSet;
8use std::env;
9
10#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
12pub enum NthWeek {
13 First,
14 Second,
15 Third,
16 Fourth,
17 Last,
18}
19#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
21pub enum HalfCheck {
22 Before,
23 After,
24}
25
26#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
28pub enum Holiday {
29 WeekDay(Weekday),
31 MovableYearlyDay {
33 month: u32,
34 day: u32,
35 first: Option<i32>,
36 last: Option<i32>,
37 half_check: Option<HalfCheck>,
38 },
39 SingularDay(NaiveDate),
41 EasterOffset {
43 offset: i32,
44 first: Option<i32>,
45 last: Option<i32>,
46 },
47 MonthWeekday {
50 month: u32,
51 weekday: Weekday,
52 nth: NthWeek,
53 first: Option<i32>,
54 last: Option<i32>,
55 half_check: Option<HalfCheck>,
56 },
57}
58
59#[derive(Debug, Clone)]
61pub struct Calendar {
62 holidays: BTreeSet<NaiveDate>,
63 halfdays: BTreeSet<NaiveDate>,
64 weekdays: Vec<Weekday>,
65}
66
67impl Calendar {
68 pub fn calc_calendar(holiday_rules: &[Holiday], start: i32, end: i32) -> Calendar {
72 let mut holidays = BTreeSet::new();
73 let mut halfdays = BTreeSet::new();
74 let mut weekdays = Vec::new();
75
76 for rule in holiday_rules {
77 match rule {
78 Holiday::SingularDay(date) => {
79 let year = date.year();
80 if year >= start && year <= end {
81 holidays.insert(*date);
82 }
83 }
84 Holiday::WeekDay(weekday) => {
85 weekdays.push(*weekday);
86 }
87 Holiday::MovableYearlyDay {
89 month,
90 day,
91 first,
92 last,
93 half_check,
94 } => {
95 let (first, last) = Self::calc_first_and_last(start, end, first, last);
96 for year in first..last + 1 {
97 let date = Calendar::from_ymd(year, *month, *day);
98 let orig_wd = date.weekday();
100 let mut moved_already = false;
101 let date = match orig_wd {
102 Weekday::Sat => {
103 moved_already = true;
104 date.pred_opt().unwrap()
105 }
106 Weekday::Sun => {
107 moved_already = true;
108 date.succ_opt().unwrap()
109 }
110 _ => date,
111 };
112 let (last_date_of_month, last_date_of_year) = accounting_period_end(date);
113 if date != last_date_of_month && date != last_date_of_year {
115 holidays.insert(date);
116 if !moved_already {
117 do_halfday_check(&date, &mut halfdays, half_check);
118 }
119 }
120 }
121 }
122 Holiday::EasterOffset {
123 offset,
124 first,
125 last,
126 } => {
127 let (first, last) = Self::calc_first_and_last(start, end, first, last);
128 for year in first..last + 1 {
129 let easter = computus::gregorian(year).unwrap();
130 let easter = Calendar::from_ymd(easter.year, easter.month, easter.day);
131 let date = easter
132 .checked_add_signed(Duration::days(*offset as i64))
133 .unwrap();
134 holidays.insert(date);
135 }
136 }
137 Holiday::MonthWeekday {
138 month,
139 weekday,
140 nth,
141 first,
142 last,
143 half_check,
144 } => {
145 let (first, last) = Self::calc_first_and_last(start, end, first, last);
146 for year in first..last + 1 {
147 let day = match nth {
148 NthWeek::First => 1,
149 NthWeek::Second => 8,
150 NthWeek::Third => 15,
151 NthWeek::Fourth => 22,
152 NthWeek::Last => last_day_of_month(year, *month),
153 };
154 let mut date = Calendar::from_ymd(year, *month, day);
155 while date.weekday() != *weekday {
156 date = match nth {
157 NthWeek::Last => date.pred_opt().unwrap(),
158 _ => date.succ_opt().unwrap(),
159 }
160 }
161 holidays.insert(date);
162 do_halfday_check(&date, &mut halfdays, half_check);
163 }
164 }
165 }
166 }
167 Calendar {
168 holidays,
169 halfdays,
170 weekdays,
171 }
172 }
173
174 pub fn next_biz_day(&self, date: NaiveDate) -> NaiveDate {
176 let mut date = date.succ_opt().unwrap();
177 while !self.is_business_day(date) {
178 date = date.succ_opt().unwrap();
179 }
180 date
181 }
182
183 pub fn prev_biz_day(&self, date: NaiveDate) -> NaiveDate {
185 let mut date = date.pred_opt().unwrap();
186 while !self.is_business_day(date) {
187 date = date.pred_opt().unwrap();
188 }
189 date
190 }
191
192 fn calc_first_and_last(
193 start: i32,
194 end: i32,
195 first: &Option<i32>,
196 last: &Option<i32>,
197 ) -> (i32, i32) {
198 let first = match first {
199 Some(year) => std::cmp::max(start, *year),
200 _ => start,
201 };
202 let last = match last {
203 Some(year) => std::cmp::min(end, *year),
204 _ => end,
205 };
206 (first, last)
207 }
208
209 pub fn is_weekend(&self, day: NaiveDate) -> bool {
211 let weekday = day.weekday();
212 for w_day in &self.weekdays {
213 if weekday == *w_day {
214 return true;
215 }
216 }
217 false
218 }
219
220 pub fn is_holiday(&self, date: NaiveDate) -> bool {
222 self.holidays.contains(&date)
223 }
224
225 pub fn is_half_holiday(&self, date: NaiveDate) -> bool {
227 self.halfdays.contains(&date)
228 }
229
230 pub fn is_business_day(&self, date: NaiveDate) -> bool {
232 !self.is_weekend(date) && !self.is_holiday(date)
233 }
234
235 pub fn from_ymd(year: i32, month: u32, day: u32) -> NaiveDate {
236 NaiveDate::from_ymd_opt(year, month, day).unwrap()
237 }
238}
239
240pub fn is_leap_year(year: i32) -> bool {
242 NaiveDate::from_ymd_opt(year, 2, 29).is_some()
243}
244
245pub fn accounting_period_end(date: NaiveDate) -> (NaiveDate, NaiveDate) {
247 let month = date.month();
248 let year = date.year();
249 let last_date_of_month = NaiveDate::from_ymd_opt(year, month + 1, 1)
250 .unwrap_or_else(|| Calendar::from_ymd(year + 1, 1, 1))
251 .pred_opt()
252 .unwrap();
253 let last_date_of_year = NaiveDate::from_ymd_opt(year, 12, 31).unwrap();
254 (last_date_of_month, last_date_of_year)
255}
256
257pub fn do_halfday_check(
258 date: &NaiveDate,
259 halfdays: &mut BTreeSet<NaiveDate>,
260 half_check: &Option<HalfCheck>,
261) {
262 let weekday = date.weekday();
263 match half_check {
264 None => {}
265 Some(HalfCheck::Before) => {
266 if weekday == Weekday::Mon {
267 return;
268 }
269 let prior = date.pred_opt().unwrap();
270 halfdays.insert(prior);
271 }
272 Some(HalfCheck::After) => {
273 if weekday == Weekday::Fri {
274 return;
275 }
276 let next = date.succ_opt().unwrap();
277 halfdays.insert(next);
278 }
279 }
280}
281
282pub fn last_day_of_month(year: i32, month: u32) -> u32 {
284 NaiveDate::from_ymd_opt(year, month + 1, 1)
285 .unwrap_or_else(|| Calendar::from_ymd(year + 1, 1, 1))
286 .pred_opt()
287 .unwrap()
288 .day()
289}
290
291#[derive(Debug, Clone)]
293pub struct UsExchangeCalendar {
294 cal: Calendar,
295 holiday_rules: Vec<Holiday>,
296}
297
298impl UsExchangeCalendar {
299 pub fn with_default_range(populate: bool) -> UsExchangeCalendar {
303 let mut holiday_rules = vec![
304 Holiday::WeekDay(Weekday::Sat),
306 Holiday::WeekDay(Weekday::Sun),
308 Holiday::MovableYearlyDay {
310 month: 1,
311 day: 1,
312 first: None,
313 last: None,
314 half_check: None,
315 },
316 Holiday::MonthWeekday {
318 month: 1,
319 weekday: Weekday::Mon,
320 nth: NthWeek::Third,
321 first: None,
322 last: None,
323 half_check: None,
324 },
325 Holiday::MonthWeekday {
327 month: 2,
328 weekday: Weekday::Mon,
329 nth: NthWeek::Third,
330 first: None,
331 last: None,
332 half_check: None,
333 },
334 Holiday::EasterOffset {
336 offset: -2,
337 first: Some(2000),
338 last: None,
339 },
340 Holiday::MonthWeekday {
342 month: 5,
343 weekday: Weekday::Mon,
344 nth: NthWeek::Last,
345 first: None,
346 last: None,
347 half_check: None,
348 },
349 Holiday::MovableYearlyDay {
351 month: 6,
352 day: 19,
353 first: Some(2022),
354 last: None,
355 half_check: None,
356 },
357 Holiday::MovableYearlyDay {
359 month: 7,
360 day: 4,
361 first: None,
362 last: None,
363 half_check: Some(HalfCheck::Before),
364 },
365 Holiday::MonthWeekday {
367 month: 9,
368 weekday: Weekday::Mon,
369 nth: NthWeek::First,
370 first: None,
371 last: None,
372 half_check: None,
373 },
374 Holiday::MonthWeekday {
376 month: 11,
377 weekday: Weekday::Thu,
378 nth: NthWeek::Fourth,
379 first: None,
380 last: None,
381 half_check: Some(HalfCheck::After),
382 },
383 Holiday::MovableYearlyDay {
385 month: 12,
386 day: 25,
387 first: None,
388 last: None,
389 half_check: Some(HalfCheck::Before),
390 },
391 Holiday::SingularDay(Calendar::from_ymd(2001, 9, 11)),
392 ];
393 let additional_rules = env::var("ADDITIONAL_RULES");
394 if additional_rules.is_ok() {
395 let mut additional_rules: Vec<Holiday> =
396 serde_json::from_str(&additional_rules.unwrap()).unwrap();
397 holiday_rules.append(&mut additional_rules);
398 }
399 let cal = Calendar {
400 holidays: BTreeSet::new(),
401 halfdays: BTreeSet::new(),
402 weekdays: Vec::new(),
403 };
404 let mut sc = UsExchangeCalendar { cal, holiday_rules };
405 if populate {
406 sc.populate_cal(None, None);
407 }
408 sc
409 }
410
411 pub fn add_holiday_rule(&mut self, holiday: Holiday) -> &mut Self {
413 self.holiday_rules.push(holiday);
414 self
415 }
416
417 pub fn populate_cal(&mut self, start: Option<i32>, end: Option<i32>) -> &mut Self {
420 let start = start.unwrap_or(2000);
421 let end = end.unwrap_or(2050);
422 self.cal = Calendar::calc_calendar(&self.holiday_rules, start, end);
423 self
424 }
425
426 pub fn get_cal(&self) -> Calendar {
427 self.cal.clone()
428 }
429}
430
431#[cfg(test)]
432mod tests {
433 use super::*;
434
435 fn make_cal() -> Calendar {
436 let usec = UsExchangeCalendar::with_default_range(true);
437 usec.get_cal()
438 }
439
440 #[test]
441 fn fixed_dates_calendar() {
442 let holidays = vec![
443 Holiday::SingularDay(Calendar::from_ymd(2019, 11, 20)),
444 Holiday::SingularDay(Calendar::from_ymd(2019, 11, 24)),
445 Holiday::SingularDay(Calendar::from_ymd(2019, 11, 25)),
446 Holiday::WeekDay(Weekday::Sat),
447 Holiday::WeekDay(Weekday::Sun),
448 ];
449 let cal = Calendar::calc_calendar(&holidays, 2019, 2019);
450
451 assert_eq!(false, cal.is_business_day(Calendar::from_ymd(2019, 11, 20)));
452 assert_eq!(true, cal.is_business_day(Calendar::from_ymd(2019, 11, 21)));
453 assert_eq!(true, cal.is_business_day(Calendar::from_ymd(2019, 11, 22)));
454 assert_eq!(false, cal.is_business_day(Calendar::from_ymd(2019, 11, 23)));
456 assert_eq!(true, cal.is_weekend(Calendar::from_ymd(2019, 11, 23)));
457 assert_eq!(false, cal.is_holiday(Calendar::from_ymd(2019, 11, 23)));
458 assert_eq!(false, cal.is_business_day(Calendar::from_ymd(2019, 11, 24)));
460 assert_eq!(true, cal.is_weekend(Calendar::from_ymd(2019, 11, 24)));
461 assert_eq!(true, cal.is_holiday(Calendar::from_ymd(2019, 11, 24)));
462 assert_eq!(false, cal.is_business_day(Calendar::from_ymd(2019, 11, 25)));
463 assert_eq!(true, cal.is_business_day(Calendar::from_ymd(2019, 11, 26)));
464 }
465
466 #[test]
467 fn test_movable_yearly_day() {
468 let holidays = vec![Holiday::MovableYearlyDay {
469 month: 1,
470 day: 1,
471 first: None,
472 last: None,
473 half_check: None,
474 }];
475 let cal = Calendar::calc_calendar(&holidays, 2021, 2022);
476 assert_eq!(false, cal.is_holiday(Calendar::from_ymd(2021, 12, 31)));
477 }
478
479 #[test]
480 fn test_easter_offset() {
482 let holidays = vec![Holiday::EasterOffset {
483 offset: -2,
484 first: None,
485 last: None,
486 }];
487 let cal = Calendar::calc_calendar(&holidays, 2021, 2022);
488 assert_eq!(false, cal.is_business_day(Calendar::from_ymd(2021, 4, 2)));
489 assert_eq!(false, cal.is_business_day(Calendar::from_ymd(2022, 4, 15)));
490 }
491
492 #[test]
493 fn test_month_weekday() {
494 let holidays = vec![
495 Holiday::MonthWeekday {
497 month: 1,
498 weekday: Weekday::Mon,
499 nth: NthWeek::Third,
500 first: None,
501 last: None,
502 half_check: None,
503 },
504 Holiday::MonthWeekday {
506 month: 2,
507 weekday: Weekday::Mon,
508 nth: NthWeek::Third,
509 first: None,
510 last: None,
511 half_check: None,
512 },
513 ];
514 let cal = Calendar::calc_calendar(&holidays, 2022, 2022);
515 assert_eq!(true, cal.is_holiday(Calendar::from_ymd(2022, 1, 17)));
516 assert_eq!(true, cal.is_holiday(Calendar::from_ymd(2022, 2, 21)));
517 }
518
519 #[test]
520 fn serialize_cal_definition() {
522 let holidays = vec![
523 Holiday::MonthWeekday {
524 month: 11,
525 weekday: Weekday::Mon,
526 nth: NthWeek::First,
527 first: None,
528 last: None,
529 half_check: None,
530 },
531 Holiday::MovableYearlyDay {
532 month: 11,
533 day: 1,
534 first: Some(2016),
535 last: None,
536 half_check: None,
537 },
538 Holiday::SingularDay(Calendar::from_ymd(2019, 11, 25)),
539 Holiday::WeekDay(Weekday::Sat),
540 Holiday::EasterOffset {
541 offset: -2,
542 first: None,
543 last: None,
544 },
545 ];
546 let json = serde_json::to_string_pretty(&holidays).unwrap();
547 assert_eq!(
548 json,
549 r#"[
550 {
551 "MonthWeekday": {
552 "month": 11,
553 "weekday": "Mon",
554 "nth": "First",
555 "first": null,
556 "last": null,
557 "half_check": null
558 }
559 },
560 {
561 "MovableYearlyDay": {
562 "month": 11,
563 "day": 1,
564 "first": 2016,
565 "last": null,
566 "half_check": null
567 }
568 },
569 {
570 "SingularDay": "2019-11-25"
571 },
572 {
573 "WeekDay": "Sat"
574 },
575 {
576 "EasterOffset": {
577 "offset": -2,
578 "first": null,
579 "last": null
580 }
581 }
582]"#
583 );
584 let holidays2: Vec<Holiday> = serde_json::from_str(&json).unwrap();
585 assert_eq!(holidays.len(), holidays2.len());
586 for i in 0..holidays.len() {
587 assert_eq!(holidays[i], holidays2[i]);
588 }
589 }
590
591 #[test]
592 fn test_usexchange_calendar_empty() {
593 let sc = UsExchangeCalendar::with_default_range(false);
594 let c = sc.get_cal();
595 assert!(c.holidays.len() == 0);
596 assert!(c.halfdays.len() == 0);
597 assert!(c.weekdays.len() == 0);
598 }
599
600 #[test]
601 fn test_usexchange_calendar_populated() {
602 let sc = UsExchangeCalendar::with_default_range(true);
603 let c = sc.get_cal();
604 assert!(c.holidays.len() > 0);
605 assert!(c.halfdays.len() > 0);
606 assert!(c.weekdays.len() > 0);
607 assert!(c.is_holiday(Calendar::from_ymd(2021, 1, 1)));
608 assert_eq!(false, c.is_holiday(Calendar::from_ymd(2021, 12, 31)))
609 }
610
611 #[test]
612 fn test_usexchange_calendar_with_new_rule() {
613 let mut sc = UsExchangeCalendar::with_default_range(false);
615 let holiday = Holiday::MonthWeekday {
616 month: 3,
617 weekday: Weekday::Wed,
618 nth: NthWeek::Third,
619 first: None,
620 last: None,
621 half_check: None,
622 };
623 sc.add_holiday_rule(holiday).populate_cal(None, None);
624 let c = sc.get_cal();
625 assert_eq!(true, c.is_holiday(Calendar::from_ymd(2022, 3, 16)));
626 }
627
628 #[test]
629 fn test_is_trading_date() {
630 let cal = make_cal();
631 assert_eq!(cal.is_business_day(Calendar::from_ymd(2021, 4, 18)), false);
632 assert_eq!(cal.is_business_day(Calendar::from_ymd(2021, 4, 19)), true);
633 assert_eq!(cal.is_business_day(Calendar::from_ymd(2021, 1, 1)), false);
634 }
635
636 #[test]
637 fn test_is_partial_trading_date() {
638 let cal = make_cal();
639 assert_eq!(cal.is_half_holiday(Calendar::from_ymd(2027, 12, 23)), false);
640 assert_eq!(cal.is_half_holiday(Calendar::from_ymd(2026, 7, 2)), false);
641 assert_eq!(cal.is_half_holiday(Calendar::from_ymd(2021, 11, 26)), true);
642 assert_eq!(cal.is_half_holiday(Calendar::from_ymd(2022, 5, 12)), false);
643 }
644
645 #[test]
646 fn test_prev_biz_day() {
647 let cal = make_cal();
648 assert_eq!(
649 cal.prev_biz_day(Calendar::from_ymd(2021, 1, 18)),
650 Calendar::from_ymd(2021, 1, 15)
651 );
652 assert_eq!(
653 cal.prev_biz_day(Calendar::from_ymd(2021, 4, 19)),
654 Calendar::from_ymd(2021, 4, 16)
655 );
656 assert_eq!(
657 cal.prev_biz_day(Calendar::from_ymd(2021, 8, 9)),
658 Calendar::from_ymd(2021, 8, 6)
659 );
660 }
661
662 #[test]
663 fn test_next_biz_day() {
664 let cal = make_cal();
665 assert_eq!(
666 cal.next_biz_day(Calendar::from_ymd(2021, 4, 16)),
667 Calendar::from_ymd(2021, 4, 19)
668 );
669 assert_eq!(
670 cal.next_biz_day(Calendar::from_ymd(2021, 4, 19)),
671 Calendar::from_ymd(2021, 4, 20)
672 );
673 assert_eq!(
674 cal.next_biz_day(Calendar::from_ymd(2021, 4, 2)),
675 Calendar::from_ymd(2021, 4, 5)
676 );
677 }
678}