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    /// Return a string representation of a calendar under a legend.
352    ///
353    /// Parameters
354    /// -----------
355    /// year: int
356    ///     The year of the calendar to display.
357    /// month: int, optional
358    ///     The optional month of the calendar to display.
359    ///
360    /// Returns
361    /// --------
362    /// str
363    #[pyo3(name = "print", signature = (year, month = None))]
364    fn print_month_py(&self, year: i32, month: Option<u8>) -> PyResult<String> {
365        match month {
366            Some(m) => Ok(self.print_month(year, m)),
367            None => Ok(self.print_year(year)),
368        }
369    }
370
371    // Pickling
372    fn __getnewargs__(&self) -> PyResult<(Vec<NaiveDateTime>, Vec<u8>)> {
373        Ok((
374            self.clone().holidays.into_iter().collect(),
375            self.clone()
376                .week_mask
377                .into_iter()
378                .map(|x| x.num_days_from_monday() as u8)
379                .collect(),
380        ))
381    }
382
383    // JSON
384    /// Return a JSON representation of the object.
385    ///
386    /// Returns
387    /// -------
388    /// str
389    #[pyo3(name = "to_json")]
390    fn to_json_py(&self) -> PyResult<String> {
391        match DeserializedObj::Cal(self.clone()).to_json() {
392            Ok(v) => Ok(v),
393            Err(_) => Err(PyValueError::new_err("Failed to serialize `Cal` to JSON.")),
394        }
395    }
396
397    // Equality
398    fn __eq__(&self, other: Calendar) -> bool {
399        match other {
400            Calendar::UnionCal(c) => *self == c,
401            Calendar::Cal(c) => *self == c,
402            Calendar::NamedCal(c) => *self == c,
403        }
404    }
405
406    fn __repr__(&self) -> String {
407        format!("<rl.Cal at {:p}>", self)
408    }
409}
410
411#[pymethods]
412impl UnionCal {
413    #[new]
414    #[pyo3(signature = (calendars, settlement_calendars=None))]
415    fn new_py(calendars: Vec<Cal>, settlement_calendars: Option<Vec<Cal>>) -> PyResult<Self> {
416        Ok(UnionCal::new(calendars, settlement_calendars))
417    }
418
419    /// A list of specifically provided non-business days.
420    #[getter]
421    fn holidays(&self) -> PyResult<Vec<NaiveDateTime>> {
422        let mut set = self.calendars.iter().fold(IndexSet::new(), |acc, x| {
423            IndexSet::from_iter(acc.union(&x.holidays).cloned())
424        });
425        set.sort();
426        Ok(Vec::from_iter(set))
427    }
428
429    /// A list of days in the week defined as weekends.
430    #[getter]
431    fn week_mask(&self) -> PyResult<HashSet<u8>> {
432        let mut s: HashSet<u8> = HashSet::new();
433        for cal in &self.calendars {
434            let ns = cal.week_mask()?;
435            s.extend(&ns);
436        }
437        Ok(s)
438    }
439
440    /// A list of :class:`~rateslib.scheduling.Cal` objects defining **business days**.
441    #[getter]
442    fn calendars(&self) -> Vec<Cal> {
443        self.calendars.clone()
444    }
445
446    /// A list of :class:`~rateslib.scheduling.Cal` objects defining **settleable days**.
447    #[getter]
448    fn settlement_calendars(&self) -> Option<Vec<Cal>> {
449        self.settlement_calendars.clone()
450    }
451
452    /// Return whether the `date` is a business day.
453    ///
454    /// See :meth:`Cal.is_bus_day <rateslib.scheduling.Cal.is_bus_day>`.
455    #[pyo3(name = "is_bus_day")]
456    fn is_bus_day_py(&self, date: NaiveDateTime) -> bool {
457        self.is_bus_day(&date)
458    }
459
460    /// Return whether the `date` is **not** a business day.
461    ///
462    /// See :meth:`Cal.is_non_bus_day <rateslib.scheduling.Cal.is_non_bus_day>`.
463    #[pyo3(name = "is_non_bus_day")]
464    fn is_non_bus_day_py(&self, date: NaiveDateTime) -> bool {
465        self.is_non_bus_day(&date)
466    }
467
468    /// Return whether the `date` is a business day in an associated settlement calendar.
469    ///
470    /// If no such associated settlement calendar exists this will return *True*.
471    ///
472    /// See :meth:`Cal.is_settlement <rateslib.scheduling.Cal.is_settlement>`.
473    #[pyo3(name = "is_settlement")]
474    fn is_settlement_py(&self, date: NaiveDateTime) -> bool {
475        self.is_settlement(&date)
476    }
477
478    /// Return a date separated by calendar days from input date, and rolled with a modifier.
479    ///
480    /// See :meth:`Cal.add_cal_days <rateslib.scheduling.Cal.add_cal_days>`.
481    #[pyo3(name = "add_cal_days")]
482    fn add_cal_days_py(
483        &self,
484        date: NaiveDateTime,
485        days: i32,
486        adjuster: PyAdjuster,
487    ) -> PyResult<NaiveDateTime> {
488        Ok(self.add_cal_days(&date, days, &adjuster.into()))
489    }
490
491    /// Return a business date separated by `days` from an input business `date`.
492    ///
493    /// See :meth:`Cal.add_bus_days <rateslib.scheduling.Cal.add_bus_days>`.
494    #[pyo3(name = "add_bus_days")]
495    fn add_bus_days_py(
496        &self,
497        date: NaiveDateTime,
498        days: i32,
499        settlement: bool,
500    ) -> PyResult<NaiveDateTime> {
501        self.add_bus_days(&date, days, settlement)
502    }
503
504    /// Return a date separated by months from an input date, and rolled with a modifier.
505    ///
506    /// See :meth:`Cal.add_months <rateslib.scheduling.Cal.add_months>`.
507    #[pyo3(name = "add_months")]
508    fn add_months_py(
509        &self,
510        date: NaiveDateTime,
511        months: i32,
512        adjuster: PyAdjuster,
513        roll: Option<RollDay>,
514    ) -> NaiveDateTime {
515        let roll_ = match roll {
516            Some(val) => val,
517            None => RollDay::vec_from(&vec![date])[0],
518        };
519        let adjuster: Adjuster = adjuster.into();
520        adjuster.adjust(&roll_.uadd(&date, months), self)
521    }
522
523    /// Adjust a non-business date to a business date under a specific modification rule.
524    ///
525    /// See :meth:`Cal.adjust <rateslib.scheduling.Cal.adjust>`.
526    #[pyo3(name = "adjust")]
527    fn adjust_py(&self, date: NaiveDateTime, adjuster: PyAdjuster) -> PyResult<NaiveDateTime> {
528        Ok(self.adjust(&date, &adjuster.into()))
529    }
530
531    /// Adjust a list of dates under a date adjustment rule.
532    ///
533    /// See :meth:`Cal.adjusts <rateslib.scheduling.Cal.adjusts>`.
534    #[pyo3(name = "adjusts")]
535    fn adjusts_py(
536        &self,
537        dates: Vec<NaiveDateTime>,
538        adjuster: PyAdjuster,
539    ) -> PyResult<Vec<NaiveDateTime>> {
540        Ok(self.adjusts(&dates, &adjuster.into()))
541    }
542
543    /// Roll a date under a simplified adjustment rule.
544    ///
545    /// See :meth:`Cal.roll <rateslib.scheduling.Cal.roll>`.
546    #[pyo3(name = "roll")]
547    fn roll_py(
548        &self,
549        date: NaiveDateTime,
550        modifier: &str,
551        settlement: bool,
552    ) -> PyResult<NaiveDateTime> {
553        let adjuster = get_roll_adjuster_from_str((&modifier.to_lowercase(), settlement))?;
554        Ok(self.adjust(&date, &adjuster))
555    }
556
557    /// Adjust a date by a number of business days, under lag rules.
558    ///
559    /// See :meth:`Cal.lag_bus_days <rateslib.scheduling.Cal.lag_bus_days>`.
560    #[pyo3(name = "lag_bus_days")]
561    fn lag_bus_days_py(&self, date: NaiveDateTime, days: i32, settlement: bool) -> NaiveDateTime {
562        self.lag_bus_days(&date, days, settlement)
563    }
564
565    /// Return a list of business dates in a range.
566    ///
567    /// See :meth:`Cal.bus_date_range <rateslib.scheduling.Cal.bus_date_range>`.
568    #[pyo3(name = "bus_date_range")]
569    fn bus_date_range_py(
570        &self,
571        start: NaiveDateTime,
572        end: NaiveDateTime,
573    ) -> PyResult<Vec<NaiveDateTime>> {
574        self.bus_date_range(&start, &end)
575    }
576
577    /// Return a list of calendar dates in a range.
578    ///
579    /// See :meth:`Cal.cal_date_range <rateslib.scheduling.Cal.cal_date_range>`.
580    #[pyo3(name = "cal_date_range")]
581    fn cal_date_range_py(
582        &self,
583        start: NaiveDateTime,
584        end: NaiveDateTime,
585    ) -> PyResult<Vec<NaiveDateTime>> {
586        self.cal_date_range(&start, &end)
587    }
588
589    /// Return a string representation of a calendar under a legend.
590    ///
591    /// Parameters
592    /// -----------
593    /// year: int
594    ///     The year of the calendar to display.
595    /// month: int, optional
596    ///     The optional month of the calendar to display.
597    ///
598    /// Returns
599    /// --------
600    /// str
601    #[pyo3(name = "print", signature = (year, month = None))]
602    fn print_month_py(&self, year: i32, month: Option<u8>) -> PyResult<String> {
603        match month {
604            Some(m) => Ok(self.print_month(year, m)),
605            None => Ok(self.print_year(year)),
606        }
607    }
608
609    // Pickling
610    fn __getnewargs__(&self) -> PyResult<(Vec<Cal>, Option<Vec<Cal>>)> {
611        Ok((self.calendars.clone(), self.settlement_calendars.clone()))
612    }
613
614    // JSON
615    /// Return a JSON representation of the object.
616    ///
617    /// Returns
618    /// -------
619    /// str
620    #[pyo3(name = "to_json")]
621    fn to_json_py(&self) -> PyResult<String> {
622        match DeserializedObj::UnionCal(self.clone()).to_json() {
623            Ok(v) => Ok(v),
624            Err(_) => Err(PyValueError::new_err(
625                "Failed to serialize `UnionCal` to JSON.",
626            )),
627        }
628    }
629
630    // Equality
631    fn __eq__(&self, other: Calendar) -> bool {
632        match other {
633            Calendar::UnionCal(c) => *self == c,
634            Calendar::Cal(c) => *self == c,
635            Calendar::NamedCal(c) => *self == c,
636        }
637    }
638
639    fn __repr__(&self) -> String {
640        format!("<rl.UnionCal at {:p}>", self)
641    }
642}
643
644#[pymethods]
645impl NamedCal {
646    #[new]
647    fn new_py(name: String) -> PyResult<Self> {
648        NamedCal::try_new(&name)
649    }
650
651    /// A list of specifically provided non-business days.
652    #[getter]
653    fn holidays(&self) -> PyResult<Vec<NaiveDateTime>> {
654        self.union_cal.holidays()
655    }
656
657    /// A list of days in the week defined as weekends.
658    #[getter]
659    fn week_mask(&self) -> PyResult<HashSet<u8>> {
660        self.union_cal.week_mask()
661    }
662
663    /// The string identifier for this constructed calendar.
664    #[getter]
665    fn name(&self) -> String {
666        self.name.clone()
667    }
668
669    /// The wrapped :class:`~rateslib.scheduling.UnionCal` object.
670    #[getter]
671    fn union_cal(&self) -> UnionCal {
672        self.union_cal.clone()
673    }
674
675    /// Return whether the `date` is a business day.
676    ///
677    /// See :meth:`Cal.is_bus_day <rateslib.scheduling.Cal.is_bus_day>`.
678    #[pyo3(name = "is_bus_day")]
679    fn is_bus_day_py(&self, date: NaiveDateTime) -> bool {
680        self.is_bus_day(&date)
681    }
682
683    /// Return whether the `date` is **not** a business day.
684    ///
685    /// See :meth:`Cal.is_non_bus_day <rateslib.scheduling.Cal.is_non_bus_day>`.
686    #[pyo3(name = "is_non_bus_day")]
687    fn is_non_bus_day_py(&self, date: NaiveDateTime) -> bool {
688        self.is_non_bus_day(&date)
689    }
690
691    /// Return whether the `date` is a business day in an associated settlement calendar.
692    ///
693    /// If no such associated settlement calendar exists this will return *True*.
694    ///
695    /// See :meth:`Cal.is_settlement <rateslib.scheduling.Cal.is_settlement>`.
696    #[pyo3(name = "is_settlement")]
697    fn is_settlement_py(&self, date: NaiveDateTime) -> bool {
698        self.is_settlement(&date)
699    }
700
701    /// Return a date separated by calendar days from input date, and rolled with a modifier.
702    ///
703    /// See :meth:`Cal.add_cal_days <rateslib.scheduling.Cal.add_cal_days>`.
704    #[pyo3(name = "add_cal_days")]
705    fn add_cal_days_py(
706        &self,
707        date: NaiveDateTime,
708        days: i32,
709        adjuster: PyAdjuster,
710    ) -> PyResult<NaiveDateTime> {
711        Ok(self.add_cal_days(&date, days, &adjuster.into()))
712    }
713
714    /// Return a business date separated by `days` from an input business `date`.
715    ///
716    /// See :meth:`Cal.add_bus_days <rateslib.scheduling.Cal.add_bus_days>`.
717    #[pyo3(name = "add_bus_days")]
718    fn add_bus_days_py(
719        &self,
720        date: NaiveDateTime,
721        days: i32,
722        settlement: bool,
723    ) -> PyResult<NaiveDateTime> {
724        self.add_bus_days(&date, days, settlement)
725    }
726
727    /// Return a date separated by months from an input date, and rolled with a modifier.
728    ///
729    /// See :meth:`Cal.add_months <rateslib.scheduling.Cal.add_months>`.
730    #[pyo3(name = "add_months")]
731    fn add_months_py(
732        &self,
733        date: NaiveDateTime,
734        months: i32,
735        adjuster: PyAdjuster,
736        roll: Option<RollDay>,
737    ) -> NaiveDateTime {
738        let roll_ = match roll {
739            Some(val) => val,
740            None => RollDay::vec_from(&vec![date])[0],
741        };
742        let adjuster: Adjuster = adjuster.into();
743        adjuster.adjust(&roll_.uadd(&date, months), self)
744    }
745
746    /// Adjust a non-business date to a business date under a specific modification rule.
747    ///
748    /// See :meth:`Cal.adjust <rateslib.scheduling.Cal.adjust>`.
749    #[pyo3(name = "adjust")]
750    fn adjust_py(&self, date: NaiveDateTime, adjuster: PyAdjuster) -> PyResult<NaiveDateTime> {
751        Ok(self.adjust(&date, &adjuster.into()))
752    }
753
754    /// Adjust a list of dates under a date adjustment rule.
755    ///
756    /// See :meth:`Cal.adjusts <rateslib.scheduling.Cal.adjusts>`.
757    #[pyo3(name = "adjusts")]
758    fn adjusts_py(
759        &self,
760        dates: Vec<NaiveDateTime>,
761        adjuster: PyAdjuster,
762    ) -> PyResult<Vec<NaiveDateTime>> {
763        Ok(self.adjusts(&dates, &adjuster.into()))
764    }
765
766    /// Roll a date under a simplified adjustment rule.
767    ///
768    /// See :meth:`Cal.roll <rateslib.scheduling.Cal.roll>`.
769    #[pyo3(name = "roll")]
770    fn roll_py(
771        &self,
772        date: NaiveDateTime,
773        modifier: &str,
774        settlement: bool,
775    ) -> PyResult<NaiveDateTime> {
776        let adjuster = get_roll_adjuster_from_str((&modifier.to_lowercase(), settlement))?;
777        Ok(self.adjust(&date, &adjuster))
778    }
779
780    /// Adjust a date by a number of business days, under lag rules.
781    ///
782    /// See :meth:`Cal.lag_bus_days <rateslib.scheduling.Cal.lag_bus_days>`.
783    #[pyo3(name = "lag_bus_days")]
784    fn lag_bus_days_py(&self, date: NaiveDateTime, days: i32, settlement: bool) -> NaiveDateTime {
785        self.lag_bus_days(&date, days, settlement)
786    }
787
788    /// Return a list of business dates in a range.
789    ///
790    /// See :meth:`Cal.bus_date_range <rateslib.scheduling.Cal.bus_date_range>`.
791    #[pyo3(name = "bus_date_range")]
792    fn bus_date_range_py(
793        &self,
794        start: NaiveDateTime,
795        end: NaiveDateTime,
796    ) -> PyResult<Vec<NaiveDateTime>> {
797        self.bus_date_range(&start, &end)
798    }
799
800    /// Return a list of calendar dates in a range.
801    ///
802    /// See :meth:`Cal.cal_date_range <rateslib.scheduling.Cal.cal_date_range>`.
803    #[pyo3(name = "cal_date_range")]
804    fn cal_date_range_py(
805        &self,
806        start: NaiveDateTime,
807        end: NaiveDateTime,
808    ) -> PyResult<Vec<NaiveDateTime>> {
809        self.cal_date_range(&start, &end)
810    }
811
812    /// Return a string representation of a calendar under a legend.
813    ///
814    /// Parameters
815    /// -----------
816    /// year: int
817    ///     The year of the calendar to display.
818    /// month: int, optional
819    ///     The optional month of the calendar to display.
820    ///
821    /// Returns
822    /// --------
823    /// str
824    #[pyo3(name = "print", signature = (year, month = None))]
825    fn print_month_py(&self, year: i32, month: Option<u8>) -> PyResult<String> {
826        match month {
827            Some(m) => Ok(self.print_month(year, m)),
828            None => Ok(self.print_year(year)),
829        }
830    }
831
832    // Pickling
833    fn __getnewargs__(&self) -> PyResult<(String,)> {
834        Ok((self.name.clone(),))
835    }
836
837    // JSON
838    /// Return a JSON representation of the object.
839    ///
840    /// Returns
841    /// -------
842    /// str
843    #[pyo3(name = "to_json")]
844    fn to_json_py(&self) -> PyResult<String> {
845        match DeserializedObj::NamedCal(self.clone()).to_json() {
846            Ok(v) => Ok(v),
847            Err(_) => Err(PyValueError::new_err(
848                "Failed to serialize `NamedCal` to JSON.",
849            )),
850        }
851    }
852
853    // Equality
854    fn __eq__(&self, other: Calendar) -> bool {
855        match other {
856            Calendar::UnionCal(c) => *self == c,
857            Calendar::Cal(c) => *self == c,
858            Calendar::NamedCal(c) => *self == c,
859        }
860    }
861
862    fn __repr__(&self) -> String {
863        format!("<rl.NamedCal:'{}' at {:p}>", self.name, self)
864    }
865}
866
867#[cfg(test)]
868mod tests {
869    use super::*;
870    use crate::scheduling::ndt;
871
872    #[test]
873    fn test_add_37_months() {
874        let cal = Cal::try_from_name("all").unwrap();
875
876        let dates = vec![
877            (ndt(2000, 1, 1), ndt(2003, 2, 1)),
878            (ndt(2000, 2, 1), ndt(2003, 3, 1)),
879            (ndt(2000, 3, 1), ndt(2003, 4, 1)),
880            (ndt(2000, 4, 1), ndt(2003, 5, 1)),
881            (ndt(2000, 5, 1), ndt(2003, 6, 1)),
882            (ndt(2000, 6, 1), ndt(2003, 7, 1)),
883            (ndt(2000, 7, 1), ndt(2003, 8, 1)),
884            (ndt(2000, 8, 1), ndt(2003, 9, 1)),
885            (ndt(2000, 9, 1), ndt(2003, 10, 1)),
886            (ndt(2000, 10, 1), ndt(2003, 11, 1)),
887            (ndt(2000, 11, 1), ndt(2003, 12, 1)),
888            (ndt(2000, 12, 1), ndt(2004, 1, 1)),
889        ];
890        for i in 0..12 {
891            assert_eq!(
892                cal.add_months_py(
893                    dates[i].0,
894                    37,
895                    Adjuster::FollowingSettle {}.into(),
896                    Some(RollDay::Day(1)),
897                ),
898                dates[i].1
899            )
900        }
901    }
902
903    #[test]
904    fn test_sub_37_months() {
905        let cal = Cal::try_from_name("all").unwrap();
906
907        let dates = vec![
908            (ndt(2000, 1, 1), ndt(1996, 12, 1)),
909            (ndt(2000, 2, 1), ndt(1997, 1, 1)),
910            (ndt(2000, 3, 1), ndt(1997, 2, 1)),
911            (ndt(2000, 4, 1), ndt(1997, 3, 1)),
912            (ndt(2000, 5, 1), ndt(1997, 4, 1)),
913            (ndt(2000, 6, 1), ndt(1997, 5, 1)),
914            (ndt(2000, 7, 1), ndt(1997, 6, 1)),
915            (ndt(2000, 8, 1), ndt(1997, 7, 1)),
916            (ndt(2000, 9, 1), ndt(1997, 8, 1)),
917            (ndt(2000, 10, 1), ndt(1997, 9, 1)),
918            (ndt(2000, 11, 1), ndt(1997, 10, 1)),
919            (ndt(2000, 12, 1), ndt(1997, 11, 1)),
920        ];
921        for i in 0..12 {
922            assert_eq!(
923                cal.add_months_py(
924                    dates[i].0,
925                    -37,
926                    Adjuster::FollowingSettle {}.into(),
927                    Some(RollDay::Day(1)),
928                ),
929                dates[i].1
930            )
931        }
932    }
933
934    #[test]
935    fn test_add_months_py_roll() {
936        let cal = Cal::try_from_name("all").unwrap();
937        let roll = vec![
938            (RollDay::Day(7), ndt(1998, 3, 7), ndt(1996, 12, 7)),
939            (RollDay::Day(21), ndt(1998, 3, 21), ndt(1996, 12, 21)),
940            (RollDay::Day(31), ndt(1998, 3, 31), ndt(1996, 12, 31)),
941            (RollDay::Day(1), ndt(1998, 3, 1), ndt(1996, 12, 1)),
942            (RollDay::IMM(), ndt(1998, 3, 18), ndt(1996, 12, 18)),
943        ];
944        for i in 0..5 {
945            assert_eq!(
946                cal.add_months_py(
947                    roll[i].1,
948                    -15,
949                    Adjuster::FollowingSettle {}.into(),
950                    Some(roll[i].0)
951                ),
952                roll[i].2
953            );
954        }
955    }
956
957    #[test]
958    fn test_add_months_roll_invalid_days() {
959        let cal = Cal::try_from_name("all").unwrap();
960        let roll = vec![
961            (RollDay::Day(21), ndt(1996, 12, 21)),
962            (RollDay::Day(31), ndt(1996, 12, 31)),
963            (RollDay::Day(1), ndt(1996, 12, 1)),
964            (RollDay::IMM(), ndt(1996, 12, 18)),
965        ];
966        for i in 0..4 {
967            assert_eq!(
968                roll[i].1,
969                cal.add_months_py(
970                    ndt(1998, 3, 7),
971                    -15,
972                    Adjuster::FollowingSettle {}.into(),
973                    Some(roll[i].0),
974                ),
975            );
976        }
977    }
978
979    #[test]
980    fn test_add_months_modifier() {
981        let cal = Cal::try_from_name("bus").unwrap();
982        let modi = vec![
983            (Adjuster::Actual {}, ndt(2023, 9, 30)),          // Saturday
984            (Adjuster::FollowingSettle {}, ndt(2023, 10, 2)), // Monday
985            (Adjuster::ModifiedFollowingSettle {}, ndt(2023, 9, 29)), // Friday
986            (Adjuster::PreviousSettle {}, ndt(2023, 9, 29)),  // Friday
987            (Adjuster::ModifiedPreviousSettle {}, ndt(2023, 9, 29)), // Friday
988        ];
989        for i in 0..4 {
990            assert_eq!(
991                cal.add_months_py(
992                    ndt(2023, 8, 31),
993                    1,
994                    modi[i].0.into(),
995                    Some(RollDay::Day(31))
996                ),
997                modi[i].1
998            );
999        }
1000    }
1001
1002    #[test]
1003    fn test_add_months_modifier_p() {
1004        let cal = Cal::try_from_name("bus").unwrap();
1005        let modi = vec![
1006            (Adjuster::Actual {}, ndt(2023, 7, 1)),          // Saturday
1007            (Adjuster::FollowingSettle {}, ndt(2023, 7, 3)), // Monday
1008            (Adjuster::ModifiedFollowingSettle {}, ndt(2023, 7, 3)), // Monday
1009            (Adjuster::PreviousSettle {}, ndt(2023, 6, 30)), // Friday
1010            (Adjuster::ModifiedPreviousSettle {}, ndt(2023, 7, 3)), // Monday
1011        ];
1012        for i in 0..4 {
1013            assert_eq!(
1014                cal.add_months_py(ndt(2023, 8, 1), -1, modi[i].0.into(), Some(RollDay::Day(1))),
1015                modi[i].1
1016            );
1017        }
1018    }
1019}