rateslib/scheduling/py/
calendar.rs

1//! Wrapper module to export to Python using pyo3 bindings.
2
3use crate::json::json_py::DeserializedObj;
4use crate::json::JSON;
5use crate::scheduling::py::adjuster::get_roll_adjuster_from_str;
6use crate::scheduling::{
7    Adjuster, Adjustment, Cal, Calendar, CalendarAdjustment, DateRoll, NamedCal, PyAdjuster,
8    RollDay, UnionCal,
9};
10use chrono::NaiveDateTime;
11use indexmap::set::IndexSet;
12use pyo3::exceptions::PyValueError;
13use pyo3::prelude::*;
14use pyo3::types::PyType;
15use std::collections::HashSet;
16
17#[pymethods]
18impl Cal {
19    /// Create a new *Cal* object.
20    ///
21    /// Parameters
22    /// ----------
23    /// holidays: list[datetime]
24    ///     List of datetimes as the specific holiday days.
25    /// week_mask: list[int],
26    ///     List of integers defining the weekends, [5, 6] for Saturday and Sunday.
27    #[new]
28    fn new_py(holidays: Vec<NaiveDateTime>, week_mask: Vec<u8>) -> PyResult<Self> {
29        Ok(Cal::new(holidays, week_mask))
30    }
31
32    /// Create a new *Cal* object from simple string name.
33    /// Parameters
34    /// ----------
35    /// name: str
36    ///     The 3-digit name of the calendar to load. Must be pre-defined in the Rust core code.
37    ///
38    /// Returns
39    /// -------
40    /// Cal
41    #[classmethod]
42    #[pyo3(name = "from_name")]
43    fn from_name_py(_cls: &Bound<'_, PyType>, name: String) -> PyResult<Self> {
44        Cal::try_from_name(&name)
45    }
46
47    /// A list of specifically provided non-business days.
48    #[getter]
49    fn holidays(&self) -> PyResult<Vec<NaiveDateTime>> {
50        Ok(self.holidays.clone().into_iter().collect())
51    }
52
53    /// A list of days in the week defined as weekends.
54    #[getter]
55    fn week_mask(&self) -> PyResult<HashSet<u8>> {
56        Ok(HashSet::from_iter(
57            self.week_mask
58                .clone()
59                .into_iter()
60                .map(|x| x.num_days_from_monday() as u8),
61        ))
62    }
63
64    // #[getter]
65    // fn rules(&self) -> PyResult<String> {
66    //     Ok(self.meta.join(",\n"))
67    // }
68
69    /// Return whether the `date` is a business day.
70    ///
71    /// Parameters
72    /// ----------
73    /// date: datetime
74    ///     Date to test
75    ///
76    /// Returns
77    /// -------
78    /// bool
79    #[pyo3(name = "is_bus_day")]
80    fn is_bus_day_py(&self, date: NaiveDateTime) -> bool {
81        self.is_bus_day(&date)
82    }
83
84    /// Return whether the `date` is **not** a business day.
85    ///
86    /// Parameters
87    /// ----------
88    /// date: datetime
89    ///     Date to test
90    ///
91    /// Returns
92    /// -------
93    /// bool
94    #[pyo3(name = "is_non_bus_day")]
95    fn is_non_bus_day_py(&self, date: NaiveDateTime) -> bool {
96        self.is_non_bus_day(&date)
97    }
98
99    /// Return whether the `date` is a business day of an associated settlement calendar.
100    ///
101    /// .. note::
102    ///
103    ///    *Cal* objects will always return *True*, since they do not contain any
104    ///    associated settlement calendars. This method is provided only for API consistency.
105    ///
106    /// Parameters
107    /// ----------
108    /// date: datetime
109    ///     Date to test
110    ///
111    /// Returns
112    /// -------
113    /// bool
114    #[pyo3(name = "is_settlement")]
115    fn is_settlement_py(&self, date: NaiveDateTime) -> bool {
116        self.is_settlement(&date)
117    }
118
119    /// Return a date separated by calendar days from input date, and rolled with a modifier.
120    ///
121    /// Parameters
122    /// ----------
123    /// date: datetime
124    ///     The original business date. Raise if a non-business date is given.
125    /// days: int
126    ///     The number of calendar days to add.
127    /// adjuster: Adjuster
128    ///     The date adjustment rule to use on the unadjusted result.
129    ///
130    /// Returns
131    /// -------
132    /// datetime
133    #[pyo3(name = "add_cal_days")]
134    fn add_cal_days_py(
135        &self,
136        date: NaiveDateTime,
137        days: i32,
138        adjuster: PyAdjuster,
139    ) -> PyResult<NaiveDateTime> {
140        Ok(self.add_cal_days(&date, days, &adjuster.into()))
141    }
142
143    /// Return a business date separated by `days` from an input business `date`.
144    ///
145    /// Parameters
146    /// ----------
147    /// date: datetime
148    ///     The original business date. *Raises* if a non-business date is given.
149    /// days: int
150    ///     Number of business days to add.
151    /// settlement: bool
152    ///     Enforce an associated settlement calendar, if *True* and if one exists.
153    ///
154    /// Returns
155    /// -------
156    /// datetime
157    ///
158    /// Notes
159    /// -----
160    /// If adding negative number of business days a failing
161    /// settlement will be rolled **backwards**, whilst adding a
162    /// positive number of days will roll a failing settlement day **forwards**,
163    /// if ``settlement`` is *True*.
164    ///
165    /// .. seealso::
166    ///
167    ///    :meth:`~rateslib.scheduling.Cal.lag_bus_days`: Add business days to inputs which are potentially
168    ///    non-business dates.
169    #[pyo3(name = "add_bus_days")]
170    fn add_bus_days_py(
171        &self,
172        date: NaiveDateTime,
173        days: i32,
174        settlement: bool,
175    ) -> PyResult<NaiveDateTime> {
176        self.add_bus_days(&date, days, settlement)
177    }
178
179    /// Return a date separated by months from an input date, and rolled with a modifier.
180    ///
181    /// Parameters
182    /// ----------
183    /// date: datetime
184    ///     The original date to adjust.
185    /// months: int
186    ///     The number of months to add.
187    /// adjuster: Adjuster
188    ///     The date adjustment rule to apply to the unadjusted result.
189    /// roll: RollDay, optional
190    ///     The day of the month to adjust to. If not given adopts the calendar day of ``date``.
191    ///
192    /// Returns
193    /// -------
194    /// datetime
195    #[pyo3(name = "add_months")]
196    fn add_months_py(
197        &self,
198        date: NaiveDateTime,
199        months: i32,
200        adjuster: PyAdjuster,
201        roll: Option<RollDay>,
202    ) -> NaiveDateTime {
203        let roll_ = match roll {
204            Some(val) => val,
205            None => RollDay::vec_from(&vec![date])[0],
206        };
207        let adjuster: Adjuster = adjuster.into();
208        adjuster.adjust(&roll_.uadd(&date, months), self)
209    }
210
211    /// Roll a date under a simplified adjustment rule.
212    ///
213    /// Parameters
214    /// -----------
215    /// date: datetime
216    ///     The date to adjust.
217    /// modifier: str in {"F", "P", "MF", "MP", "Act"}
218    ///     The simplified date adjustment rule to apply
219    /// settlement: bool
220    ///     Whether to adhere to an additional settlement calendar.
221    ///
222    /// Returns
223    /// -------
224    /// datetime
225    #[pyo3(name = "roll")]
226    fn roll_py(
227        &self,
228        date: NaiveDateTime,
229        modifier: &str,
230        settlement: bool,
231    ) -> PyResult<NaiveDateTime> {
232        let adjuster = get_roll_adjuster_from_str((&modifier.to_lowercase(), settlement))?;
233        Ok(self.adjust(&date, &adjuster))
234    }
235
236    /// Adjust a date under a date adjustment rule.
237    ///
238    /// Parameters
239    /// -----------
240    /// date: datetime
241    ///     The date to adjust.
242    /// adjuster: Adjuster
243    ///     The date adjustment rule to apply.
244    ///
245    /// Returns
246    /// -------
247    /// datetime
248    #[pyo3(name = "adjust")]
249    fn adjust_py(&self, date: NaiveDateTime, adjuster: PyAdjuster) -> PyResult<NaiveDateTime> {
250        Ok(self.adjust(&date, &adjuster.into()))
251    }
252
253    /// Adjust a list of dates under a date adjustment rule.
254    ///
255    /// Parameters
256    /// -----------
257    /// dates: list[datetime]
258    ///     The dates to adjust.
259    /// adjuster: Adjuster
260    ///     The date adjustment rule to apply.
261    ///
262    /// Returns
263    /// -------
264    /// list[datetime]
265    #[pyo3(name = "adjusts")]
266    fn adjusts_py(
267        &self,
268        dates: Vec<NaiveDateTime>,
269        adjuster: PyAdjuster,
270    ) -> PyResult<Vec<NaiveDateTime>> {
271        Ok(self.adjusts(&dates, &adjuster.into()))
272    }
273
274    /// Adjust a date by a number of business days, under lag rules.
275    ///
276    /// Parameters
277    /// -----------
278    /// date: datetime
279    ///     The date to adjust.
280    /// days: int
281    ///     Number of business days to add.
282    /// settlement: bool
283    ///     Whether to enforce settlement against an associated settlement calendar.
284    ///
285    /// Returns
286    /// --------
287    /// datetime
288    ///
289    /// Notes
290    /// -----
291    /// ``lag_bus_days`` and ``add_bus_days`` will return the same value if the input date is a business
292    /// date. If not a business date, ``add_bus_days`` will raise, while ``lag_bus_days`` will follow
293    /// lag rules. ``lag_bus_days`` should be used when the input date cannot be guaranteed to be a
294    /// business date.
295    ///
296    /// **Lag rules** define the addition of business days to a date that is a non-business date:
297    ///
298    /// - Adding zero days will roll the date **forwards** to the next available business day.
299    /// - Adding one day will roll the date **forwards** to the next available business day.
300    /// - Subtracting one day will roll the date **backwards** to the previous available business day.
301    ///
302    /// Adding (or subtracting) further business days adopts the
303    /// :meth:`~rateslib.scheduling.Cal.add_bus_days` approach with a valid result.
304    #[pyo3(name = "lag_bus_days")]
305    fn lag_bus_days_py(&self, date: NaiveDateTime, days: i32, settlement: bool) -> NaiveDateTime {
306        self.lag_bus_days(&date, days, settlement)
307    }
308
309    /// Return a list of business dates in a range.
310    ///
311    /// Parameters
312    /// ----------
313    /// start: datetime
314    ///     The start date of the range, inclusive.
315    /// end: datetime
316    ///     The end date of the range, inclusive.
317    ///
318    /// Returns
319    /// -------
320    /// list[datetime]
321    #[pyo3(name = "bus_date_range")]
322    fn bus_date_range_py(
323        &self,
324        start: NaiveDateTime,
325        end: NaiveDateTime,
326    ) -> PyResult<Vec<NaiveDateTime>> {
327        self.bus_date_range(&start, &end)
328    }
329
330    /// Return a list of calendar dates within a range.
331    ///
332    /// Parameters
333    /// -----------
334    /// start: datetime
335    ///     The start date of the range, inclusive.
336    /// end: datetime
337    ///     The end date of the range, inclusive,
338    ///
339    /// Returns
340    /// --------
341    /// list[datetime]
342    #[pyo3(name = "cal_date_range")]
343    fn cal_date_range_py(
344        &self,
345        start: NaiveDateTime,
346        end: NaiveDateTime,
347    ) -> PyResult<Vec<NaiveDateTime>> {
348        self.cal_date_range(&start, &end)
349    }
350
351    // Pickling
352    fn __getnewargs__(&self) -> PyResult<(Vec<NaiveDateTime>, Vec<u8>)> {
353        Ok((
354            self.clone().holidays.into_iter().collect(),
355            self.clone()
356                .week_mask
357                .into_iter()
358                .map(|x| x.num_days_from_monday() as u8)
359                .collect(),
360        ))
361    }
362
363    // JSON
364    /// Return a JSON representation of the object.
365    ///
366    /// Returns
367    /// -------
368    /// str
369    #[pyo3(name = "to_json")]
370    fn to_json_py(&self) -> PyResult<String> {
371        match DeserializedObj::Cal(self.clone()).to_json() {
372            Ok(v) => Ok(v),
373            Err(_) => Err(PyValueError::new_err("Failed to serialize `Cal` to JSON.")),
374        }
375    }
376
377    // Equality
378    fn __eq__(&self, other: Calendar) -> bool {
379        match other {
380            Calendar::UnionCal(c) => *self == c,
381            Calendar::Cal(c) => *self == c,
382            Calendar::NamedCal(c) => *self == c,
383        }
384    }
385}
386
387#[pymethods]
388impl UnionCal {
389    #[new]
390    #[pyo3(signature = (calendars, settlement_calendars=None))]
391    fn new_py(calendars: Vec<Cal>, settlement_calendars: Option<Vec<Cal>>) -> PyResult<Self> {
392        Ok(UnionCal::new(calendars, settlement_calendars))
393    }
394
395    /// A list of specifically provided non-business days.
396    #[getter]
397    fn holidays(&self) -> PyResult<Vec<NaiveDateTime>> {
398        let mut set = self.calendars.iter().fold(IndexSet::new(), |acc, x| {
399            IndexSet::from_iter(acc.union(&x.holidays).cloned())
400        });
401        set.sort();
402        Ok(Vec::from_iter(set))
403    }
404
405    /// A list of days in the week defined as weekends.
406    #[getter]
407    fn week_mask(&self) -> PyResult<HashSet<u8>> {
408        let mut s: HashSet<u8> = HashSet::new();
409        for cal in &self.calendars {
410            let ns = cal.week_mask()?;
411            s.extend(&ns);
412        }
413        Ok(s)
414    }
415
416    /// A list of :class:`~rateslib.scheduling.Cal` objects defining **business days**.
417    #[getter]
418    fn calendars(&self) -> Vec<Cal> {
419        self.calendars.clone()
420    }
421
422    /// A list of :class:`~rateslib.scheduling.Cal` objects defining **settleable days**.
423    #[getter]
424    fn settlement_calendars(&self) -> Option<Vec<Cal>> {
425        self.settlement_calendars.clone()
426    }
427
428    /// Return whether the `date` is a business day.
429    ///
430    /// See :meth:`Cal.is_bus_day <rateslib.scheduling.Cal.is_bus_day>`.
431    #[pyo3(name = "is_bus_day")]
432    fn is_bus_day_py(&self, date: NaiveDateTime) -> bool {
433        self.is_bus_day(&date)
434    }
435
436    /// Return whether the `date` is **not** a business day.
437    ///
438    /// See :meth:`Cal.is_non_bus_day <rateslib.scheduling.Cal.is_non_bus_day>`.
439    #[pyo3(name = "is_non_bus_day")]
440    fn is_non_bus_day_py(&self, date: NaiveDateTime) -> bool {
441        self.is_non_bus_day(&date)
442    }
443
444    /// Return whether the `date` is a business day in an associated settlement calendar.
445    ///
446    /// If no such associated settlement calendar exists this will return *True*.
447    ///
448    /// See :meth:`Cal.is_settlement <rateslib.scheduling.Cal.is_settlement>`.
449    #[pyo3(name = "is_settlement")]
450    fn is_settlement_py(&self, date: NaiveDateTime) -> bool {
451        self.is_settlement(&date)
452    }
453
454    /// Return a date separated by calendar days from input date, and rolled with a modifier.
455    ///
456    /// See :meth:`Cal.add_cal_days <rateslib.scheduling.Cal.add_cal_days>`.
457    #[pyo3(name = "add_cal_days")]
458    fn add_cal_days_py(
459        &self,
460        date: NaiveDateTime,
461        days: i32,
462        adjuster: PyAdjuster,
463    ) -> PyResult<NaiveDateTime> {
464        Ok(self.add_cal_days(&date, days, &adjuster.into()))
465    }
466
467    /// Return a business date separated by `days` from an input business `date`.
468    ///
469    /// See :meth:`Cal.add_bus_days <rateslib.scheduling.Cal.add_bus_days>`.
470    #[pyo3(name = "add_bus_days")]
471    fn add_bus_days_py(
472        &self,
473        date: NaiveDateTime,
474        days: i32,
475        settlement: bool,
476    ) -> PyResult<NaiveDateTime> {
477        self.add_bus_days(&date, days, settlement)
478    }
479
480    /// Return a date separated by months from an input date, and rolled with a modifier.
481    ///
482    /// See :meth:`Cal.add_months <rateslib.scheduling.Cal.add_months>`.
483    #[pyo3(name = "add_months")]
484    fn add_months_py(
485        &self,
486        date: NaiveDateTime,
487        months: i32,
488        adjuster: PyAdjuster,
489        roll: Option<RollDay>,
490    ) -> NaiveDateTime {
491        let roll_ = match roll {
492            Some(val) => val,
493            None => RollDay::vec_from(&vec![date])[0],
494        };
495        let adjuster: Adjuster = adjuster.into();
496        adjuster.adjust(&roll_.uadd(&date, months), self)
497    }
498
499    /// Adjust a non-business date to a business date under a specific modification rule.
500    ///
501    /// See :meth:`Cal.adjust <rateslib.scheduling.Cal.adjust>`.
502    #[pyo3(name = "adjust")]
503    fn adjust_py(&self, date: NaiveDateTime, adjuster: PyAdjuster) -> PyResult<NaiveDateTime> {
504        Ok(self.adjust(&date, &adjuster.into()))
505    }
506
507    /// Adjust a list of dates under a date adjustment rule.
508    ///
509    /// See :meth:`Cal.adjusts <rateslib.scheduling.Cal.adjusts>`.
510    #[pyo3(name = "adjusts")]
511    fn adjusts_py(
512        &self,
513        dates: Vec<NaiveDateTime>,
514        adjuster: PyAdjuster,
515    ) -> PyResult<Vec<NaiveDateTime>> {
516        Ok(self.adjusts(&dates, &adjuster.into()))
517    }
518
519    /// Roll a date under a simplified adjustment rule.
520    ///
521    /// See :meth:`Cal.roll <rateslib.scheduling.Cal.roll>`.
522    #[pyo3(name = "roll")]
523    fn roll_py(
524        &self,
525        date: NaiveDateTime,
526        modifier: &str,
527        settlement: bool,
528    ) -> PyResult<NaiveDateTime> {
529        let adjuster = get_roll_adjuster_from_str((&modifier.to_lowercase(), settlement))?;
530        Ok(self.adjust(&date, &adjuster))
531    }
532
533    /// Adjust a date by a number of business days, under lag rules.
534    ///
535    /// See :meth:`Cal.lag_bus_days <rateslib.scheduling.Cal.lag_bus_days>`.
536    #[pyo3(name = "lag_bus_days")]
537    fn lag_bus_days_py(&self, date: NaiveDateTime, days: i32, settlement: bool) -> NaiveDateTime {
538        self.lag_bus_days(&date, days, settlement)
539    }
540
541    /// Return a list of business dates in a range.
542    ///
543    /// See :meth:`Cal.bus_date_range <rateslib.scheduling.Cal.bus_date_range>`.
544    #[pyo3(name = "bus_date_range")]
545    fn bus_date_range_py(
546        &self,
547        start: NaiveDateTime,
548        end: NaiveDateTime,
549    ) -> PyResult<Vec<NaiveDateTime>> {
550        self.bus_date_range(&start, &end)
551    }
552
553    /// Return a list of calendar dates in a range.
554    ///
555    /// See :meth:`Cal.cal_date_range <rateslib.scheduling.Cal.cal_date_range>`.
556    #[pyo3(name = "cal_date_range")]
557    fn cal_date_range_py(
558        &self,
559        start: NaiveDateTime,
560        end: NaiveDateTime,
561    ) -> PyResult<Vec<NaiveDateTime>> {
562        self.cal_date_range(&start, &end)
563    }
564
565    // Pickling
566    fn __getnewargs__(&self) -> PyResult<(Vec<Cal>, Option<Vec<Cal>>)> {
567        Ok((self.calendars.clone(), self.settlement_calendars.clone()))
568    }
569
570    // JSON
571    /// Return a JSON representation of the object.
572    ///
573    /// Returns
574    /// -------
575    /// str
576    #[pyo3(name = "to_json")]
577    fn to_json_py(&self) -> PyResult<String> {
578        match DeserializedObj::UnionCal(self.clone()).to_json() {
579            Ok(v) => Ok(v),
580            Err(_) => Err(PyValueError::new_err(
581                "Failed to serialize `UnionCal` to JSON.",
582            )),
583        }
584    }
585
586    // Equality
587    fn __eq__(&self, other: Calendar) -> bool {
588        match other {
589            Calendar::UnionCal(c) => *self == c,
590            Calendar::Cal(c) => *self == c,
591            Calendar::NamedCal(c) => *self == c,
592        }
593    }
594}
595
596#[pymethods]
597impl NamedCal {
598    #[new]
599    fn new_py(name: String) -> PyResult<Self> {
600        NamedCal::try_new(&name)
601    }
602
603    /// A list of specifically provided non-business days.
604    #[getter]
605    fn holidays(&self) -> PyResult<Vec<NaiveDateTime>> {
606        self.union_cal.holidays()
607    }
608
609    /// A list of days in the week defined as weekends.
610    #[getter]
611    fn week_mask(&self) -> PyResult<HashSet<u8>> {
612        self.union_cal.week_mask()
613    }
614
615    /// The string identifier for this constructed calendar.
616    #[getter]
617    fn name(&self) -> String {
618        self.name.clone()
619    }
620
621    /// The wrapped :class:`~rateslib.scheduling.UnionCal` object.
622    #[getter]
623    fn union_cal(&self) -> UnionCal {
624        self.union_cal.clone()
625    }
626
627    /// Return whether the `date` is a business day.
628    ///
629    /// See :meth:`Cal.is_bus_day <rateslib.scheduling.Cal.is_bus_day>`.
630    #[pyo3(name = "is_bus_day")]
631    fn is_bus_day_py(&self, date: NaiveDateTime) -> bool {
632        self.is_bus_day(&date)
633    }
634
635    /// Return whether the `date` is **not** a business day.
636    ///
637    /// See :meth:`Cal.is_non_bus_day <rateslib.scheduling.Cal.is_non_bus_day>`.
638    #[pyo3(name = "is_non_bus_day")]
639    fn is_non_bus_day_py(&self, date: NaiveDateTime) -> bool {
640        self.is_non_bus_day(&date)
641    }
642
643    /// Return whether the `date` is a business day in an associated settlement calendar.
644    ///
645    /// If no such associated settlement calendar exists this will return *True*.
646    ///
647    /// See :meth:`Cal.is_settlement <rateslib.scheduling.Cal.is_settlement>`.
648    #[pyo3(name = "is_settlement")]
649    fn is_settlement_py(&self, date: NaiveDateTime) -> bool {
650        self.is_settlement(&date)
651    }
652
653    /// Return a date separated by calendar days from input date, and rolled with a modifier.
654    ///
655    /// See :meth:`Cal.add_cal_days <rateslib.scheduling.Cal.add_cal_days>`.
656    #[pyo3(name = "add_cal_days")]
657    fn add_cal_days_py(
658        &self,
659        date: NaiveDateTime,
660        days: i32,
661        adjuster: PyAdjuster,
662    ) -> PyResult<NaiveDateTime> {
663        Ok(self.add_cal_days(&date, days, &adjuster.into()))
664    }
665
666    /// Return a business date separated by `days` from an input business `date`.
667    ///
668    /// See :meth:`Cal.add_bus_days <rateslib.scheduling.Cal.add_bus_days>`.
669    #[pyo3(name = "add_bus_days")]
670    fn add_bus_days_py(
671        &self,
672        date: NaiveDateTime,
673        days: i32,
674        settlement: bool,
675    ) -> PyResult<NaiveDateTime> {
676        self.add_bus_days(&date, days, settlement)
677    }
678
679    /// Return a date separated by months from an input date, and rolled with a modifier.
680    ///
681    /// See :meth:`Cal.add_months <rateslib.scheduling.Cal.add_months>`.
682    #[pyo3(name = "add_months")]
683    fn add_months_py(
684        &self,
685        date: NaiveDateTime,
686        months: i32,
687        adjuster: PyAdjuster,
688        roll: Option<RollDay>,
689    ) -> NaiveDateTime {
690        let roll_ = match roll {
691            Some(val) => val,
692            None => RollDay::vec_from(&vec![date])[0],
693        };
694        let adjuster: Adjuster = adjuster.into();
695        adjuster.adjust(&roll_.uadd(&date, months), self)
696    }
697
698    /// Adjust a non-business date to a business date under a specific modification rule.
699    ///
700    /// See :meth:`Cal.adjust <rateslib.scheduling.Cal.adjust>`.
701    #[pyo3(name = "adjust")]
702    fn adjust_py(&self, date: NaiveDateTime, adjuster: PyAdjuster) -> PyResult<NaiveDateTime> {
703        Ok(self.adjust(&date, &adjuster.into()))
704    }
705
706    /// Adjust a list of dates under a date adjustment rule.
707    ///
708    /// See :meth:`Cal.adjusts <rateslib.scheduling.Cal.adjusts>`.
709    #[pyo3(name = "adjusts")]
710    fn adjusts_py(
711        &self,
712        dates: Vec<NaiveDateTime>,
713        adjuster: PyAdjuster,
714    ) -> PyResult<Vec<NaiveDateTime>> {
715        Ok(self.adjusts(&dates, &adjuster.into()))
716    }
717
718    /// Roll a date under a simplified adjustment rule.
719    ///
720    /// See :meth:`Cal.roll <rateslib.scheduling.Cal.roll>`.
721    #[pyo3(name = "roll")]
722    fn roll_py(
723        &self,
724        date: NaiveDateTime,
725        modifier: &str,
726        settlement: bool,
727    ) -> PyResult<NaiveDateTime> {
728        let adjuster = get_roll_adjuster_from_str((&modifier.to_lowercase(), settlement))?;
729        Ok(self.adjust(&date, &adjuster))
730    }
731
732    /// Adjust a date by a number of business days, under lag rules.
733    ///
734    /// See :meth:`Cal.lag_bus_days <rateslib.scheduling.Cal.lag_bus_days>`.
735    #[pyo3(name = "lag_bus_days")]
736    fn lag_bus_days_py(&self, date: NaiveDateTime, days: i32, settlement: bool) -> NaiveDateTime {
737        self.lag_bus_days(&date, days, settlement)
738    }
739
740    /// Return a list of business dates in a range.
741    ///
742    /// See :meth:`Cal.bus_date_range <rateslib.scheduling.Cal.bus_date_range>`.
743    #[pyo3(name = "bus_date_range")]
744    fn bus_date_range_py(
745        &self,
746        start: NaiveDateTime,
747        end: NaiveDateTime,
748    ) -> PyResult<Vec<NaiveDateTime>> {
749        self.bus_date_range(&start, &end)
750    }
751
752    /// Return a list of calendar dates in a range.
753    ///
754    /// See :meth:`Cal.cal_date_range <rateslib.scheduling.Cal.cal_date_range>`.
755    #[pyo3(name = "cal_date_range")]
756    fn cal_date_range_py(
757        &self,
758        start: NaiveDateTime,
759        end: NaiveDateTime,
760    ) -> PyResult<Vec<NaiveDateTime>> {
761        self.cal_date_range(&start, &end)
762    }
763
764    // Pickling
765    fn __getnewargs__(&self) -> PyResult<(String,)> {
766        Ok((self.name.clone(),))
767    }
768
769    // JSON
770    /// Return a JSON representation of the object.
771    ///
772    /// Returns
773    /// -------
774    /// str
775    #[pyo3(name = "to_json")]
776    fn to_json_py(&self) -> PyResult<String> {
777        match DeserializedObj::NamedCal(self.clone()).to_json() {
778            Ok(v) => Ok(v),
779            Err(_) => Err(PyValueError::new_err(
780                "Failed to serialize `NamedCal` to JSON.",
781            )),
782        }
783    }
784
785    // Equality
786    fn __eq__(&self, other: Calendar) -> bool {
787        match other {
788            Calendar::UnionCal(c) => *self == c,
789            Calendar::Cal(c) => *self == c,
790            Calendar::NamedCal(c) => *self == c,
791        }
792    }
793}
794
795#[cfg(test)]
796mod tests {
797    use super::*;
798    use crate::scheduling::ndt;
799
800    #[test]
801    fn test_add_37_months() {
802        let cal = Cal::try_from_name("all").unwrap();
803
804        let dates = vec![
805            (ndt(2000, 1, 1), ndt(2003, 2, 1)),
806            (ndt(2000, 2, 1), ndt(2003, 3, 1)),
807            (ndt(2000, 3, 1), ndt(2003, 4, 1)),
808            (ndt(2000, 4, 1), ndt(2003, 5, 1)),
809            (ndt(2000, 5, 1), ndt(2003, 6, 1)),
810            (ndt(2000, 6, 1), ndt(2003, 7, 1)),
811            (ndt(2000, 7, 1), ndt(2003, 8, 1)),
812            (ndt(2000, 8, 1), ndt(2003, 9, 1)),
813            (ndt(2000, 9, 1), ndt(2003, 10, 1)),
814            (ndt(2000, 10, 1), ndt(2003, 11, 1)),
815            (ndt(2000, 11, 1), ndt(2003, 12, 1)),
816            (ndt(2000, 12, 1), ndt(2004, 1, 1)),
817        ];
818        for i in 0..12 {
819            assert_eq!(
820                cal.add_months_py(
821                    dates[i].0,
822                    37,
823                    Adjuster::FollowingSettle {}.into(),
824                    Some(RollDay::Day(1)),
825                ),
826                dates[i].1
827            )
828        }
829    }
830
831    #[test]
832    fn test_sub_37_months() {
833        let cal = Cal::try_from_name("all").unwrap();
834
835        let dates = vec![
836            (ndt(2000, 1, 1), ndt(1996, 12, 1)),
837            (ndt(2000, 2, 1), ndt(1997, 1, 1)),
838            (ndt(2000, 3, 1), ndt(1997, 2, 1)),
839            (ndt(2000, 4, 1), ndt(1997, 3, 1)),
840            (ndt(2000, 5, 1), ndt(1997, 4, 1)),
841            (ndt(2000, 6, 1), ndt(1997, 5, 1)),
842            (ndt(2000, 7, 1), ndt(1997, 6, 1)),
843            (ndt(2000, 8, 1), ndt(1997, 7, 1)),
844            (ndt(2000, 9, 1), ndt(1997, 8, 1)),
845            (ndt(2000, 10, 1), ndt(1997, 9, 1)),
846            (ndt(2000, 11, 1), ndt(1997, 10, 1)),
847            (ndt(2000, 12, 1), ndt(1997, 11, 1)),
848        ];
849        for i in 0..12 {
850            assert_eq!(
851                cal.add_months_py(
852                    dates[i].0,
853                    -37,
854                    Adjuster::FollowingSettle {}.into(),
855                    Some(RollDay::Day(1)),
856                ),
857                dates[i].1
858            )
859        }
860    }
861
862    #[test]
863    fn test_add_months_py_roll() {
864        let cal = Cal::try_from_name("all").unwrap();
865        let roll = vec![
866            (RollDay::Day(7), ndt(1998, 3, 7), ndt(1996, 12, 7)),
867            (RollDay::Day(21), ndt(1998, 3, 21), ndt(1996, 12, 21)),
868            (RollDay::Day(31), ndt(1998, 3, 31), ndt(1996, 12, 31)),
869            (RollDay::Day(1), ndt(1998, 3, 1), ndt(1996, 12, 1)),
870            (RollDay::IMM(), ndt(1998, 3, 18), ndt(1996, 12, 18)),
871        ];
872        for i in 0..5 {
873            assert_eq!(
874                cal.add_months_py(
875                    roll[i].1,
876                    -15,
877                    Adjuster::FollowingSettle {}.into(),
878                    Some(roll[i].0)
879                ),
880                roll[i].2
881            );
882        }
883    }
884
885    #[test]
886    fn test_add_months_roll_invalid_days() {
887        let cal = Cal::try_from_name("all").unwrap();
888        let roll = vec![
889            (RollDay::Day(21), ndt(1996, 12, 21)),
890            (RollDay::Day(31), ndt(1996, 12, 31)),
891            (RollDay::Day(1), ndt(1996, 12, 1)),
892            (RollDay::IMM(), ndt(1996, 12, 18)),
893        ];
894        for i in 0..4 {
895            assert_eq!(
896                roll[i].1,
897                cal.add_months_py(
898                    ndt(1998, 3, 7),
899                    -15,
900                    Adjuster::FollowingSettle {}.into(),
901                    Some(roll[i].0),
902                ),
903            );
904        }
905    }
906
907    #[test]
908    fn test_add_months_modifier() {
909        let cal = Cal::try_from_name("bus").unwrap();
910        let modi = vec![
911            (Adjuster::Actual {}, ndt(2023, 9, 30)),          // Saturday
912            (Adjuster::FollowingSettle {}, ndt(2023, 10, 2)), // Monday
913            (Adjuster::ModifiedFollowingSettle {}, ndt(2023, 9, 29)), // Friday
914            (Adjuster::PreviousSettle {}, ndt(2023, 9, 29)),  // Friday
915            (Adjuster::ModifiedPreviousSettle {}, ndt(2023, 9, 29)), // Friday
916        ];
917        for i in 0..4 {
918            assert_eq!(
919                cal.add_months_py(
920                    ndt(2023, 8, 31),
921                    1,
922                    modi[i].0.into(),
923                    Some(RollDay::Day(31))
924                ),
925                modi[i].1
926            );
927        }
928    }
929
930    #[test]
931    fn test_add_months_modifier_p() {
932        let cal = Cal::try_from_name("bus").unwrap();
933        let modi = vec![
934            (Adjuster::Actual {}, ndt(2023, 7, 1)),          // Saturday
935            (Adjuster::FollowingSettle {}, ndt(2023, 7, 3)), // Monday
936            (Adjuster::ModifiedFollowingSettle {}, ndt(2023, 7, 3)), // Monday
937            (Adjuster::PreviousSettle {}, ndt(2023, 6, 30)), // Friday
938            (Adjuster::ModifiedPreviousSettle {}, ndt(2023, 7, 3)), // Monday
939        ];
940        for i in 0..4 {
941            assert_eq!(
942                cal.add_months_py(ndt(2023, 8, 1), -1, modi[i].0.into(), Some(RollDay::Day(1))),
943                modi[i].1
944            );
945        }
946    }
947}