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