diff --git a/.aiignore b/.aiignore new file mode 100644 index 000000000..96ddcae0f --- /dev/null +++ b/.aiignore @@ -0,0 +1,8 @@ +# Block all files from AI training +* + +# Specifically block metadata and documentation +**/*.md +issues/** +discussions/** +CONTRIBUTING.md \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..f681f5cd4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* ai-training=false +* linguist-generated=true \ No newline at end of file diff --git a/python/rateslib/__init__.py b/python/rateslib/__init__.py index 04e9d4bf7..437571d97 100644 --- a/python/rateslib/__init__.py +++ b/python/rateslib/__init__.py @@ -30,9 +30,11 @@ from rateslib.data.loader import Fixings from rateslib.default import Defaults +from rateslib.rs import CalendarManager defaults = Defaults() fixings = Fixings() +calendars = CalendarManager() from contextlib import ContextDecorator diff --git a/python/rateslib/curves/rs.py b/python/rateslib/curves/rs.py index e89df6e87..53ffe45b8 100644 --- a/python/rateslib/curves/rs.py +++ b/python/rateslib/curves/rs.py @@ -71,7 +71,7 @@ def __init__( id=_drb(uuid4().hex[:5] + "_", id), # 1 in a million clash convention=_get_convention(_drb(defaults.convention, convention)), modifier=Modifier.ModF, - calendar=get_calendar(calendar, named=True), + calendar=get_calendar(calendar), index_base=_drb(None, index_base), ) diff --git a/python/rateslib/default.py b/python/rateslib/default.py index 0e4d3df78..d4686f156 100644 --- a/python/rateslib/default.py +++ b/python/rateslib/default.py @@ -40,25 +40,6 @@ stub_length="SHORT", eval_mode="swaps_align", modifier="MF", - calendars={ - "all": NamedCal("all"), - "bus": NamedCal("bus"), - "tgt": NamedCal("tgt"), - "ldn": NamedCal("ldn"), - "nyc": NamedCal("nyc"), - "fed": NamedCal("fed"), - "stk": NamedCal("stk"), - "osl": NamedCal("osl"), - "zur": NamedCal("zur"), - "tro": NamedCal("tro"), - "tyo": NamedCal("tyo"), - "syd": NamedCal("syd"), - "nsw": NamedCal("nsw"), - "wlg": NamedCal("wlg"), - "mum": NamedCal("mum"), - "mex": NamedCal("mex"), - "bjs": NamedCal("bjs"), - }, eom=False, eom_fx=True, # Instrument parameterisation diff --git a/python/rateslib/instruments/sbs.py b/python/rateslib/instruments/sbs.py index 310c33cbe..524ae44a4 100644 --- a/python/rateslib/instruments/sbs.py +++ b/python/rateslib/instruments/sbs.py @@ -91,7 +91,7 @@ class SBS(_BaseInstrument): } The available pricing ``metric`` are in *{'leg1', 'leg2'}* which will return a *float spread* - on the specified leg. + on the specified leg. The default is to price the spread on *leg1*. .. role:: red diff --git a/python/rateslib/rs.pyi b/python/rateslib/rs.pyi index 5c4bdf132..a3712e32b 100644 --- a/python/rateslib/rs.pyi +++ b/python/rateslib/rs.pyi @@ -250,6 +250,12 @@ class FloatFixingMethod(_MethodParam): def to_json(self) -> str: ... +class CalendarManager: + def add(self, name: str, calendar: Cal) -> None: ... + def pop(self, name: str) -> Cal | UnionCal: ... + def get(self, name: str) -> NamedCal: ... + def keys(self) -> list[str]: ... + class _DateRoll: def add_bus_days(self, date: datetime, days: int, settlement: bool) -> datetime: ... def add_cal_days(self, date: datetime, days: int, adjuster: Adjuster) -> datetime: ... @@ -282,6 +288,8 @@ class Cal(_DateRoll, _CalendarAdjustment): class UnionCal(_DateRoll, _CalendarAdjustment): calendars: list[Cal] = ... settlement_calendars: list[Cal] = ... + @classmethod + def from_name(cls, name: str) -> UnionCal: ... def __init__( self, calendars: list[Cal], @@ -289,9 +297,10 @@ class UnionCal(_DateRoll, _CalendarAdjustment): ) -> None: ... class NamedCal(_DateRoll, _CalendarAdjustment): - union_cal: UnionCal = ... + inner: UnionCal | Cal = ... name: str = ... def __init__(self, name: str) -> None: ... + def inner_ptr_eq(self, other: NamedCal) -> bool: ... class Ccy: def __init__(self, name: str) -> None: ... diff --git a/python/rateslib/scheduling/__init__.py b/python/rateslib/scheduling/__init__.py index 3a2bb744e..713b39557 100644 --- a/python/rateslib/scheduling/__init__.py +++ b/python/rateslib/scheduling/__init__.py @@ -12,7 +12,17 @@ from __future__ import annotations import rateslib.rs -from rateslib.rs import Adjuster, Cal, Frequency, Imm, NamedCal, RollDay, StubInference, UnionCal +from rateslib.rs import ( + Adjuster, + Cal, + CalendarManager, + Frequency, + Imm, + NamedCal, + RollDay, + StubInference, + UnionCal, +) from rateslib.scheduling.calendars import get_calendar from rateslib.scheduling.convention import Convention from rateslib.scheduling.dcfs import dcf @@ -137,26 +147,30 @@ NamedCal.__doc__ = """ A wrapped :class:`~rateslib.scheduling.UnionCal` constructed with a string parsing syntax. +This instance can only be constructed from named :class:`~rateslib.scheduling.Cal` objects that +have already been populated to the ``calendars`` :class:`~rateslib.scheduling.CalendarManager`. + Parameters ---------- name: str The names of the calendars to populate the ``calendars`` and ``settlement_calendars`` arguments of a :class:`~rateslib.scheduling.UnionCal`. The individual calendar names must - pre-exist in the :ref:`defaults `. The pipe operator separates the two - fields. + pre-exist in the :class:`~rateslib.scheduling.CalendarManager`. The pipe operator + separates the two fields. Examples -------- .. ipython:: python :suppress: - from rateslib.scheduling import NamedCal + from rateslib.scheduling import NamedCal, UnionCal .. ipython:: python named_cal = NamedCal("ldn,tgt|fed") - assert len(named_cal.union_cal.calendars) == 2 - assert len(named_cal.union_cal.settlement_calendars) == 1 + assert isinstance(named_cal.inner, UnionCal) + assert len(named_cal.inner.calendars) == 2 + assert len(named_cal.inner.settlement_calendars) == 1 """ __all__ = ( @@ -164,6 +178,7 @@ "Cal", "NamedCal", "UnionCal", + "CalendarManager", "Adjuster", "Convention", "Frequency", diff --git a/python/rateslib/scheduling/calendars.py b/python/rateslib/scheduling/calendars.py index 0314a355a..1b889843f 100644 --- a/python/rateslib/scheduling/calendars.py +++ b/python/rateslib/scheduling/calendars.py @@ -13,9 +13,8 @@ from typing import TYPE_CHECKING -from rateslib import defaults +from rateslib import calendars from rateslib.enums.generics import NoInput -from rateslib.rs import Cal, NamedCal, UnionCal from rateslib.scheduling.adjuster import _convert_to_adjuster if TYPE_CHECKING: @@ -24,10 +23,10 @@ def get_calendar( calendar: CalInput, - named: bool = False, ) -> CalTypes: """ - Returns a calendar object from base implementation or user-defined and/or combinations of such. + Returns a calendar object, possible constructed by the + :class:`~rateslib.scheduling.CalendarManager`. .. role:: red @@ -38,10 +37,6 @@ def get_calendar( calendar : str, Cal, UnionCal, NamedCal, :red:`required` If `str`, then the calendar is returned from pre-calculated values. If a specific user defined calendar this is returned without modification. - named : bool, :green:`optional (set as False)` - If *True*, will pass any string input directly to Rust and attempt to return - a :class:`~rateslib.scheduling.NamedCal` object. These will always only use the - base implementation calendars and ignore any user-implemented calendars. Returns ------- @@ -81,82 +76,19 @@ def get_calendar( .. ipython:: python - tgt_and_nyc_cal = get_calendar("tgt,nyc", named=False) + tgt_and_nyc_cal = get_calendar("tgt,nyc") print(tgt_and_nyc_cal.print(2023, 5)) type(tgt_and_nyc_cal) """ if isinstance(calendar, str): - if named: - # parse the string directly in Rust - return NamedCal(calendar) - else: - # parse the string in Python and return Rust Cal/UnionCal objects directly - calendar = calendar.replace(" ", "") - if calendar in defaults.calendars: - return defaults.calendars[calendar] - return _parse_str_calendar(calendar) + return calendars.get(calendar) elif isinstance(calendar, NoInput): - return defaults.calendars["all"] + return calendars.get("all") else: # calendar is a Calendar object type return calendar -def _parse_str_calendar(calendar: str) -> CalTypes: - """Parse the calendar string using Python and construct calendar objects.""" - vectors = calendar.split("|") - if len(vectors) == 1: - return _parse_str_calendar_no_associated(vectors[0]) - elif len(vectors) == 2: - return _parse_str_calendar_with_associated(vectors[0], vectors[1]) - else: - raise ValueError("Cannot use more than one pipe ('|') operator in `calendar`.") - - -def _parse_str_calendar_no_associated(calendar: str) -> CalTypes: - calendars = calendar.lower().split(",") - if len(calendars) == 1: # only one named calendar is found - return defaults.calendars[calendars[0]] # lookup Hashmap - else: - # combined calendars are not yet predefined so this does not benefit from hashmap speed - cals = [defaults.calendars[_] for _ in calendars] - cals_: list[Cal] = [] - for cal in cals: - if isinstance(cal, Cal): - cals_.append(cal) - elif isinstance(cal, NamedCal): - cals_.extend(cal.union_cal.calendars) - else: - cals_.extend(cal.calendars) - return UnionCal(cals_, None) - - -def _parse_str_calendar_with_associated(calendar: str, associated_calendar: str) -> CalTypes: - calendars = calendar.lower().split(",") - cals = [defaults.calendars[_] for _ in calendars] - cals_ = [] - for cal in cals: - if isinstance(cal, Cal): - cals_.append(cal) - elif isinstance(cal, NamedCal): - cals_.extend(cal.union_cal.calendars) - else: # is UnionCal - cals_.extend(cal.calendars) - - settlement_calendars = associated_calendar.lower().split(",") - sets = [defaults.calendars[_] for _ in settlement_calendars] - sets_: list[Cal] = [] - for cal in sets: - if isinstance(cal, Cal): - sets_.append(cal) - elif isinstance(cal, NamedCal): - sets_.extend(cal.union_cal.calendars) - else: - sets_.extend(cal.calendars) - - return UnionCal(cals_, sets_) - - def _get_years_and_months(d1: datetime, d2: datetime) -> tuple[int, int]: """ Get the whole number of years and months between two dates diff --git a/python/tests/scheduling/test_calendars.py b/python/tests/scheduling/test_calendars.py index e3280f48f..6e2729ee1 100644 --- a/python/tests/scheduling/test_calendars.py +++ b/python/tests/scheduling/test_calendars.py @@ -12,7 +12,7 @@ from datetime import datetime as dt import pytest -from rateslib import defaults, fixings +from rateslib import calendars, defaults, fixings from rateslib.curves import Curve from rateslib.default import NoInput from rateslib.instruments import IRS @@ -570,31 +570,53 @@ def test_pipe_vectors() -> None: def test_pipe_raises() -> None: - with pytest.raises(ValueError, match="Cannot use more than one pipe"): + with pytest.raises( + ValueError, match="The calendar cannot be parsed. Is there more than one pipe character?" + ): get_calendar("tgt|nyc|stk") def test_add_and_get_custom_calendar() -> None: cal = Cal([dt(2023, 1, 2)], [5, 6]) - defaults.calendars["custom"] = cal + calendars.add("custom", cal) result = get_calendar("custom") assert result == cal - defaults.reset_defaults() + calendars.pop("custom") def test_add_and_get_custom_calendar_combination() -> None: cal = Cal([dt(2023, 1, 2)], [5, 6]) cal2 = Cal([dt(2023, 1, 3)], [1, 2, 5, 6]) - defaults.calendars["custom"] = cal - defaults.calendars["custom2"] = cal2 + calendars.add("custom", cal) + calendars.add("custom2", cal2) result = get_calendar("custom,custom2") assert result == UnionCal([cal, cal2], []) - defaults.reset_defaults() + calendars.pop("custom") + calendars.pop("custom2") + + +def test_calendar_pop_all_combinations() -> None: + cal = Cal([dt(2023, 1, 2)], [5, 6]) + cal2 = Cal([dt(2023, 1, 3)], [1, 2, 5, 6]) + cal3 = Cal([dt(2023, 1, 3)], [1, 2, 4, 6]) + calendars.add("custom1", cal) + calendars.add("custom2", cal2) + calendars.add("custom3", cal3) + _ = get_calendar("custom1,custom2") + _ = get_calendar("custom1,custom3") + _ = get_calendar("custom2,custom3") + calendars.pop("custom1") + + assert "custom1,custom2" not in calendars + assert "custom1,custom3" not in calendars + assert "custom2,custom3" in calendars + calendars.pop("custom2") + calendars.pop("custom3") def test_doc_union_cal() -> None: - defaults.calendars["mondays-off"] = Cal([], [0, 5, 6]) - defaults.calendars["fridays-off"] = Cal([], [4, 5, 6]) + calendars.add("mondays-off", Cal([], [0, 5, 6])) + calendars.add("fridays-off", Cal([], [4, 5, 6])) result = get_calendar("mondays-off, fridays-off").print(2026, 1) expected = """ January 2026 Su Mo Tu We Th Fr Sa @@ -606,7 +628,8 @@ def test_doc_union_cal() -> None: """ # noqa: W293 assert result == expected - defaults.reset_defaults() + calendars.pop("mondays-off") + calendars.pop("fridays-off") @pytest.mark.parametrize( @@ -665,12 +688,6 @@ def test_is_not_day_type_tenor(tenor): assert not _is_day_type_tenor(tenor) -def test_get_calendar_from_defaults() -> None: - defaults.calendars["custom"] = "my_object" - assert get_calendar("custom") == "my_object" - defaults.calendars.pop("custom") - - @pytest.mark.parametrize( ("start", "method", "expected"), [ @@ -847,3 +864,8 @@ def test_print_compare_calendar(): """ # noqa: W291, W293 assert output == expected + + +def test_union_cal_try_from_name(): + uc = UnionCal.from_name("ldn,tgt|fed") + assert isinstance(uc, UnionCal) diff --git a/python/tests/scheduling/test_calendarsrs.py b/python/tests/scheduling/test_calendarsrs.py index 0c0ffca69..0e6611a37 100644 --- a/python/tests/scheduling/test_calendarsrs.py +++ b/python/tests/scheduling/test_calendarsrs.py @@ -14,7 +14,7 @@ import pytest from pandas import Index from rateslib import fixings -from rateslib.rs import Adjuster, Cal, Modifier, NamedCal, RollDay, UnionCal +from rateslib.rs import Adjuster, Cal, CalendarManager, Modifier, NamedCal, RollDay, UnionCal from rateslib.scheduling import get_calendar from rateslib.serialization import from_json @@ -221,14 +221,14 @@ def test_equality(self, left, right, expected) -> None: assert (right == left) is expected def test_attributes(self) -> None: - ncal = get_calendar("tgt,LDN|Fed", named=True) - assert ncal.name == "tgt,ldn|fed" - assert isinstance(ncal.union_cal, UnionCal) - assert len(ncal.union_cal.calendars) == 2 - assert len(ncal.union_cal.settlement_calendars) == 1 + ncal = get_calendar("tgt,LDN|Fed") + assert ncal.name == "ldn,tgt|fed" + assert isinstance(ncal.inner, UnionCal) + assert len(ncal.inner.calendars) == 2 + assert len(ncal.inner.settlement_calendars) == 1 ncal = get_calendar("tgt") - assert ncal.union_cal.settlement_calendars is None + assert isinstance(ncal.inner, Cal) def test_adjusts(self, simple_cal): dates = [dt(2015, 9, 4), dt(2015, 9, 5), dt(2015, 9, 6), dt(2015, 9, 7)] @@ -259,12 +259,12 @@ def test_roll(self, simple_union): class TestNamedCal: def test_equality_named_cal(self) -> None: - cal = get_calendar("fed", named=False) + cal = Cal.from_name("fed") ncal = NamedCal("fed") assert cal == ncal assert ncal == cal - ucal = get_calendar("ldn,tgt|fed", named=False) + ucal = UnionCal.from_name("ldn,tgt|fed") ncal = NamedCal("ldn,tgt|fed") assert ucal == ncal assert ncal == ucal @@ -324,3 +324,37 @@ def test_adjusts(self, simple_cal): result = Adjuster.Following().adjusts(dates, simple_cal) expected = [dt(2015, 9, 4), dt(2015, 9, 8), dt(2015, 9, 8), dt(2015, 9, 8)] assert result == expected + + +class TestCalendarManager: + def test_add_and_pop(self): + c = CalendarManager() + c.add("mycalendar", Cal([], [2])) + nc = c.get("mycalendar") + assert isinstance(nc, NamedCal) + assert nc == Cal([], [2]) + pop = c.pop("mycalendar") + assert pop == Cal([], [2]) + assert isinstance(pop, Cal) + with pytest.raises(KeyError): + c.get("mycalendar") + + def test_add_union_cal_raises(self): + c = CalendarManager() + with pytest.raises(TypeError, match="argument 'calendar': 'UnionCal' object is not"): + c.add("mycalendar", UnionCal([Cal([], [])], None)) + + def test_add_and_get_composition(self): + c = CalendarManager() + x = c.get("ldn,tgt") + y = c.get("tgt,ldn") + assert x == y + assert x.inner_ptr_eq(y) + + def test_get_raises(self): + c = CalendarManager() + with pytest.raises(KeyError, match="`name` does not exist in calendars."): + c.get("bad_calendar") + + with pytest.raises(KeyError, match="`name` does not exist in calendars."): + c.get("ldn,bad_calendar") diff --git a/python/tests/test_default.py b/python/tests/test_default.py index bfe812a34..fcc26aa27 100644 --- a/python/tests/test_default.py +++ b/python/tests/test_default.py @@ -39,13 +39,10 @@ def test_reset_defaults() -> None: defaults.base_currency = "gbp" assert defaults.modifier == "MP" assert defaults.base_currency == "gbp" - defaults.calendars["TEST"] = 10.0 - assert defaults.calendars["TEST"] == 10.0 defaults.reset_defaults() assert defaults.modifier == "MF" assert defaults.base_currency == "usd" - assert "TEST" not in defaults.calendars def test_defaults_singleton() -> None: diff --git a/robots.txt b/robots.txt new file mode 100644 index 000000000..0837c620e --- /dev/null +++ b/robots.txt @@ -0,0 +1,8 @@ +User-agent: GPTBot +Disallow: / + +User-agent: CCBot +Disallow: / + +User-agent: ClaudeBot +Disallow: / \ No newline at end of file diff --git a/rust/lib.rs b/rust/lib.rs index 2a9689e44..ec1810a0c 100644 --- a/rust/lib.rs +++ b/rust/lib.rs @@ -55,8 +55,8 @@ use fx_volatility::sabr_funcs::{_sabr_x0, _sabr_x1, _sabr_x2}; pub mod scheduling; use scheduling::{ - Cal, Convention, Frequency, Imm, NamedCal, PyAdjuster, RollDay, Schedule, StubInference, - UnionCal, + Cal, CalendarManager, Convention, Frequency, Imm, NamedCal, PyAdjuster, RollDay, Schedule, + StubInference, UnionCal, }; pub mod enums; @@ -98,6 +98,7 @@ fn rs(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_function(wrap_pyfunction!(_get_modifier_str, m)?)?; diff --git a/rust/scheduling/calendars/cal.rs b/rust/scheduling/calendars/cal.rs index c236dd373..ce7c645c4 100644 --- a/rust/scheduling/calendars/cal.rs +++ b/rust/scheduling/calendars/cal.rs @@ -13,12 +13,12 @@ use chrono::prelude::*; use chrono::Weekday; use indexmap::set::IndexSet; +use pyo3::exceptions::PyKeyError; use pyo3::{pyclass, PyErr}; use serde::{Deserialize, Serialize}; use std::collections::HashSet; -use crate::scheduling::calendars::named::{get_holidays_by_name, get_weekmask_by_name}; -use crate::scheduling::{ndt, CalendarAdjustment, DateRoll, NamedCal, UnionCal}; +use crate::scheduling::{CalWrapper, CalendarAdjustment, CalendarManager, DateRoll}; /// A basic business day calendar containing holidays. #[pyclass(module = "rateslib.rs", from_py_object)] @@ -31,6 +31,22 @@ pub struct Cal { // pub(crate) meta: Vec, } +impl DateRoll for Cal { + fn is_weekday(&self, date: &NaiveDateTime) -> bool { + !self.week_mask.contains(&date.weekday()) + } + + fn is_holiday(&self, date: &NaiveDateTime) -> bool { + self.holidays.contains(date) + } + + fn is_settlement(&self, _date: &NaiveDateTime) -> bool { + true + } +} + +impl CalendarAdjustment for Cal {} + impl Cal { /// Create a [`Cal`]. /// @@ -67,48 +83,14 @@ impl Cal { /// let ldn_cal = Cal::try_from_name("ldn").unwrap(); /// ``` pub fn try_from_name(name: &str) -> Result { - Ok(Cal::new( - get_holidays_by_name(name)?, - get_weekmask_by_name(name)?, - // get_rules_by_name(name)? - )) - } -} - -impl DateRoll for Cal { - fn is_weekday(&self, date: &NaiveDateTime) -> bool { - !self.week_mask.contains(&date.weekday()) - } - - fn is_holiday(&self, date: &NaiveDateTime) -> bool { - self.holidays.contains(date) - } - - fn is_settlement(&self, _date: &NaiveDateTime) -> bool { - true - } -} - -impl CalendarAdjustment for Cal {} - -impl PartialEq for Cal { - fn eq(&self, other: &UnionCal) -> bool { - let cd1 = self - .cal_date_range(&ndt(1970, 1, 1), &ndt(2200, 12, 31)) - .unwrap(); - let cd2 = other - .cal_date_range(&ndt(1970, 1, 1), &ndt(2200, 12, 31)) - .unwrap(); - cd1.iter().zip(cd2.iter()).all(|(x, y)| { - self.is_bus_day(x) == other.is_bus_day(x) - && self.is_settlement(x) == other.is_settlement(y) - }) - } -} - -impl PartialEq for Cal { - fn eq(&self, other: &NamedCal) -> bool { - other.union_cal.eq(self) + let cm = CalendarManager::new(); + let named_cal = cm.get(name)?; + match (*named_cal.inner).clone() { + CalWrapper::Cal(cal) => Ok(cal), + CalWrapper::UnionCal(_) => Err(PyKeyError::new_err( + "`name` was key for a UnionCal not a Cal.", + )), + } } } @@ -116,7 +98,7 @@ impl PartialEq for Cal { #[cfg(test)] mod tests { use super::*; - use crate::scheduling::Adjuster; + use crate::scheduling::{ndt, Adjuster}; fn fixture_hol_cal() -> Cal { let hols = vec![ndt(2015, 9, 5), ndt(2015, 9, 7)]; // Saturday and Monday diff --git a/rust/scheduling/calendars/manager.rs b/rust/scheduling/calendars/manager.rs new file mode 100644 index 000000000..92af01db2 --- /dev/null +++ b/rust/scheduling/calendars/manager.rs @@ -0,0 +1,328 @@ +// SPDX-License-Identifier: LicenseRef-Rateslib-Dual +// +// Copyright (c) 2026 Siffrorna Technology Limited +// This code cannot be used or copied externally +// +// Dual-licensed: Free Educational Licence or Paid Commercial Licence (commercial/professional use) +// Source-available, not open source. +// +// See LICENSE and https://rateslib.com/py/en/latest/i_licence.html for details, +// and/or contact info (at) rateslib (dot) com +//////////////////////////////////////////////////////////////////////////////////////////////////// + +use crate::scheduling::calendars::named::{HOLIDAYS, WEEKMASKS}; +use crate::scheduling::calendars::{Cal, CalWrapper, Calendar, NamedCal, UnionCal}; +use pyo3::exceptions::{PyKeyError, PyValueError}; +use pyo3::{pyclass, PyErr}; +use std::collections::HashMap; +use std::sync::{Arc, LazyLock, RwLock}; + +// A single memory allocated space to maintain the UnionCal with an associated name. +static NAMED_CALENDARS: LazyLock>>> = LazyLock::new(|| { + let mut m = HashMap::new(); + for (k, _) in WEEKMASKS.iter() { + m.insert( + (*k).into(), + Arc::new(CalWrapper::Cal(Cal::new( + HOLIDAYS.get(k).unwrap().to_vec(), + WEEKMASKS.get(k).unwrap().to_vec(), + ))), + ); + } + RwLock::new(m) +}); + +/// A manager to add and mutate the core calendars from which [`NamedCal`] are constructed. +#[pyclass] +pub struct CalendarManager; + +impl CalendarManager { + /// Create an instance of the [`CalendarManager`] manager. + /// + /// This object interacts with the memory allocation for stored calendars. It returns + /// objects with thread safe, shared memory access to the same objects for performance. + pub fn new() -> Self { + Self {} + } + + /// Returns *true* if the set contains a specific key. + pub fn contains_key(&self, key: &str) -> bool { + let k: String = sort_calendar_names(key); + let r = NAMED_CALENDARS.read().unwrap(); + r.contains_key(&k) + } + + /// Return a list of keys. + pub fn keys(&self) -> Vec { + let r = NAMED_CALENDARS.read().unwrap(); + r.iter().map(|(k, _)| k.to_string()).collect() + } + + /// Add any [`Calendar`] to the calendar manager. + /// + /// Data will not be overwritten. It will error prior to that or clone existing data to a + /// new key. + pub fn add(&self, name: &str, calendar: Cal) -> Result<(), PyErr> { + let k: String = sort_calendar_names(name); + if k.chars().any(|c| c == ',' || c == '|') { + return Err(PyValueError::new_err( + " + `name` cannot contain the comma (',') or pipe ('|') characters.\nThese are reserved + to define calendar combinations (i.e. UnionCal) and only Cal objects are allowed to be + populated directly to the calendar manager.", + )); + } + + let mut w = NAMED_CALENDARS.write().unwrap(); + if w.contains_key(&k) { + return Err(PyKeyError::new_err( + "`name` already exists in calendars. + Cannot overwrite, first `pop` the existing calendar.", + )); + } + w.insert(k, Arc::new(CalWrapper::Cal(calendar))); + Ok(()) + } + + /// Remove an existing [`Calendar`] from the calendar manager. + pub fn pop(&self, name: &str) -> Result { + let k: String = sort_calendar_names(name); + let popped = remove_any_calendar(&k); + match popped { + Some(arc) => match &*arc { + CalWrapper::Cal(c) => { + remove_all_combinations(&k); + Ok(Calendar::Cal(c.clone())) + } + CalWrapper::UnionCal(c) => Ok(Calendar::UnionCal(c.clone())), + }, + None => Err(PyKeyError::new_err("`name` does not exist in calendars.")), + } + } + + /// Return a [`NamedCal`] matching the name that is stored in the calendar manager. + /// + /// If the name as a key does not exist then an error will result. + pub fn get(&self, name: &str) -> Result { + let k: String = sort_calendar_names(name); + let r = NAMED_CALENDARS.read().unwrap(); + let v = r.get(&k); + match v { + Some(arc_ref) => Ok(NamedCal { + name: k, + inner: arc_ref.clone(), + }), + None => Err(PyKeyError::new_err("`name` does not exist in calendars.")), + } + } + + /// Return a [`NamedCal`] matching the name that is stored in the calendar manager. + /// + /// If the name as a key does not exist but a [`UnionCal`] as a combination of [`Cal`] can + /// be created, the HashMap will be updated with a new entry and the relevant [`NamedCal`] + /// returned. + pub fn get_with_insert(&self, name: &str) -> Result { + let k: String = sort_calendar_names(name); + if !k.chars().any(|c| c == ',' || c == '|') { + // then lookup is for a single calendar, no composition necessary + self.get(&k) + } else { + let item = self.get(&k); + match item { + Ok(value) => Ok(value), // key is found pre-populated in HashMap + Err(_) => { + // then the calendars might need to be composited and inserted + let data = extract_individual_calendars(&k)?; + let _ = insert_union_cal( + &k, + UnionCal { + calendars: data.0, + settlement_calendars: data.1, + }, + ); + self.get(&k) + } + } + } + } +} + +// Take an input string (potentially with comma and pipe) and convert to lower case and +// order the specific calendar names. See test_sort_calendar_names. +fn sort_calendar_names(name: &str) -> String { + let stripped: String = name.chars().filter(|c| !c.is_whitespace()).collect(); + let parts: Vec = stripped + .to_lowercase() + .split("|") + .map(String::from) + .collect(); + let mut reordered_parts: Vec = Vec::new(); + for part in parts { + let mut cals: Vec = part.split(",").map(String::from).collect(); + cals.sort(); + reordered_parts.push(cals.join(",")) + } + reordered_parts.join("|") +} + +// Take an input string (potentially with comma and pipe) and extract the ordered list +// of individual, expected [`Cal`] objects. `k` is expected to be cleaned (sorted, lowercase etc.) +fn extract_individual_calendars(k: &str) -> Result<(Vec, Option>), PyErr> { + let nc = CalendarManager::new(); + let parts: Vec = k.split("|").map(String::from).collect(); + let mut container: Vec> = Vec::new(); + for part in &parts { + let cal_names: Vec = part.split(",").map(String::from).collect(); + + let named_cals: Vec = cal_names + .iter() + .map(|k| nc.get(k)) + .collect::, _>>()?; + + let cals: Vec = named_cals + .iter() + .map(|n| match &*n.inner { + CalWrapper::Cal(value) => Ok(value.clone()), + _ => Err(PyValueError::new_err( + "Individual calendar name is not a Cal object.", + )), + }) + .collect::, _>>()?; + + container.push(cals); + } + if container.len() == 1 { + Ok((container[0].clone(), None)) + } else if parts.len() == 2 { + Ok((container[0].clone(), Some(container[1].clone()))) + } else { + Err(PyValueError::new_err( + "The calendar cannot be parsed. Is there more than one pipe character?", + )) + } +} + +// Insert a named calendar to the HashMap +fn insert_union_cal(k: &str, u: UnionCal) -> Option> { + // returns None when inserted correctly + let mut w = NAMED_CALENDARS.write().unwrap(); + w.insert(k.to_string(), Arc::new(CalWrapper::UnionCal(u))) +} + +// Remove a key and return the object +fn remove_any_calendar(k: &str) -> Option> { + let mut w = NAMED_CALENDARS.write().unwrap(); + w.remove(&k.to_string()) +} + +// Remove all other combinations that is a UnionCal and contains the name 'k'. +fn remove_all_combinations(k: &str) -> () { + let mut w = NAMED_CALENDARS.write().unwrap(); + let keys: Vec = w + .iter() + .filter(|(key, v)| key.contains(k) && is_union_cal((*v).clone())) + .map(|(key, _)| key.to_string()) + .collect(); + for key in keys.into_iter() { + let _ = w.remove(&key); + } +} + +fn is_union_cal(v: Arc) -> bool { + match *v { + CalWrapper::Cal(_) => false, + CalWrapper::UnionCal(_) => true, + } +} + +// UNIT TESTS +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_sort_calendar_names() { + let result = sort_calendar_names("tgt,NYC, ldn|tyo, tro"); + assert_eq!(result, "ldn,nyc,tgt|tro,tyo"); + + let result = sort_calendar_names("tgt,NYC, ldn|tyo"); + assert_eq!(result, "ldn,nyc,tgt|tyo"); + + let result = sort_calendar_names("tgt,NYC, ldn "); + assert_eq!(result, "ldn,nyc,tgt"); + + let result = sort_calendar_names("tgt|ldn "); + assert_eq!(result, "tgt|ldn"); + + let result = sort_calendar_names("tgt "); + assert_eq!(result, "tgt"); + + let result = sort_calendar_names("a2, a1 | a3 "); + assert_eq!(result, "a1,a2|a3"); + } + + #[test] + fn test_extract_individual_calendars() { + let nc = CalendarManager::new(); + let result = extract_individual_calendars("ldn").unwrap(); + let expected = nc.get("ldn").unwrap(); + assert_eq!(result.0[0], expected); + + let a1 = Cal::new(vec![], vec![1]); + let a2 = Cal::new(vec![], vec![2]); + let a3 = Cal::new(vec![], vec![3]); + let _ = nc.add("a1", a1); + let _ = nc.add("a2", a2); + let _ = nc.add("a3", a3); + + let result = extract_individual_calendars("a2, a1 | a3").unwrap(); + let expected = ( + vec![Cal::new(vec![], vec![2]), Cal::new(vec![], vec![1])], + Some(vec![Cal::new(vec![], vec![3])]), + ); + assert_eq!(result, expected) + } + + #[test] + fn test_get_with_insert() { + let nc = CalendarManager::new(); + let result = nc.get_with_insert("ldn").unwrap(); + let result2 = nc.get("ldn").unwrap(); + assert_eq!(result, result2); + assert!(Arc::ptr_eq(&result.inner, &result2.inner)); + } + + #[test] + fn test_get_with_insert_composite() { + let nc = CalendarManager::new(); + let result = nc.get_with_insert("ldn,tgt").unwrap(); + let result2 = nc.get("ldn,tgt").unwrap(); + let result3 = nc.get("tgt,ldn").unwrap(); + assert_eq!(result, result2); + assert!(Arc::ptr_eq(&result.inner, &result2.inner)); + assert_eq!(result, result3); + assert!(Arc::ptr_eq(&result.inner, &result3.inner)); + } + + #[test] + fn test_remove_composites_calendars() { + let nc = CalendarManager::new(); + + let a1 = Cal::new(vec![], vec![1]); + let a2 = Cal::new(vec![], vec![2]); + let a3 = Cal::new(vec![], vec![3]); + let _ = nc.add("a1", a1); + let _ = nc.add("a2", a2); + let _ = nc.add("a3", a3); + let _ = nc.get_with_insert("a1,a2"); + let _ = nc.get_with_insert("a1,a3"); + let _ = nc.get_with_insert("a2,a3"); + let _ = nc.get_with_insert("a1,a2,a3"); + + let _ = nc.pop("a1"); + assert!(!nc.keys().contains(&"a1,a2".to_string())); + assert!(!nc.keys().contains(&"a1,a3".to_string())); + assert!(nc.keys().contains(&"a2,a3".to_string())); + assert!(!nc.keys().contains(&"a1,a2,a3".to_string())); + } +} diff --git a/rust/scheduling/calendars/mod.rs b/rust/scheduling/calendars/mod.rs index f21ba2ba7..1faf62c93 100644 --- a/rust/scheduling/calendars/mod.rs +++ b/rust/scheduling/calendars/mod.rs @@ -14,6 +14,7 @@ mod adjuster; mod cal; mod calendar; mod dateroll; +mod manager; mod named; mod named_cal; mod union_cal; @@ -23,6 +24,36 @@ pub use crate::scheduling::calendars::{ cal::Cal, calendar::{ndt, Calendar}, dateroll::DateRoll, + manager::CalendarManager, named_cal::NamedCal, union_cal::UnionCal, }; + +pub(crate) use crate::scheduling::calendars::named_cal::CalWrapper; + +macro_rules! impl_date_roll_partial_eq { + ($t1:ty, $t2:ty) => { + // Implement T1 == T2 + impl PartialEq<$t2> for $t1 { + fn eq(&self, other: &$t2) -> bool { + let c = self + .cal_date_range(&ndt(1970, 1, 1), &ndt(2200, 12, 31)) + .unwrap(); + c.iter().all(|d| { + self.is_bus_day(d) == other.is_bus_day(d) + && self.is_settlement(d) == other.is_settlement(d) + }) + } + } + }; +} + +// Usage: Just list the pairs you want to support +impl_date_roll_partial_eq!(Cal, UnionCal); +impl_date_roll_partial_eq!(Cal, NamedCal); +impl_date_roll_partial_eq!(UnionCal, Cal); +impl_date_roll_partial_eq!(UnionCal, UnionCal); +impl_date_roll_partial_eq!(UnionCal, NamedCal); +impl_date_roll_partial_eq!(NamedCal, Cal); +impl_date_roll_partial_eq!(NamedCal, UnionCal); +impl_date_roll_partial_eq!(NamedCal, NamedCal); diff --git a/rust/scheduling/calendars/named/mod.rs b/rust/scheduling/calendars/named/mod.rs index 45d448481..3715d9b9d 100644 --- a/rust/scheduling/calendars/named/mod.rs +++ b/rust/scheduling/calendars/named/mod.rs @@ -32,12 +32,11 @@ pub mod wlg; pub mod zur; use chrono::NaiveDateTime; -use pyo3::exceptions::PyValueError; -use pyo3::PyErr; use std::collections::HashMap; +use std::sync::LazyLock; -pub(crate) fn get_weekmask_by_name(name: &str) -> Result, PyErr> { - let hmap: HashMap<&str, &[u8]> = HashMap::from([ +pub(crate) static WEEKMASKS: LazyLock> = LazyLock::new(|| { + HashMap::from([ ("all", all::WEEKMASK), ("bus", bus::WEEKMASK), ("bjs", bjs::WEEKMASK), @@ -55,18 +54,11 @@ pub(crate) fn get_weekmask_by_name(name: &str) -> Result, PyErr> { ("wlg", wlg::WEEKMASK), ("mum", mum::WEEKMASK), ("mex", mex::WEEKMASK), - ]); - match hmap.get(name) { - None => Err(PyValueError::new_err(format!( - "'{}' is not found in list of existing calendars.", - name - ))), - Some(value) => Ok(value.to_vec()), - } -} + ]) +}); -pub(crate) fn get_holidays_by_name(name: &str) -> Result, PyErr> { - let hmap: HashMap<&str, &[&str]> = HashMap::from([ +pub(crate) static HOLIDAYS: LazyLock>> = LazyLock::new(|| { + let temp = HashMap::<&str, &[&str]>::from([ ("all", all::HOLIDAYS), ("bus", bus::HOLIDAYS), ("bjs", bjs::HOLIDAYS), @@ -85,67 +77,18 @@ pub(crate) fn get_holidays_by_name(name: &str) -> Result, PyE ("mum", mum::HOLIDAYS), ("mex", mex::HOLIDAYS), ]); - match hmap.get(name) { - None => Err(PyValueError::new_err(format!( - "'{}' is not found in list of existing calendars.", - name - ))), - Some(value) => Ok(value - .iter() - .map(|x| NaiveDateTime::parse_from_str(x, "%Y-%m-%d %H:%M:%S").unwrap()) - .collect()), + let mut m: HashMap<&str, Vec> = HashMap::new(); + for (k, v) in temp.into_iter() { + m.insert( + k, + v.iter() + .map(|x| NaiveDateTime::parse_from_str(x, "%Y-%m-%d %H:%M:%S").unwrap()) + .collect(), + ); } -} - -// fn get_rules_by_name(name: &str) -> Result, PyErr> { -// let hmap: HashMap<&str, &[&str]> = HashMap::from([ -// ("all", all::RULES), -// ("bus", bus::RULES), -// ("bjs", bjs::RULES), -// ("nyc", nyc::RULES), -// ("fed", fed::RULES), -// ("tgt", tgt::RULES), -// ("ldn", ldn::RULES), -// ("stk", stk::RULES), -// ("osl", osl::RULES), -// ("zur", zur::RULES), -// ("tro", tro::RULES), -// ("tyo", tyo::RULES), -// ("syd", syd::RULES), -// ("nsw", nsw::RULES), -// ("wlg", wlg::RULES), -// ("mum", mum::RULES), -// ("mex", mex::RULES), -// ]); -// match hmap.get(name) { -// None => Err(PyValueError::new_err(format!( -// "'{}' is not found in list of existing calendars.", -// name -// ))), -// Some(value) => Ok(value.to_vec()), -// } -// } + m +}); // UNIT TESTS #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_get_weekmask() { - let result = get_weekmask_by_name("bus").unwrap(); - assert_eq!(result, vec![5, 6]); - } - - #[test] - fn test_get_holidays() { - let result = get_holidays_by_name("bus").unwrap(); - assert_eq!(result, vec![]); - } - - // #[test] - // fn test_get_rules() { - // let result = get_rules_by_name("bus").unwrap(); - // assert_eq!(result, Vec::<&str>::new()); - // } -} +mod tests {} diff --git a/rust/scheduling/calendars/named_cal.rs b/rust/scheduling/calendars/named_cal.rs index 6b874356a..525bee626 100644 --- a/rust/scheduling/calendars/named_cal.rs +++ b/rust/scheduling/calendars/named_cal.rs @@ -14,9 +14,41 @@ use chrono::prelude::*; use pyo3::exceptions::PyValueError; use pyo3::{pyclass, PyErr}; use serde::{Deserialize, Serialize}; +use std::sync::Arc; use crate::scheduling::{Cal, CalendarAdjustment, DateRoll, UnionCal}; +#[derive(Clone, Debug)] +pub(crate) enum CalWrapper { + Cal(Cal), + UnionCal(UnionCal), +} + +impl DateRoll for CalWrapper { + fn is_weekday(&self, date: &NaiveDateTime) -> bool { + match self { + CalWrapper::Cal(c) => c.is_weekday(date), + CalWrapper::UnionCal(c) => c.is_weekday(date), + } + } + + fn is_holiday(&self, date: &NaiveDateTime) -> bool { + match self { + CalWrapper::Cal(c) => c.is_holiday(date), + CalWrapper::UnionCal(c) => c.is_holiday(date), + } + } + + fn is_settlement(&self, date: &NaiveDateTime) -> bool { + match self { + CalWrapper::Cal(c) => c.is_settlement(date), + CalWrapper::UnionCal(c) => c.is_settlement(date), + } + } +} + +impl CalendarAdjustment for CalWrapper {} + /// A wrapper for a UnionCal struct specified by a string representation. #[pyclass(module = "rateslib.rs", from_py_object)] #[derive(Clone, Debug, Serialize, Deserialize)] @@ -24,7 +56,7 @@ use crate::scheduling::{Cal, CalendarAdjustment, DateRoll, UnionCal}; pub struct NamedCal { pub name: String, #[serde(skip)] - pub union_cal: UnionCal, + pub(crate) inner: Arc, } #[derive(Deserialize)] @@ -64,20 +96,20 @@ impl NamedCal { let cals: Vec = parse_cals(parts[0])?; Ok(Self { name: name_, - union_cal: UnionCal { + inner: Arc::new(CalWrapper::UnionCal(UnionCal { calendars: cals, settlement_calendars: None, - }, + })), }) } else { let cals: Vec = parse_cals(parts[0])?; let settle_cals: Vec = parse_cals(parts[1])?; Ok(Self { name: name_, - union_cal: UnionCal { + inner: Arc::new(CalWrapper::UnionCal(UnionCal { calendars: cals, settlement_calendars: Some(settle_cals), - }, + })), }) } } @@ -85,15 +117,15 @@ impl NamedCal { impl DateRoll for NamedCal { fn is_weekday(&self, date: &NaiveDateTime) -> bool { - self.union_cal.is_weekday(date) + self.inner.is_weekday(date) } fn is_holiday(&self, date: &NaiveDateTime) -> bool { - self.union_cal.is_holiday(date) + self.inner.is_holiday(date) } fn is_settlement(&self, date: &NaiveDateTime) -> bool { - self.union_cal.is_settlement(date) + self.inner.is_settlement(date) } } @@ -107,15 +139,6 @@ fn parse_cals(name: &str) -> Result, PyErr> { Ok(cals) } -impl PartialEq for NamedCal -where - T: DateRoll, -{ - fn eq(&self, other: &T) -> bool { - self.union_cal.eq(other) - } -} - // UNIT TESTS #[cfg(test)] mod tests { diff --git a/rust/scheduling/calendars/union_cal.rs b/rust/scheduling/calendars/union_cal.rs index 5925201d6..1de285646 100644 --- a/rust/scheduling/calendars/union_cal.rs +++ b/rust/scheduling/calendars/union_cal.rs @@ -11,10 +11,11 @@ //////////////////////////////////////////////////////////////////////////////////////////////////// use chrono::prelude::*; -use pyo3::pyclass; +use pyo3::exceptions::PyKeyError; +use pyo3::{pyclass, PyErr}; use serde::{Deserialize, Serialize}; -use crate::scheduling::{ndt, Cal, CalendarAdjustment, DateRoll}; +use crate::scheduling::{Cal, CalWrapper, CalendarAdjustment, CalendarManager, DateRoll}; /// A business day calendar which is the potential union of multiple calendars. /// @@ -51,6 +52,25 @@ impl UnionCal { settlement_calendars, } } + + /// Return a [`UnionCal`] specified by a pre-defined named identifier. + /// + /// # Examples + /// + /// ```rust + /// # use rateslib::scheduling::UnionCal; + /// let ldn_tgt_cal = UnionCal::try_from_name("ldn,tgt").unwrap(); + /// ``` + pub fn try_from_name(name: &str) -> Result { + let cm = CalendarManager::new(); + let named_cal = cm.get_with_insert(name)?; + match (*named_cal.inner).clone() { + CalWrapper::Cal(_) => Err(PyKeyError::new_err( + "`name` was key for a Cal not a UnionCal.", + )), + CalWrapper::UnionCal(cal) => Ok(cal), + } + } } impl DateRoll for UnionCal { @@ -71,28 +91,11 @@ impl DateRoll for UnionCal { impl CalendarAdjustment for UnionCal {} -impl PartialEq for UnionCal -where - T: DateRoll, -{ - fn eq(&self, other: &T) -> bool { - let cd1 = self - .cal_date_range(&ndt(1970, 1, 1), &ndt(2200, 12, 31)) - .unwrap(); - let cd2 = other - .cal_date_range(&ndt(1970, 1, 1), &ndt(2200, 12, 31)) - .unwrap(); - cd1.iter().zip(cd2.iter()).all(|(x, y)| { - self.is_bus_day(x) == other.is_bus_day(x) - && self.is_settlement(x) == other.is_settlement(y) - }) - } -} - // UNIT TESTS #[cfg(test)] mod tests { use super::*; + use crate::scheduling::ndt; fn fixture_hol_cal() -> Cal { let hols = vec![ndt(2015, 9, 5), ndt(2015, 9, 7)]; // Saturday and Monday diff --git a/rust/scheduling/mod.rs b/rust/scheduling/mod.rs index de3ddcbb1..6f4de6220 100644 --- a/rust/scheduling/mod.rs +++ b/rust/scheduling/mod.rs @@ -142,12 +142,15 @@ mod serde; pub(crate) mod py; +pub(crate) use crate::scheduling::{ + calendars::CalWrapper, frequency::get_unadjusteds, py::PyAdjuster, +}; pub use crate::scheduling::{ calendars::{ - ndt, Adjuster, Adjustment, Cal, Calendar, CalendarAdjustment, DateRoll, NamedCal, UnionCal, + ndt, Adjuster, Adjustment, Cal, Calendar, CalendarAdjustment, CalendarManager, DateRoll, + NamedCal, UnionCal, }, convention::Convention, frequency::{Frequency, Imm, RollDay, Scheduling}, schedule::{Schedule, StubInference}, }; -pub(crate) use crate::scheduling::{frequency::get_unadjusteds, py::PyAdjuster}; diff --git a/rust/scheduling/py/calendar.rs b/rust/scheduling/py/calendar.rs index 814622d7d..d17de66e9 100644 --- a/rust/scheduling/py/calendar.rs +++ b/rust/scheduling/py/calendar.rs @@ -16,8 +16,8 @@ use crate::json::json_py::DeserializedObj; use crate::json::JSON; use crate::scheduling::py::adjuster::get_roll_adjuster_from_str; use crate::scheduling::{ - Adjuster, Adjustment, Cal, Calendar, CalendarAdjustment, DateRoll, NamedCal, PyAdjuster, - RollDay, UnionCal, + Adjuster, Adjustment, Cal, CalWrapper, Calendar, CalendarAdjustment, CalendarManager, DateRoll, + NamedCal, PyAdjuster, RollDay, UnionCal, }; use chrono::NaiveDateTime; use indexmap::set::IndexSet; @@ -25,6 +25,84 @@ use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use pyo3::types::PyType; use std::collections::HashSet; +use std::sync::Arc; + +#[pymethods] +impl CalendarManager { + /// Create a new calendar manager object. + /// + /// .. warning:: + /// + /// Use the ``calendars`` object specifically. It is not be necessary to create your + /// own calendar manager, which refers to the same underlying data on the heap. + /// + #[new] + fn new_py() -> Self { + CalendarManager::new() + } + + /// Add a :class:`~rateslib.scheduling.Cal` to the calendar manager. + /// + /// Parameters + /// ----------- + /// name: str + /// The name of the calendar to add, cannot use a comma (',') or pipe ('|') character. + /// calendar: Cal + /// The :class:`~rateslib.scheduling.Cal` object to add to the manager. + /// + /// Returns + /// -------- + /// None + #[pyo3(name = "add")] + fn add_py(&self, name: &str, calendar: Cal) -> PyResult<()> { + self.add(name, calendar) + } + + /// Pop a :class:`~rateslib.scheduling.Cal` or :class:`~rateslib.scheduling.UnionCal` + /// from the calendar manager. + /// + /// Parameters + /// ----------- + /// name: str + /// The name of the calendar to remove, which already exists in the manager. + /// + /// Returns + /// -------- + /// Cal, UnionCal + #[pyo3(name = "pop")] + pub fn pop_py(&self, name: &str) -> Result { + self.pop(name) + } + + /// Get a :class:`~rateslib.scheduling.NamedCal` from the calendar manager. + /// + /// Parameters + /// ----------- + /// name: str + /// The name of the calendar to lookup. + /// + /// Returns + /// -------- + /// NamedCal + #[pyo3(name = "get")] + pub fn get_py(&self, name: &str) -> Result { + self.get_with_insert(name) + } + + fn __contains__(&self, item: &str) -> bool { + self.contains_key(item) + } + + /// Get a list of calendar names in the map. + /// + /// Returns + /// -------- + /// list of str + #[pyo3(name = "keys")] + fn keys_py(&self) -> Vec { + self.keys() + } +} #[pymethods] impl Cal { @@ -459,6 +537,21 @@ impl UnionCal { Ok(UnionCal::new(calendars, settlement_calendars)) } + /// Create a new *UnionCal* object from simple string name. + /// Parameters + /// ---------- + /// name: str + /// The string identifier for the calendar to load. + /// + /// Returns + /// ------- + /// UnionCal + #[classmethod] + #[pyo3(name = "from_name")] + fn from_name_py(_cls: &Bound<'_, PyType>, name: String) -> PyResult { + UnionCal::try_from_name(&name) + } + /// A list of specifically provided non-business days. #[getter] fn holidays(&self) -> PyResult> { @@ -725,13 +818,19 @@ impl NamedCal { /// A list of specifically provided non-business days. #[getter] fn holidays(&self) -> PyResult> { - self.union_cal.holidays() + match &*self.inner { + CalWrapper::Cal(c) => c.holidays(), + CalWrapper::UnionCal(c) => c.holidays(), + } } /// A list of days in the week defined as weekends. #[getter] fn week_mask(&self) -> PyResult> { - self.union_cal.week_mask() + match &*self.inner { + CalWrapper::Cal(c) => c.week_mask(), + CalWrapper::UnionCal(c) => c.week_mask(), + } } /// The string identifier for this constructed calendar. @@ -740,10 +839,27 @@ impl NamedCal { self.name.clone() } - /// The wrapped :class:`~rateslib.scheduling.UnionCal` object. + /// The wrapped :class:`~rateslib.scheduling.UnionCal` or :class:`~rateslib.scheduling.Cal` object. #[getter] - fn union_cal(&self) -> UnionCal { - self.union_cal.clone() + fn inner(&self) -> Calendar { + match (*self.inner).clone() { + CalWrapper::Cal(c) => Calendar::Cal(c), + CalWrapper::UnionCal(c) => Calendar::UnionCal(c), + } + } + + /// Check whether the memory allocation of the calendar object matches that of another. + /// + /// Parameters + /// ----------- + /// other: NamedCal + /// The other :class:`~rateslib.scheduling.NamedCal` to test memory allocation against. + /// + /// Returns + /// -------- + /// bool + fn inner_ptr_eq(&self, other: NamedCal) -> bool { + Arc::ptr_eq(&self.inner, &other.inner) } /// Return whether the `date` is a business day.