rateslib/scheduling/calendars/
union_cal.rs

1use chrono::prelude::*;
2use pyo3::pyclass;
3use serde::{Deserialize, Serialize};
4
5use crate::scheduling::{ndt, Cal, CalendarAdjustment, DateRoll};
6
7/// A business day calendar which is the potential union of multiple calendars.
8///
9/// # Notes
10/// The following set definitions are observed by this object:
11/// - A **business day** is such if it is a *business day* in each one of the `calendars`.
12/// - A **settleable day** is such if it is a *business day* in each one of the
13///   `settlement_calendars`, otherwise every calendar day is a *settleable day*.
14/// - A **settleable business day** is both a *business day* and a *settleable day*.
15#[pyclass(module = "rateslib.rs")]
16#[derive(Clone, Default, Debug, Serialize, Deserialize)]
17pub struct UnionCal {
18    /// A vector of [Cal] used to determine **business** days.
19    pub calendars: Vec<Cal>,
20    /// A vector of [Cal] used to determine **settleable** days.
21    pub settlement_calendars: Option<Vec<Cal>>,
22}
23
24impl UnionCal {
25    /// Create a new [`UnionCal`].
26    ///
27    /// # Examples
28    /// ```rust
29    /// # use rateslib::scheduling::{Cal, UnionCal, ndt, DateRoll};
30    /// let stk = Cal::new(vec![ndt(2025, 6, 20)], vec![5,6]);
31    /// let fed = Cal::new(vec![ndt(2025, 6, 19)], vec![5,6]);
32    /// let stk_pipe_fed = UnionCal::new(vec![stk], Some(vec![fed]));
33    /// assert_eq!(true, stk_pipe_fed.is_bus_day(&ndt(2025, 6, 19)));
34    /// assert_eq!(false, stk_pipe_fed.is_settlement(&ndt(2025, 6, 19)));
35    /// ```
36    pub fn new(calendars: Vec<Cal>, settlement_calendars: Option<Vec<Cal>>) -> Self {
37        UnionCal {
38            calendars,
39            settlement_calendars,
40        }
41    }
42}
43
44impl DateRoll for UnionCal {
45    fn is_weekday(&self, date: &NaiveDateTime) -> bool {
46        self.calendars.iter().all(|cal| cal.is_weekday(date))
47    }
48
49    fn is_holiday(&self, date: &NaiveDateTime) -> bool {
50        self.calendars.iter().any(|cal| cal.is_holiday(date))
51    }
52
53    fn is_settlement(&self, date: &NaiveDateTime) -> bool {
54        self.settlement_calendars
55            .as_ref()
56            .map_or(true, |v| !v.iter().any(|cal| cal.is_non_bus_day(date)))
57    }
58}
59
60impl CalendarAdjustment for UnionCal {}
61
62impl<T> PartialEq<T> for UnionCal
63where
64    T: DateRoll,
65{
66    fn eq(&self, other: &T) -> bool {
67        let cd1 = self
68            .cal_date_range(&ndt(1970, 1, 1), &ndt(2200, 12, 31))
69            .unwrap();
70        let cd2 = other
71            .cal_date_range(&ndt(1970, 1, 1), &ndt(2200, 12, 31))
72            .unwrap();
73        cd1.iter().zip(cd2.iter()).all(|(x, y)| {
74            self.is_bus_day(x) == other.is_bus_day(x)
75                && self.is_settlement(x) == other.is_settlement(y)
76        })
77    }
78}
79
80// UNIT TESTS
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    fn fixture_hol_cal() -> Cal {
86        let hols = vec![ndt(2015, 9, 5), ndt(2015, 9, 7)]; // Saturday and Monday
87        Cal::new(hols, vec![5, 6])
88    }
89
90    fn fixture_hol_cal2() -> Cal {
91        let hols = vec![
92            NaiveDateTime::parse_from_str("2015-09-08 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(),
93            NaiveDateTime::parse_from_str("2015-09-09 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(),
94        ];
95        Cal::new(hols, vec![5, 6])
96    }
97
98    #[test]
99    fn test_union_cal() {
100        let cal1 = fixture_hol_cal();
101        let cal2 = fixture_hol_cal2();
102        let ucal = UnionCal::new(vec![cal1, cal2], None);
103
104        let sat =
105            NaiveDateTime::parse_from_str("2015-09-05 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
106        let next = ucal.roll_forward_bus_day(&sat);
107        assert_eq!(
108            next,
109            NaiveDateTime::parse_from_str("2015-09-10 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap()
110        );
111    }
112
113    #[test]
114    fn test_union_cal_with_settle() {
115        let hols = vec![
116            NaiveDateTime::parse_from_str("2015-09-08 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(),
117            NaiveDateTime::parse_from_str("2015-09-09 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(),
118        ];
119        let scal = Cal::new(hols, vec![5, 6]);
120        let cal = Cal::new(vec![], vec![5, 6]);
121        let ucal = UnionCal::new(vec![cal], vec![scal].into());
122
123        let mon =
124            NaiveDateTime::parse_from_str("2015-09-08 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
125        let next = ucal.roll_forward_bus_day(&mon);
126        assert_eq!(
127            next,
128            NaiveDateTime::parse_from_str("2015-09-08 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap()
129        );
130    }
131
132    #[test]
133    fn test_cross_equality() {
134        let cal = fixture_hol_cal();
135        let ucal = UnionCal::new(vec![cal.clone()], None);
136        assert_eq!(cal, ucal);
137        assert_eq!(ucal, cal);
138
139        let ucals = UnionCal::new(vec![cal.clone()], vec![cal.clone()].into());
140        assert_ne!(cal, ucals);
141        assert_ne!(ucals, cal);
142
143        let cal2 = fixture_hol_cal2();
144        assert_ne!(cal2, ucal);
145        assert_ne!(ucal, cal2);
146    }
147}