rateslib/scheduling/calendars/
named_cal.rs

1use chrono::prelude::*;
2use pyo3::exceptions::PyValueError;
3use pyo3::{pyclass, PyErr};
4use serde::{Deserialize, Serialize};
5
6use crate::scheduling::{Cal, CalendarAdjustment, DateRoll, UnionCal};
7
8/// A wrapper for a UnionCal struct specified by a string representation.
9#[pyclass(module = "rateslib.rs")]
10#[derive(Clone, Debug, Serialize, Deserialize)]
11#[serde(from = "NamedCalDataModel")]
12pub struct NamedCal {
13    pub name: String,
14    #[serde(skip)]
15    pub union_cal: UnionCal,
16}
17
18#[derive(Deserialize)]
19struct NamedCalDataModel {
20    name: String,
21}
22
23impl std::convert::From<NamedCalDataModel> for NamedCal {
24    fn from(model: NamedCalDataModel) -> Self {
25        Self::try_new(&model.name).expect("NamedCal data model contains bad data.")
26    }
27}
28
29impl NamedCal {
30    /// Create a new [`NamedCal`].
31    ///
32    /// # Notes
33    /// `name` must be a string that contains pre-defined calendars separated by commas, additionally
34    /// separating business day calendars with associated settlement calendars by a pipe operator.
35    ///
36    /// # Examples
37    /// ```rust
38    /// # use rateslib::scheduling::{NamedCal};
39    /// let named_cal = NamedCal::try_new("ldn,tgt|fed");
40    /// # let named_cal = named_cal.unwrap();
41    /// assert_eq!(named_cal.union_cal.calendars.len(), 2);
42    /// assert!(named_cal.union_cal.settlement_calendars.is_some());
43    /// ```
44    pub fn try_new(name: &str) -> Result<Self, PyErr> {
45        let name_ = name.to_lowercase();
46        let parts: Vec<&str> = name_.split("|").collect();
47        if parts.len() > 2 {
48            Err(PyValueError::new_err(
49                "Cannot use more than one pipe ('|') operator in `name`.",
50            ))
51        } else if parts.len() == 1 {
52            let cals: Vec<Cal> = parse_cals(parts[0])?;
53            Ok(Self {
54                name: name_,
55                union_cal: UnionCal {
56                    calendars: cals,
57                    settlement_calendars: None,
58                },
59            })
60        } else {
61            let cals: Vec<Cal> = parse_cals(parts[0])?;
62            let settle_cals: Vec<Cal> = parse_cals(parts[1])?;
63            Ok(Self {
64                name: name_,
65                union_cal: UnionCal {
66                    calendars: cals,
67                    settlement_calendars: Some(settle_cals),
68                },
69            })
70        }
71    }
72}
73
74impl DateRoll for NamedCal {
75    fn is_weekday(&self, date: &NaiveDateTime) -> bool {
76        self.union_cal.is_weekday(date)
77    }
78
79    fn is_holiday(&self, date: &NaiveDateTime) -> bool {
80        self.union_cal.is_holiday(date)
81    }
82
83    fn is_settlement(&self, date: &NaiveDateTime) -> bool {
84        self.union_cal.is_settlement(date)
85    }
86}
87
88impl CalendarAdjustment for NamedCal {}
89
90fn parse_cals(name: &str) -> Result<Vec<Cal>, PyErr> {
91    let mut cals: Vec<Cal> = Vec::new();
92    for cal in name.split(",") {
93        cals.push(Cal::try_from_name(cal)?)
94    }
95    Ok(cals)
96}
97
98impl<T> PartialEq<T> for NamedCal
99where
100    T: DateRoll,
101{
102    fn eq(&self, other: &T) -> bool {
103        self.union_cal.eq(other)
104    }
105}
106
107// UNIT TESTS
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use crate::scheduling::ndt;
112
113    #[test]
114    fn test_named_cal() {
115        let ncal = NamedCal::try_new("tgt,nyc").unwrap();
116
117        assert!(ncal.is_non_bus_day(&ndt(1970, 2, 16))); // NYC holiday
118        assert!(ncal.is_non_bus_day(&ndt(1970, 5, 1))); // TGT holiday
119        assert!(ncal.is_bus_day(&ndt(1970, 2, 17)));
120    }
121
122    #[test]
123    fn test_named_cal_pipe() {
124        let ncal = NamedCal::try_new("tgt,nyc|ldn").unwrap();
125
126        assert!(ncal.is_non_bus_day(&ndt(1970, 2, 16))); // NYC holiday
127        assert!(ncal.is_non_bus_day(&ndt(1970, 5, 1))); // TGT holiday
128        assert!(ncal.is_bus_day(&ndt(1970, 2, 17)));
129
130        assert!(!ncal.is_settlement(&ndt(1970, 5, 4))); // LDN holiday
131        assert!(ncal.is_settlement(&ndt(1970, 5, 1))); // not LDN holiday
132    }
133
134    #[test]
135    fn test_named_cal_error() {
136        let ncal = NamedCal::try_new("tgt,nyc|ldn|");
137        assert!(ncal.is_err());
138
139        let ncal = NamedCal::try_new("");
140        assert!(ncal.is_err());
141    }
142}