rateslib/scheduling/calendars/
cal.rs

1use chrono::prelude::*;
2use chrono::Weekday;
3use indexmap::set::IndexSet;
4use pyo3::{pyclass, PyErr};
5use serde::{Deserialize, Serialize};
6use std::collections::HashSet;
7
8use crate::scheduling::calendars::named::{get_holidays_by_name, get_weekmask_by_name};
9use crate::scheduling::{ndt, CalendarAdjustment, DateRoll, NamedCal, UnionCal};
10
11/// A basic business day calendar containing holidays.
12#[pyclass(module = "rateslib.rs")]
13#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
14pub struct Cal {
15    /// A vector of specific dates that are defined as **non-business** days.
16    pub holidays: IndexSet<NaiveDateTime>,
17    /// A vector of days in the week that are defined as **non-business** days. E.g. `[5, 6]` for Saturday and Sunday.
18    pub week_mask: HashSet<Weekday>,
19    // pub(crate) meta: Vec<String>,
20}
21
22impl Cal {
23    /// Create a [`Cal`].
24    ///
25    /// # Examples
26    /// ```rust
27    /// # use rateslib::scheduling::{Cal, ndt, DateRoll};
28    /// let cal = Cal::new(vec![ndt(2017, 5, 1)], vec![5, 6]); // With May Bank Holiday
29    /// let spot = cal.add_bus_days(&ndt(2017, 4, 28), 2, true);
30    /// # let spot = spot.unwrap();
31    /// assert_eq!(ndt(2017, 5, 3), spot);
32    /// ```
33    pub fn new(
34        holidays: Vec<NaiveDateTime>,
35        week_mask: Vec<u8>,
36        // rules: Vec<&str>
37    ) -> Self {
38        Cal {
39            holidays: IndexSet::from_iter(holidays),
40            week_mask: HashSet::from_iter(
41                week_mask.into_iter().map(|v| Weekday::try_from(v).unwrap()),
42            ),
43            // meta: rules.into_iter().map(|x| x.to_string()).collect(),
44        }
45    }
46
47    /// Return a [`Cal`] specified by a pre-defined named identifier.
48    ///
49    /// For available 3-digit names see `named` module documentation.
50    ///
51    /// # Examples
52    ///
53    /// ```rust
54    /// # use rateslib::scheduling::Cal;
55    /// let ldn_cal = Cal::try_from_name("ldn").unwrap();
56    /// ```
57    pub fn try_from_name(name: &str) -> Result<Cal, PyErr> {
58        Ok(Cal::new(
59            get_holidays_by_name(name)?,
60            get_weekmask_by_name(name)?,
61            // get_rules_by_name(name)?
62        ))
63    }
64}
65
66impl DateRoll for Cal {
67    fn is_weekday(&self, date: &NaiveDateTime) -> bool {
68        !self.week_mask.contains(&date.weekday())
69    }
70
71    fn is_holiday(&self, date: &NaiveDateTime) -> bool {
72        self.holidays.contains(date)
73    }
74
75    fn is_settlement(&self, _date: &NaiveDateTime) -> bool {
76        true
77    }
78}
79
80impl CalendarAdjustment for Cal {}
81
82impl PartialEq<UnionCal> for Cal {
83    fn eq(&self, other: &UnionCal) -> bool {
84        let cd1 = self
85            .cal_date_range(&ndt(1970, 1, 1), &ndt(2200, 12, 31))
86            .unwrap();
87        let cd2 = other
88            .cal_date_range(&ndt(1970, 1, 1), &ndt(2200, 12, 31))
89            .unwrap();
90        cd1.iter().zip(cd2.iter()).all(|(x, y)| {
91            self.is_bus_day(x) == other.is_bus_day(x)
92                && self.is_settlement(x) == other.is_settlement(y)
93        })
94    }
95}
96
97impl PartialEq<NamedCal> for Cal {
98    fn eq(&self, other: &NamedCal) -> bool {
99        other.union_cal.eq(self)
100    }
101}
102
103// UNIT TESTS
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use crate::scheduling::Adjuster;
108
109    fn fixture_hol_cal() -> Cal {
110        let hols = vec![ndt(2015, 9, 5), ndt(2015, 9, 7)]; // Saturday and Monday
111        Cal::new(hols, vec![5, 6])
112    }
113
114    #[test]
115    fn test_is_holiday() {
116        let cal = fixture_hol_cal();
117        let hol =
118            NaiveDateTime::parse_from_str("2015-09-07 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
119        let no_hol =
120            NaiveDateTime::parse_from_str("2015-09-10 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
121        let saturday =
122            NaiveDateTime::parse_from_str("2024-01-06 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
123        assert!(cal.is_holiday(&hol)); // In hol list
124        assert!(!cal.is_holiday(&no_hol)); // Not in hol list
125        assert!(!cal.is_holiday(&saturday)); // Not in hol list
126    }
127
128    #[test]
129    fn test_is_weekday() {
130        let cal = fixture_hol_cal();
131        let hol =
132            NaiveDateTime::parse_from_str("2015-09-07 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
133        let no_hol =
134            NaiveDateTime::parse_from_str("2015-09-10 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
135        let saturday =
136            NaiveDateTime::parse_from_str("2024-01-06 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
137        let sunday =
138            NaiveDateTime::parse_from_str("2024-01-07 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
139        assert!(cal.is_weekday(&hol)); // Monday
140        assert!(cal.is_weekday(&no_hol)); //Thursday
141        assert!(!cal.is_weekday(&saturday)); // Saturday
142        assert!(!cal.is_weekday(&sunday)); // Sunday
143    }
144
145    #[test]
146    fn test_calendar_adjust() {
147        let cal = fixture_hol_cal();
148        let result = cal.adjust(&ndt(2015, 9, 5), &Adjuster::Following {});
149        assert_eq!(ndt(2015, 9, 8), result);
150    }
151
152    #[test]
153    fn test_calendar_adjusts() {
154        let cal = fixture_hol_cal();
155        let result = cal.adjusts(
156            &vec![ndt(2015, 9, 5), ndt(2015, 9, 6)],
157            &Adjuster::Following {},
158        );
159        assert_eq!(vec![ndt(2015, 9, 8), ndt(2015, 9, 8)], result);
160    }
161
162    // Pre defined named calendars
163
164    #[test]
165    fn test_get_cal() {
166        let result = Cal::try_from_name("bus").unwrap();
167        let expected = Cal::new(vec![], vec![5, 6]);
168        assert_eq!(result, expected);
169    }
170
171    #[test]
172    fn test_all() {
173        let cal = Cal::try_from_name("all").unwrap();
174        assert!(cal.is_bus_day(
175            &NaiveDateTime::parse_from_str("2024-11-11 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap()
176        ));
177    }
178
179    #[test]
180    fn test_nyc() {
181        let cal = Cal::try_from_name("nyc").unwrap();
182        assert!(cal.is_holiday(
183            &NaiveDateTime::parse_from_str("2024-11-11 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap()
184        ));
185    }
186
187    #[test]
188    fn test_tgt() {
189        let cal = Cal::try_from_name("tgt").unwrap();
190        assert!(cal.is_holiday(
191            &NaiveDateTime::parse_from_str("2024-05-01 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap()
192        ));
193    }
194
195    #[test]
196    fn test_ldn() {
197        let cal = Cal::try_from_name("ldn").unwrap();
198        assert!(cal.is_holiday(
199            &NaiveDateTime::parse_from_str("2024-08-26 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap()
200        ));
201    }
202
203    #[test]
204    fn test_stk() {
205        let cal = Cal::try_from_name("stk").unwrap();
206        assert!(cal.is_holiday(
207            &NaiveDateTime::parse_from_str("2024-06-06 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap()
208        ));
209    }
210
211    #[test]
212    fn test_osl() {
213        let cal = Cal::try_from_name("osl").unwrap();
214        assert!(cal.is_holiday(
215            &NaiveDateTime::parse_from_str("2024-05-17 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap()
216        ));
217    }
218
219    #[test]
220    fn test_zur() {
221        let cal = Cal::try_from_name("zur").unwrap();
222        assert!(cal.is_holiday(
223            &NaiveDateTime::parse_from_str("2024-08-01 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap()
224        ));
225    }
226
227    #[test]
228    fn test_tro() {
229        let cal = Cal::try_from_name("tro").unwrap();
230        assert!(cal.is_holiday(
231            &NaiveDateTime::parse_from_str("2024-09-30 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap()
232        ));
233    }
234
235    #[test]
236    fn test_tyo() {
237        let cal = Cal::try_from_name("tyo").unwrap();
238        assert!(cal.is_holiday(
239            &NaiveDateTime::parse_from_str("2024-1-3 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap()
240        ));
241    }
242
243    #[test]
244    fn test_fed() {
245        let cal = Cal::try_from_name("fed").unwrap();
246        assert!(cal.is_holiday(
247            &NaiveDateTime::parse_from_str("2024-11-11 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap()
248        ));
249    }
250
251    #[test]
252    fn test_get_calendar_error() {
253        match Cal::try_from_name("badname") {
254            Ok(_) => assert!(false),
255            Err(_) => assert!(true),
256        }
257    }
258
259    #[test]
260    fn test_syd() {
261        let cal = Cal::try_from_name("syd").unwrap();
262        assert!(cal.is_holiday(
263            &NaiveDateTime::parse_from_str("2022-09-22 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap()
264        ));
265    }
266
267    #[test]
268    fn test_wlg() {
269        let cal = Cal::try_from_name("wlg").unwrap();
270        assert!(cal.is_holiday(
271            &NaiveDateTime::parse_from_str("2034-07-07 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap()
272        ));
273    }
274
275    #[test]
276    fn test_mum() {
277        let cal = Cal::try_from_name("mum").unwrap();
278        assert!(cal.is_holiday(
279            &NaiveDateTime::parse_from_str("2025-01-26 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap()
280        ));
281    }
282}