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}