rateslib/scheduling/py/
frequency.rs

1use crate::json::{DeserializedObj, JSON};
2use crate::scheduling::calendars::Calendar;
3use crate::scheduling::frequency::{Frequency, RollDay, Scheduling};
4
5use chrono::prelude::*;
6use pyo3::exceptions::PyValueError;
7use pyo3::prelude::*;
8use pyo3::types::PyTuple;
9
10enum FrequencyNewArgs {
11    CalDays(i32),
12    BusDays(i32, Calendar),
13    Months(i32, Option<RollDay>),
14    Zero(),
15}
16
17impl<'py> IntoPyObject<'py> for FrequencyNewArgs {
18    type Target = PyTuple;
19    type Output = Bound<'py, Self::Target>;
20    type Error = std::convert::Infallible;
21
22    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
23        match self {
24            FrequencyNewArgs::CalDays(x) => Ok((x,).into_pyobject(py).unwrap()),
25            FrequencyNewArgs::BusDays(x, y) => Ok((x, y).into_pyobject(py).unwrap()),
26            FrequencyNewArgs::Months(x, y) => Ok((x, y).into_pyobject(py).unwrap()),
27            FrequencyNewArgs::Zero() => Ok(PyTuple::empty(py)),
28        }
29    }
30}
31
32impl<'py> FromPyObject<'py> for FrequencyNewArgs {
33    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
34        let ext: PyResult<(i32,)> = ob.extract();
35        if ext.is_ok() {
36            let (x,) = ext.unwrap();
37            return Ok(Self::CalDays(x));
38        }
39        let ext: PyResult<(i32, Calendar)> = ob.extract();
40        if ext.is_ok() {
41            let (x, y) = ext.unwrap();
42            return Ok(Self::BusDays(x, y));
43        }
44        let ext: PyResult<(i32, Option<RollDay>)> = ob.extract();
45        if ext.is_ok() {
46            let (x, y) = ext.unwrap();
47            Ok(Self::Months(x, y))
48        } else {
49            // must be empty tuple args
50            Ok(Self::Zero())
51        }
52    }
53}
54
55#[pymethods]
56impl Frequency {
57    /// Return the next unadjusted date under the schedule frequency.
58    ///
59    /// Parameters
60    /// ----------
61    /// date: datetime
62    ///     Any unchecked date, which may or may not align with the `Frequency`.
63    ///
64    /// Returns
65    /// -------
66    /// datetime
67    #[pyo3(name = "next")]
68    fn next_py(&self, date: NaiveDateTime) -> NaiveDateTime {
69        self.next(&date)
70    }
71
72    /// Return an average number of coupons per annum measured over 50 years.
73    ///
74    /// Returns
75    /// -------
76    /// float
77    #[pyo3(name = "periods_per_annum")]
78    fn periods_per_annum_py(&self) -> f64 {
79        self.periods_per_annum()
80    }
81
82    /// Return the next unadjusted date under the schedule frequency.
83    ///
84    /// Parameters
85    /// ----------
86    /// udate: datetime
87    ///     The unadjusted start date of the frequency period. If this is not a valid unadjusted
88    ///     date aligned with the Frequency then it will raise.
89    ///
90    /// Returns
91    /// -------
92    /// datetime
93    #[pyo3(name = "unext")]
94    fn unext_py(&self, udate: NaiveDateTime) -> PyResult<NaiveDateTime> {
95        self.try_unext(&udate)
96    }
97
98    /// Return the previous unadjusted date under the schedule frequency.
99    ///
100    /// Parameters
101    /// ----------
102    /// date: datetime
103    ///     Any unchecked date, which may or may not align with the `Frequency`.
104    ///
105    /// Returns
106    /// -------
107    /// datetime
108    #[pyo3(name = "previous")]
109    fn previous_py(&self, date: NaiveDateTime) -> NaiveDateTime {
110        self.previous(&date)
111    }
112
113    /// Return the previous unadjusted date under the schedule frequency.
114    ///
115    /// Parameters
116    /// ----------
117    /// udate: datetime
118    ///     The unadjusted end date of the frequency period. If this is not a valid unadjusted
119    ///     date aligned with the Frequency then it will raise.
120    ///
121    /// Returns
122    /// -------
123    /// datetime
124    #[pyo3(name = "uprevious")]
125    fn uprevious_py(&self, udate: NaiveDateTime) -> PyResult<NaiveDateTime> {
126        self.try_uprevious(&udate)
127    }
128
129    /// Return a list of unadjusted regular schedule dates.
130    ///
131    /// Parameters
132    /// ----------
133    /// ueffective: datetime
134    ///     The unadjusted effective date of the schedule. If this is not a valid unadjusted
135    ///     date aligned with the Frequency then it will raise.
136    /// utermination: datetime
137    ///     The unadjusted termination date of the frequency period. If this is not a valid
138    ///     unadjusted date aligned with the Frequency then it will raise.
139    ///
140    /// Returns
141    /// -------
142    /// list[datetime]
143    #[pyo3(name = "uregular")]
144    fn uregular_py(
145        &self,
146        ueffective: NaiveDateTime,
147        utermination: NaiveDateTime,
148    ) -> PyResult<Vec<NaiveDateTime>> {
149        self.try_uregular(&ueffective, &utermination)
150    }
151
152    /// Infer an unadjusted stub date from given schedule endpoints.
153    ///
154    /// Parameters
155    /// ----------
156    /// ueffective: datetime
157    ///     The unadjusted effective date of the schedule.
158    /// utermination: datetime
159    ///     The unadjusted termination date of the frequency period. If this is not a valid
160    ///     unadjusted date aligned with the Frequency then it will raise.
161    /// short: bool
162    ///     Whether to infer a short or a long stub.
163    /// front: bool
164    ///     Whether to infer a front or a back stub.
165    ///
166    /// Returns
167    /// -------
168    /// datetime or None
169    ///
170    /// Notes
171    /// -----
172    /// This function will return `None` if the dates define a regular schedule and no stub is
173    /// required.
174    #[pyo3(name = "infer_ustub")]
175    fn infer_ustub_py(
176        &self,
177        ueffective: NaiveDateTime,
178        utermination: NaiveDateTime,
179        short: bool,
180        front: bool,
181    ) -> PyResult<Option<NaiveDateTime>> {
182        if front {
183            self.try_infer_ufront_stub(&ueffective, &utermination, short)
184        } else {
185            self.try_infer_uback_stub(&ueffective, &utermination, short)
186        }
187    }
188
189    /// Check whether unadjusted dates define a stub period.
190    ///
191    /// Parameters
192    /// ----------
193    /// ustart: datetime
194    ///     The unadjusted start date of the period.
195    /// uend: datetime
196    ///     The unadjusted end date of the period.
197    /// front: bool
198    ///     Test for either a front or a back stub.
199    ///
200    /// Returns
201    /// -------
202    /// bool
203    #[pyo3(name = "is_stub")]
204    fn is_stub_py(&self, ustart: NaiveDateTime, uend: NaiveDateTime, front: bool) -> bool {
205        if front {
206            self.is_front_stub(&ustart, &uend)
207        } else {
208            self.is_back_stub(&ustart, &uend)
209        }
210    }
211
212    /// Return a string representation of the Frequency.
213    ///
214    /// Returns
215    /// -------
216    /// str
217    #[pyo3(name = "string")]
218    fn string_py(&self) -> PyResult<String> {
219        match self {
220            Frequency::Zero {} => Ok("Z".to_string()),
221            Frequency::CalDays { number: n } => Ok(format!("{n}D")),
222            Frequency::BusDays {
223                number: n,
224                calendar: _,
225            } => Ok(format!("{n}B")),
226            Frequency::Months { number: 1, roll: _ } => Ok(format!("M")),
227            Frequency::Months { number: 2, roll: _ } => Ok(format!("B")),
228            Frequency::Months { number: 3, roll: _ } => Ok(format!("Q")),
229            Frequency::Months { number: 4, roll: _ } => Ok(format!("T")),
230            Frequency::Months { number: 6, roll: _ } => Ok(format!("S")),
231            Frequency::Months {
232                number: 12,
233                roll: _,
234            } => Ok(format!("A")),
235            _ => Err(PyValueError::new_err(
236                "No recognisable string representation for Frequency.",
237            )),
238        }
239    }
240
241    fn __str__(&self) -> String {
242        match self {
243            Frequency::Zero {} => "Z".to_string(),
244            Frequency::CalDays { number: n } => format!("{n}D"),
245            Frequency::BusDays {
246                number: n,
247                calendar: _,
248            } => format!("{n}B"),
249            Frequency::Months { number: n, roll: r } => {
250                let x = match r {
251                    Some(v) => v.__str__(),
252                    None => "none".to_string(),
253                };
254                format!("{n}M (roll: {x})")
255            }
256        }
257    }
258
259    fn __getnewargs__(&self) -> FrequencyNewArgs {
260        match self {
261            Frequency::BusDays {
262                number: n,
263                calendar: c,
264            } => FrequencyNewArgs::BusDays(*n, c.clone()),
265            Frequency::CalDays { number: n } => FrequencyNewArgs::CalDays(*n),
266            Frequency::Months { number: n, roll: r } => FrequencyNewArgs::Months(*n, *r),
267            Frequency::Zero {} => FrequencyNewArgs::Zero(),
268        }
269    }
270
271    #[new]
272    fn new_py(args: FrequencyNewArgs) -> Frequency {
273        match args {
274            FrequencyNewArgs::BusDays(n, c) => Frequency::BusDays {
275                number: n,
276                calendar: c,
277            },
278            FrequencyNewArgs::CalDays(n) => Frequency::CalDays { number: n },
279            FrequencyNewArgs::Months(n, r) => Frequency::Months { number: n, roll: r },
280            FrequencyNewArgs::Zero() => Frequency::Zero {},
281        }
282    }
283
284    fn __repr__(&self) -> String {
285        match self {
286            Frequency::Zero {} => format!("<rl.Frequency.Zero at {:p}>", self),
287            Frequency::CalDays { number: n } => {
288                format!("<rl.Frequency.CalDays({}) at {:p}>", n, self)
289            }
290            Frequency::BusDays {
291                number: n,
292                calendar: _,
293            } => format!("<rl.Frequency.BusDays({}, ...) at {:p}>", n, self),
294            Frequency::Months { number: n, roll: r } => match r {
295                Some(val) => format!("<rl.Frequency.Months({}, {:?}) at {:p}>", n, val, self),
296                None => format!("<rl.Frequency.Months({}, None) at {:p}>", n, self),
297            },
298        }
299    }
300
301    /// Return a JSON representation of the object.
302    ///
303    /// Returns
304    /// -------
305    /// str
306    #[pyo3(name = "to_json")]
307    fn to_json_py(&self) -> PyResult<String> {
308        match DeserializedObj::Frequency(self.clone()).to_json() {
309            Ok(v) => Ok(v),
310            Err(_) => Err(PyValueError::new_err(
311                "Failed to serialize `Frequency` to JSON.",
312            )),
313        }
314    }
315}