cal_calc/
lib.rs

1//! Implementation of (bank) holidays.
2//! Calendars are required to verify whether an exchange is open or if a certain
3//! cash flow could be settled on a specific day. They are also needed to calculate
4//! the amount of business days between to given dates.
5//! Because of the settlement rules, bank holidays have an impact on how to
6//! rollout cash flows from fixed income products.
7//! The approach taken here is to define a set of rules to determine bank holidays.
8//! From this set of rules, a calendar is generated by calculating all bank holidays
9//! within a given range of years for fast access.
10
11use serde::{Deserialize, Serialize};
12use thiserror::Error;
13use time::{Date, Duration, Weekday};
14
15use std::collections::BTreeSet;
16
17mod calendar_definitions;
18
19pub use calendar_definitions::*;
20
21#[derive(Error, Debug)]
22/// Error class for calendar calculation
23pub enum CalendarError {
24    #[error("calendar could not been found")]
25    CalendarNotFound,
26    #[error("failed to create invalid date")]
27    OutOfBound(#[from] time::error::ComponentRange),
28    #[error("try to proceed beyond max date")]
29    MaxDay,
30    #[error("try to proceed before min date")]
31    MinDay,
32}
33
34type Result<T> = std::result::Result<T, CalendarError>;
35
36/// Specifies the nth week of a month
37#[derive(Deserialize, Serialize, Debug, PartialEq)]
38pub enum NthWeek {
39    First,
40    Second,
41    Third,
42    Fourth,
43    Last,
44    SecondLast,
45    ThirdLast,
46    FourthLast,
47}
48
49#[derive(Deserialize, Serialize, Debug, PartialEq)]
50pub enum Holiday {
51    /// Though weekends are no holidays, they need to be specified in the calendar. Weekends are assumed to be non-business days.
52    /// In most countries, weekends include Saturday (`Sat`) and Sunday (`Sun`). Unfortunately, there are a few exceptions.
53    WeekDay(Weekday),
54    /// A holiday that occurs every year on the same day.
55    /// `first` and `last` are the first and last year this day is a holiday (inclusively).
56    YearlyDay {
57        month: u8,
58        day: u8,
59        first: Option<i32>,
60        last: Option<i32>,
61    },
62    /// Occurs every year, but is moved to next non-weekend day if it falls on a weekend.
63    /// Note that Saturday and Sunday here assumed to be weekend days, even if these days
64    /// are not defined as weekends in this calendar. If the next Monday is already a holiday,
65    /// the date will be moved to the next available business day.
66    /// `first` and `last` are the first and last year this day is a holiday (inclusively).
67    MovableYearlyDay {
68        month: u8,
69        day: u8,
70        first: Option<i32>,
71        last: Option<i32>,
72    },
73    /// Occurs every year, but is moved to previous Friday if it falls on Saturday
74    /// and to the next Monday if it falls on a Sunday.
75    /// `first` and `last` are the first and last year this day is a holiday (inclusively).
76    ModifiedMovableYearlyDay {
77        month: u8,
78        day: u8,
79        first: Option<i32>,
80        last: Option<i32>,
81    },
82    /// A single holiday which is valid only once in time.
83    SingularDay(Date),
84    /// A holiday that is defined in relative days (e.g. -2 for Good Friday) to Easter (Sunday).
85    EasterOffset {
86        offset: i32,
87        first: Option<i32>,
88        last: Option<i32>,
89    },
90    /// A holiday that falls on the nth (or last) weekday of a specific month, e.g. the first Monday in May.
91    /// `first` and `last` are the first and last year this day is a holiday (inclusively).
92    MonthWeekday {
93        month: u8,
94        weekday: Weekday,
95        nth: NthWeek,
96        first: Option<i32>,
97        last: Option<i32>,
98    },
99}
100
101/// Calendar for arbitrary complex holiday rules
102#[derive(Debug, Clone)]
103pub struct Calendar {
104    holidays: BTreeSet<Date>,
105    weekdays: Vec<Weekday>,
106}
107
108fn new_date(year: i32, month: u8, day: u8) -> Result<Date> {
109    Ok(Date::from_calendar_date(year, month.try_into()?, day)?)
110}
111
112impl Calendar {
113    /// Calculate all holidays and recognize weekend days for a given range of years
114    /// from `start` to `end` (inclusively). The calculation is performed on the basis
115    /// of a vector of holiday rules.
116    pub fn calc_calendar(holiday_rules: &[Holiday], start: i32, end: i32) -> Result<Calendar> {
117        let mut holidays = BTreeSet::new();
118        let mut weekdays = Vec::new();
119
120        for rule in holiday_rules {
121            match rule {
122                Holiday::SingularDay(date) => {
123                    let year = date.year();
124                    if year >= start && year <= end {
125                        holidays.insert(*date);
126                    }
127                }
128                Holiday::WeekDay(weekday) => {
129                    weekdays.push(*weekday);
130                }
131                Holiday::YearlyDay {
132                    month,
133                    day,
134                    first,
135                    last,
136                } => {
137                    let (first, last) = Self::calc_first_and_last(start, end, first, last);
138                    for year in first..last + 1 {
139                        holidays.insert(new_date(year, *month, *day)?);
140                    }
141                }
142                Holiday::MovableYearlyDay {
143                    month,
144                    day,
145                    first,
146                    last,
147                } => {
148                    let (first, last) = Self::calc_first_and_last(start, end, first, last);
149                    for year in first..last + 1 {
150                        let date = new_date(year, *month, *day)?;
151                        let mut date = match date.weekday() {
152                            Weekday::Saturday => date
153                                .next_day()
154                                .ok_or(CalendarError::MaxDay)?
155                                .next_day()
156                                .ok_or(CalendarError::MaxDay)?,
157                            Weekday::Sunday => date.next_day().ok_or(CalendarError::MaxDay)?,
158                            _ => date,
159                        };
160                        while holidays.contains(&date) {
161                            date = date.next_day().ok_or(CalendarError::MaxDay)?;
162                        }
163                        holidays.insert(date);
164                    }
165                }
166                Holiday::ModifiedMovableYearlyDay {
167                    month,
168                    day,
169                    first,
170                    last,
171                } => {
172                    let (first, last) = Self::calc_first_and_last(start, end, first, last);
173                    for year in first..last + 1 {
174                        let date = new_date(year, *month, *day)?;
175                        let moved_date = match date.weekday() {
176                            Weekday::Saturday => {
177                                date.previous_day().ok_or(CalendarError::MinDay)?
178                            }
179                            Weekday::Sunday => date.next_day().ok_or(CalendarError::MaxDay)?,
180                            _ => date,
181                        };
182                        if moved_date.month() == date.month() {
183                            holidays.insert(moved_date);
184                        } else {
185                            holidays.insert(date);
186                        }
187                    }
188                }
189                Holiday::EasterOffset {
190                    offset,
191                    first,
192                    last,
193                } => {
194                    let (first, last) = Self::calc_first_and_last(start, end, first, last);
195                    for year in first..last + 1 {
196                        let easter = computus::gregorian(year).unwrap();
197                        let easter = new_date(easter.year, easter.month as u8, easter.day as u8)?;
198                        let date = easter.checked_add(Duration::days(*offset as i64)).unwrap();
199                        holidays.insert(date);
200                    }
201                }
202                Holiday::MonthWeekday {
203                    month,
204                    weekday,
205                    nth,
206                    first,
207                    last,
208                } => {
209                    let (first, last) = Self::calc_first_and_last(start, end, first, last);
210                    for year in first..last + 1 {
211                        let day = match nth {
212                            NthWeek::First => 1,
213                            NthWeek::Second => 8,
214                            NthWeek::Third => 15,
215                            NthWeek::Fourth => 22,
216                            NthWeek::Last => last_day_of_month(year, *month),
217                            NthWeek::SecondLast => last_day_of_month(year, *month) - 7,
218                            NthWeek::ThirdLast => last_day_of_month(year, *month) - 14,
219                            NthWeek::FourthLast => last_day_of_month(year, *month) - 21,
220                        };
221                        let mut date = new_date(year, *month, day)?;
222                        while date.weekday() != *weekday {
223                            date = match nth {
224                                NthWeek::Last
225                                | NthWeek::SecondLast
226                                | NthWeek::ThirdLast
227                                | NthWeek::FourthLast => {
228                                    date.previous_day().ok_or(CalendarError::MinDay)?
229                                }
230                                _ => date.next_day().ok_or(CalendarError::MaxDay)?,
231                            }
232                        }
233                        holidays.insert(date);
234                    }
235                }
236            }
237        }
238        Ok(Calendar { holidays, weekdays })
239    }
240
241    /// Calculate the next business day
242    pub fn next_bday(&self, date: Date) -> Result<Date> {
243        let mut date = date.next_day().ok_or(CalendarError::MaxDay)?;
244        while !self.is_business_day(date) {
245            date = date.next_day().ok_or(CalendarError::MaxDay)?;
246        }
247        Ok(date)
248    }
249
250    /// Calculate the previous business day
251    pub fn prev_bday(&self, date: Date) -> Result<Date> {
252        let mut date = date.previous_day().ok_or(CalendarError::MinDay)?;
253        while !self.is_business_day(date) {
254            date = date.previous_day().ok_or(CalendarError::MinDay)?;
255        }
256        Ok(date)
257    }
258
259    fn calc_first_and_last(
260        start: i32,
261        end: i32,
262        first: &Option<i32>,
263        last: &Option<i32>,
264    ) -> (i32, i32) {
265        let first = match first {
266            Some(year) => std::cmp::max(start, *year),
267            _ => start,
268        };
269        let last = match last {
270            Some(year) => std::cmp::min(end, *year),
271            _ => end,
272        };
273        (first, last)
274    }
275
276    /// Returns true if the date falls on a weekend
277    pub fn is_weekend(&self, day: Date) -> bool {
278        let weekday = day.weekday();
279        for w_day in &self.weekdays {
280            if weekday == *w_day {
281                return true;
282            }
283        }
284        false
285    }
286
287    /// Returns true if the specified day is a bank holiday
288    pub fn is_holiday(&self, date: Date) -> bool {
289        self.holidays.contains(&date)
290    }
291
292    /// Returns true if the specified day is a business day
293    pub fn is_business_day(&self, date: Date) -> bool {
294        !self.is_weekend(date) && !self.is_holiday(date)
295    }
296}
297
298pub trait CalendarProvider {
299    fn get_calendar(&self, calendar_name: &str) -> Result<&Calendar>;
300}
301
302/// Returns true if the specified year is a leap year (i.e. Feb 29th exists for this year)
303pub fn is_leap_year(year: i32) -> bool {
304    new_date(year, 2, 29).is_ok()
305}
306
307/// Calculate the last day of a given month in a given year
308pub fn last_day_of_month(year: i32, month: u8) -> u8 {
309    if let Ok(date) = new_date(year, month + 1, 1) {
310        date.previous_day().unwrap().day()
311    } else {
312        // last day of December
313        31
314    }
315}
316
317pub struct SimpleCalendar {
318    cal: Calendar,
319}
320
321impl SimpleCalendar {
322    pub fn new(cal: &Calendar) -> SimpleCalendar {
323        SimpleCalendar { cal: cal.clone() }
324    }
325}
326
327impl CalendarProvider for SimpleCalendar {
328    fn get_calendar(&self, _calendar_name: &str) -> Result<&Calendar> {
329        Ok(&self.cal)
330    }
331}
332
333impl Default for SimpleCalendar {
334    fn default() -> SimpleCalendar {
335        SimpleCalendar {
336            cal: Calendar::calc_calendar(&[], 2020, 2021).unwrap(),
337        }
338    }
339}
340
341#[cfg(test)]
342mod tests {
343    use super::*;
344    use time::macros::date;
345
346    #[test]
347    fn fixed_dates_calendar() {
348        let holidays = vec![
349            Holiday::SingularDay(date!(2019 - 11 - 20)),
350            Holiday::SingularDay(date!(2019 - 11 - 24)),
351            Holiday::SingularDay(date!(2019 - 11 - 25)),
352            Holiday::WeekDay(Weekday::Saturday),
353            Holiday::WeekDay(Weekday::Sunday),
354        ];
355        let cal = Calendar::calc_calendar(&holidays, 2019, 2019).unwrap();
356
357        assert_eq!(false, cal.is_business_day(date!(2019 - 11 - 20)));
358        assert_eq!(true, cal.is_business_day(date!(2019 - 11 - 21)));
359        assert_eq!(true, cal.is_business_day(date!(2019 - 11 - 22)));
360        // weekend
361        assert_eq!(false, cal.is_business_day(date!(2019 - 11 - 23)));
362        assert_eq!(true, cal.is_weekend(date!(2019 - 11 - 23)));
363        assert_eq!(false, cal.is_holiday(date!(2019 - 11 - 23)));
364        // weekend and holiday
365        assert_eq!(false, cal.is_business_day(date!(2019 - 11 - 24)));
366        assert_eq!(true, cal.is_weekend(date!(2019 - 11 - 24)));
367        assert_eq!(true, cal.is_holiday(date!(2019 - 11 - 24)));
368        assert_eq!(false, cal.is_business_day(date!(2019 - 11 - 25)));
369        assert_eq!(true, cal.is_business_day(date!(2019 - 11 - 26)));
370    }
371
372    #[test]
373    fn test_yearly_day() {
374        let holidays = vec![
375            Holiday::YearlyDay {
376                month: 11,
377                day: 1,
378                first: None,
379                last: None,
380            },
381            Holiday::YearlyDay {
382                month: 11,
383                day: 2,
384                first: Some(2019),
385                last: None,
386            },
387            Holiday::YearlyDay {
388                month: 11,
389                day: 3,
390                first: None,
391                last: Some(2019),
392            },
393            Holiday::YearlyDay {
394                month: 11,
395                day: 4,
396                first: Some(2019),
397                last: Some(2019),
398            },
399        ];
400        let cal = Calendar::calc_calendar(&holidays, 2018, 2020).unwrap();
401
402        assert_eq!(true, cal.is_holiday(date!(2018 - 11 - 1)));
403        assert_eq!(true, cal.is_holiday(date!(2019 - 11 - 1)));
404        assert_eq!(true, cal.is_holiday(date!(2020 - 11 - 1)));
405
406        assert_eq!(false, cal.is_holiday(date!(2018 - 11 - 2)));
407        assert_eq!(true, cal.is_holiday(date!(2019 - 11 - 2)));
408        assert_eq!(true, cal.is_holiday(date!(2020 - 11 - 2)));
409
410        assert_eq!(true, cal.is_holiday(date!(2018 - 11 - 3)));
411        assert_eq!(true, cal.is_holiday(date!(2019 - 11 - 3)));
412        assert_eq!(false, cal.is_holiday(date!(2020 - 11 - 3)));
413
414        assert_eq!(false, cal.is_holiday(date!(2018 - 11 - 4)));
415        assert_eq!(true, cal.is_holiday(date!(2019 - 11 - 4)));
416        assert_eq!(false, cal.is_holiday(date!(2020 - 11 - 4)));
417    }
418
419    #[test]
420    fn test_movable_yearly_day() {
421        let holidays = vec![
422            Holiday::MovableYearlyDay {
423                month: 11,
424                day: 1,
425                first: None,
426                last: None,
427            },
428            Holiday::MovableYearlyDay {
429                month: 11,
430                day: 2,
431                first: None,
432                last: None,
433            },
434            Holiday::MovableYearlyDay {
435                month: 11,
436                day: 10,
437                first: None,
438                last: Some(2019),
439            },
440            Holiday::MovableYearlyDay {
441                month: 11,
442                day: 17,
443                first: Some(2019),
444                last: None,
445            },
446            Holiday::MovableYearlyDay {
447                month: 11,
448                day: 24,
449                first: Some(2019),
450                last: Some(2019),
451            },
452        ];
453        let cal = Calendar::calc_calendar(&holidays, 2018, 2020).unwrap();
454        assert_eq!(true, cal.is_holiday(date!(2018 - 11 - 1)));
455        assert_eq!(true, cal.is_holiday(date!(2018 - 11 - 2)));
456        assert_eq!(true, cal.is_holiday(date!(2019 - 11 - 1)));
457        assert_eq!(true, cal.is_holiday(date!(2019 - 11 - 4)));
458        assert_eq!(true, cal.is_holiday(date!(2020 - 11 - 2)));
459        assert_eq!(true, cal.is_holiday(date!(2020 - 11 - 3)));
460
461        assert_eq!(true, cal.is_holiday(date!(2018 - 11 - 12)));
462        assert_eq!(true, cal.is_holiday(date!(2019 - 11 - 11)));
463        assert_eq!(false, cal.is_holiday(date!(2020 - 11 - 10)));
464        assert_eq!(false, cal.is_holiday(date!(2018 - 11 - 19)));
465        assert_eq!(true, cal.is_holiday(date!(2019 - 11 - 18)));
466        assert_eq!(true, cal.is_holiday(date!(2020 - 11 - 17)));
467        assert_eq!(false, cal.is_holiday(date!(2018 - 11 - 26)));
468        assert_eq!(true, cal.is_holiday(date!(2019 - 11 - 25)));
469        assert_eq!(false, cal.is_holiday(date!(2020 - 11 - 24)));
470    }
471
472    #[test]
473    /// Good Friday example
474    fn test_easter_offset() {
475        let holidays = vec![Holiday::EasterOffset {
476            offset: -2,
477            first: None,
478            last: None,
479        }];
480        let cal = Calendar::calc_calendar(&holidays, 2019, 2020).unwrap();
481        assert_eq!(false, cal.is_business_day(date!(2019 - 4 - 19)));
482        assert_eq!(false, cal.is_business_day(date!(2020 - 4 - 10)));
483    }
484
485    #[test]
486    fn test_month_weekday() {
487        let holidays = vec![
488            Holiday::MonthWeekday {
489                month: 11,
490                weekday: Weekday::Monday,
491                nth: NthWeek::First,
492                first: None,
493                last: None,
494            },
495            Holiday::MonthWeekday {
496                month: 11,
497                weekday: Weekday::Tuesday,
498                nth: NthWeek::Second,
499                first: None,
500                last: None,
501            },
502            Holiday::MonthWeekday {
503                month: 11,
504                weekday: Weekday::Wednesday,
505                nth: NthWeek::Third,
506                first: None,
507                last: None,
508            },
509            Holiday::MonthWeekday {
510                month: 11,
511                weekday: Weekday::Thursday,
512                nth: NthWeek::Fourth,
513                first: None,
514                last: None,
515            },
516            Holiday::MonthWeekday {
517                month: 11,
518                weekday: Weekday::Friday,
519                nth: NthWeek::Last,
520                first: None,
521                last: None,
522            },
523            Holiday::MonthWeekday {
524                month: 11,
525                weekday: Weekday::Saturday,
526                nth: NthWeek::First,
527                first: None,
528                last: Some(2018),
529            },
530            Holiday::MonthWeekday {
531                month: 11,
532                weekday: Weekday::Sunday,
533                nth: NthWeek::Last,
534                first: Some(2020),
535                last: None,
536            },
537        ];
538        let cal = Calendar::calc_calendar(&holidays, 2018, 2020).unwrap();
539        assert_eq!(true, cal.is_holiday(date!(2019 - 11 - 4)));
540        assert_eq!(true, cal.is_holiday(date!(2019 - 11 - 12)));
541        assert_eq!(true, cal.is_holiday(date!(2019 - 11 - 20)));
542        assert_eq!(true, cal.is_holiday(date!(2019 - 11 - 28)));
543        assert_eq!(true, cal.is_holiday(date!(2019 - 11 - 29)));
544
545        assert_eq!(true, cal.is_holiday(date!(2018 - 11 - 3)));
546        assert_eq!(false, cal.is_holiday(date!(2019 - 11 - 2)));
547        assert_eq!(false, cal.is_holiday(date!(2020 - 11 - 7)));
548        assert_eq!(false, cal.is_holiday(date!(2018 - 11 - 25)));
549        assert_eq!(false, cal.is_holiday(date!(2019 - 11 - 24)));
550        assert_eq!(true, cal.is_holiday(date!(2020 - 11 - 29)));
551    }
552
553    #[test]
554    /// Testing serialization and deserialization of holidays definitions
555    fn serialize_cal_definition() {
556        let holidays = vec![
557            Holiday::MonthWeekday {
558                month: 11,
559                weekday: Weekday::Monday,
560                nth: NthWeek::First,
561                first: None,
562                last: None,
563            },
564            Holiday::MovableYearlyDay {
565                month: 11,
566                day: 1,
567                first: Some(2016),
568                last: None,
569            },
570            Holiday::YearlyDay {
571                month: 11,
572                day: 3,
573                first: None,
574                last: Some(2019),
575            },
576            Holiday::SingularDay(date!(2019 - 11 - 25)),
577            Holiday::WeekDay(Weekday::Saturday),
578            Holiday::EasterOffset {
579                offset: -2,
580                first: None,
581                last: None,
582            },
583        ];
584        let json = serde_json::to_string_pretty(&holidays).unwrap();
585        assert_eq!(
586            json,
587            r#"[
588  {
589    "MonthWeekday": {
590      "month": 11,
591      "weekday": 1,
592      "nth": "First",
593      "first": null,
594      "last": null
595    }
596  },
597  {
598    "MovableYearlyDay": {
599      "month": 11,
600      "day": 1,
601      "first": 2016,
602      "last": null
603    }
604  },
605  {
606    "YearlyDay": {
607      "month": 11,
608      "day": 3,
609      "first": null,
610      "last": 2019
611    }
612  },
613  {
614    "SingularDay": [
615      2019,
616      329
617    ]
618  },
619  {
620    "WeekDay": 6
621  },
622  {
623    "EasterOffset": {
624      "offset": -2,
625      "first": null,
626      "last": null
627    }
628  }
629]"#
630        );
631        let holidays2: Vec<Holiday> = serde_json::from_str(&json).unwrap();
632        assert_eq!(holidays.len(), holidays2.len());
633        for i in 0..holidays.len() {
634            assert_eq!(holidays[i], holidays2[i]);
635        }
636    }
637
638    #[test]
639    fn test_modified_movable() {
640        let holidays = vec![
641            Holiday::ModifiedMovableYearlyDay {
642                month: 12,
643                day: 25,
644                first: None,
645                last: None,
646            },
647            Holiday::ModifiedMovableYearlyDay {
648                month: 1,
649                day: 1,
650                first: None,
651                last: None,
652            },
653        ];
654        let cal = Calendar::calc_calendar(&holidays, 2020, 2023).unwrap();
655        assert!(cal.is_holiday(date!(2020 - 12 - 25)));
656        assert!(cal.is_holiday(date!(2021 - 12 - 24)));
657        assert!(cal.is_holiday(date!(2022 - 12 - 26)));
658        assert!(cal.is_holiday(date!(2023 - 12 - 25)));
659        assert!(cal.is_holiday(date!(2020 - 1 - 1)));
660        assert!(cal.is_holiday(date!(2021 - 1 - 1)));
661        assert!(cal.is_holiday(date!(2022 - 1 - 1)));
662        assert!(cal.is_holiday(date!(2023 - 1 - 2)));
663    }
664}