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}