diff --git a/docs/source/examples.rst b/docs/source/examples.rst index 2d6c084c8..f6c06079e 100644 --- a/docs/source/examples.rst +++ b/docs/source/examples.rst @@ -123,6 +123,40 @@ fly and the holiday list will be adjusted accordingly: >> date(2012, 1, 2) in us_holidays True +Date range limits: since and until parameters +--------------------------------------------- + +If you need to retrieve a set of holidays limited by specific date(s) you can +use :py:attr:`since` and/or :py:attr:`until` parameters. Both since/until +parameters accept :py:class:`DateLike` objects. Please note that the +resulting set will include holidays falling on :py:attr:`since` and +:py:attr:`until` dates too (inclusive range). + +Here is an example of retrieving US holidays for January of 2024: + +.. code-block:: python + + >>> us_holidays = holidays.US(years=2024, since="2024-01-01", until="2024-01-31") + >>> print(us_holidays) + {datetime.date(2024, 1, 1): "New Year's Day", datetime.date(2024, 1, 15): 'Martin Luther King Jr. Day'} + >>> "2024-07-04" in us_holidays + False + +Here is another example of using :py:attr:`since` parameter to get holidays for the second half of 2024: + +.. code-block:: python + + >>> from datetime import date + >>> for dt, name in sorted(holidays.US(years=2024, since=date(2024, 7, 1)).items()): + >>> print(dt, name) + 2024-07-04 Independence Day + 2024-09-02 Labor Day + 2024-10-14 Columbus Day + 2024-11-11 Veterans Day + 2024-11-28 Thanksgiving + 2024-12-25 Christmas Day + + Language support ---------------- To change the language translation, you can set the language explicitly. diff --git a/holidays/__init__.py b/holidays/__init__.py index 9a602d35b..221801584 100644 --- a/holidays/__init__.py +++ b/holidays/__init__.py @@ -21,6 +21,7 @@ ) from holidays.holiday_base import * from holidays.registry import EntityLoader +from holidays.types import DateLike # noqa: F401 from holidays.utils import * __version__ = "0.52" diff --git a/holidays/countries/angola.py b/holidays/countries/angola.py index 4cf4d0d2a..ac75ee720 100644 --- a/holidays/countries/angola.py +++ b/holidays/countries/angola.py @@ -60,6 +60,9 @@ def _is_observed(self, dt: date) -> bool: return dt >= date(1996, SEP, 27) def _add_observed(self, dt: date, **kwargs) -> Tuple[bool, Optional[date]]: + if dt is None: + return False, None + # As per Law # #11/18, from 2018/9/10, when public holiday falls on Tuesday or Thursday, # the Monday or Friday is also a holiday. kwargs.setdefault( diff --git a/holidays/countries/bulgaria.py b/holidays/countries/bulgaria.py index 9403f6b9f..8487d32c6 100644 --- a/holidays/countries/bulgaria.py +++ b/holidays/countries/bulgaria.py @@ -59,7 +59,7 @@ def __init__(self, *args, **kwargs): def _populate_observed(self, dts: Set[date], excluded_names: Set[str]) -> None: for dt in sorted(dts): - if not self._is_observed(dt): + if dt is None or not self._is_observed(dt): continue for name in self.get_list(dt): if name not in excluded_names: diff --git a/holidays/countries/cambodia.py b/holidays/countries/cambodia.py index a74e1a8f6..b9b61b315 100644 --- a/holidays/countries/cambodia.py +++ b/holidays/countries/cambodia.py @@ -97,8 +97,9 @@ def _populate_public_holidays(self): if self._year in sangkranta_years_apr_14 else self._add_holiday_apr_13(sangkranta) ) - self._add_holiday(sangkranta, _timedelta(dt, +1)) - self._add_holiday(sangkranta, _timedelta(dt, +2)) + if dt is not None: + self._add_holiday(sangkranta, _timedelta(dt, +1)) + self._add_holiday(sangkranta, _timedelta(dt, +2)) # ទិវាពលកម្មអន្តរជាតិ # Status: In-Use. diff --git a/holidays/countries/finland.py b/holidays/countries/finland.py index 951f9b095..cda6842fa 100644 --- a/holidays/countries/finland.py +++ b/holidays/countries/finland.py @@ -71,8 +71,9 @@ def _populate_public_holidays(self): else: dt = self._add_holiday_jun_23(name) - # Midsummer Day. - self._add_holiday(tr("Juhannuspäivä"), _timedelta(dt, +1)) + if dt is not None: + # Midsummer Day. + self._add_holiday(tr("Juhannuspäivä"), _timedelta(dt, +1)) # All Saints' Day. name = tr("Pyhäinpäivä") diff --git a/holidays/countries/japan.py b/holidays/countries/japan.py index 83f3793c2..f9a7b824f 100644 --- a/holidays/countries/japan.py +++ b/holidays/countries/japan.py @@ -55,6 +55,8 @@ def _populate_observed(self, dts: Set[date]) -> None: # When a national holiday falls on Sunday, next working day # shall become a public holiday (振替休日) - substitute holidays. for dt in sorted(dts): + if dt is None: + continue is_observed, dt_observed = self._add_observed( dt, # Substitute Holiday. @@ -67,7 +69,7 @@ def _populate_observed(self, dts: Set[date]) -> None: # A weekday between national holidays becomes # a holiday too (国民の休日) - national holidays. for dt in dts: - if _timedelta(dt, +2) not in dts: + if dt is None or _timedelta(dt, +2) not in dts: continue dt_observed = _timedelta(dt, +1) if self._is_sunday(dt_observed) or dt_observed in dts: diff --git a/holidays/countries/jersey.py b/holidays/countries/jersey.py index eed22b5c5..1f3dd69e3 100644 --- a/holidays/countries/jersey.py +++ b/holidays/countries/jersey.py @@ -57,6 +57,9 @@ def __init__(self, *args, **kwargs): ObservedHolidayBase.__init__(self, *args, **kwargs) def _add_observed(self, dt: date, **kwargs) -> Tuple[bool, Optional[date]]: + if dt is None: + return False, None + # Prior to 2004, in-lieu are only given for Sundays. # https://www.jerseylaw.je/laws/enacted/Pages/RO-123-2004.aspx kwargs.setdefault( diff --git a/holidays/countries/moldova.py b/holidays/countries/moldova.py index 9f8d42afb..e1016c7af 100644 --- a/holidays/countries/moldova.py +++ b/holidays/countries/moldova.py @@ -70,7 +70,7 @@ def _populate_public_holidays(self): tr("Ziua Victoriei și a comemorării eroilor căzuţi pentru Independenţa Patriei") ) - if self._year >= 2017: + if self._year >= 2017 and may_9 is not None: # Europe Day. self._add_holiday(tr("Ziua Europei"), may_9) diff --git a/holidays/countries/south_korea.py b/holidays/countries/south_korea.py index bee6e04ef..f67b5dee8 100644 --- a/holidays/countries/south_korea.py +++ b/holidays/countries/south_korea.py @@ -105,7 +105,7 @@ def __init__(self, *args, **kwargs): def _populate_observed(self, dts: Set[date], three_day_holidays: Dict[date, str]) -> None: for dt in sorted(dts.union(three_day_holidays.keys())): - if not self._is_observed(dt): + if dt is None or not self._is_observed(dt): continue dt_observed = self._get_observed_date( dt, SUN_TO_NEXT_WORKDAY if dt in three_day_holidays else SAT_SUN_TO_NEXT_WORKDAY @@ -125,6 +125,9 @@ def append_observed(dt: date, since: int): dts_observed.add(dt) def add_three_day_holiday(dt: date, name: str): + if dt is None: + return None + name = self.tr(name) for dt_alt in ( # The day preceding %s. diff --git a/holidays/countries/sweden.py b/holidays/countries/sweden.py index 9302d0fa1..a63c0a2df 100644 --- a/holidays/countries/sweden.py +++ b/holidays/countries/sweden.py @@ -99,8 +99,9 @@ def _populate_public_holidays(self): else self._add_holiday_jun_23(name) ) - # Midsummer Day. - self._add_holiday(tr("Midsommardagen"), _timedelta(dt, +1)) + if dt is not None: + # Midsummer Day. + self._add_holiday(tr("Midsommardagen"), _timedelta(dt, +1)) # All Saints' Day. self._add_holiday_1st_sat_from_oct_31(tr("Alla helgons dag")) diff --git a/holidays/countries/taiwan.py b/holidays/countries/taiwan.py index a2d9f51ae..69180df27 100644 --- a/holidays/countries/taiwan.py +++ b/holidays/countries/taiwan.py @@ -66,6 +66,8 @@ def _populate_observed( if self._year < since: return None for dt in sorted(dts): + if dt is None: + continue for name in self.get_list(dt): self._add_observed(dt, name, rule) diff --git a/holidays/helpers.py b/holidays/helpers.py index 261e8230f..25279b8b5 100644 --- a/holidays/helpers.py +++ b/holidays/helpers.py @@ -10,6 +10,52 @@ # Website: https://github.com/vacanza/python-holidays # License: MIT (see LICENSE file) +from datetime import date, datetime, timezone + +from dateutil.parser import parse + +from holidays.types import DateLike + + +def _convert_to_date(dt: DateLike) -> date: + """Convert value to date. + + :param dt: + A value that should be converted into a date. + + :return: + A date created based on the provided value. + """ + + # Attempt to catch `date` and `str` type keys first. + # Using `type()`` instead of `isinstance()` here to skip date subclasses. + # Key is `date`. + if type(dt) is date: + return dt + + # Key is `str` instance. + elif isinstance(dt, str): + try: + return parse(dt).date() + except (OverflowError, ValueError): + raise ValueError(f"Cannot parse date from string '{dt}'") + + # Key is `datetime` instance. + elif isinstance(dt, datetime): + return dt.date() + + # Must go after the `isinstance(key, datetime)` check as datetime is `date` subclass. + elif isinstance(dt, date): + return dt + + # Key is `float` or `int` instance. + elif isinstance(dt, (float, int)): + return datetime.fromtimestamp(dt, timezone.utc).date() + + # Key is not supported. + else: + raise TypeError(f"Cannot convert type '{type(dt)}' to date.") + def _normalize_arguments(cls, value): """Normalize arguments. diff --git a/holidays/holiday_base.py b/holidays/holiday_base.py index fc5dd1c75..14d7a9a1c 100644 --- a/holidays/holiday_base.py +++ b/holidays/holiday_base.py @@ -15,13 +15,11 @@ import copy import warnings from calendar import isleap -from datetime import date, datetime, timedelta, timezone +from datetime import date, datetime, timedelta from functools import cached_property from gettext import gettext, translation from pathlib import Path -from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Union, cast - -from dateutil.parser import parse +from typing import Any, Dict, List, Optional, Set, Tuple, Union, cast from holidays.calendars.gregorian import ( MON, @@ -39,17 +37,8 @@ WEEKDAYS, ) from holidays.constants import HOLIDAY_NAME_DELIMITER, PUBLIC -from holidays.helpers import _normalize_arguments, _normalize_tuple - -CategoryArg = Union[str, Iterable[str]] -DateArg = Union[date, Tuple[int, int]] -DateLike = Union[date, datetime, str, float, int] -SpecialHoliday = Union[Tuple[int, int, str], Tuple[Tuple[int, int, str], ...]] -SubstitutedHoliday = Union[ - Union[Tuple[int, int, int, int], Tuple[int, int, int, int, int]], - Tuple[Union[Tuple[int, int, int, int], Tuple[int, int, int, int, int]], ...], -] -YearArg = Union[int, Iterable[int]] +from holidays.helpers import _convert_to_date, _normalize_arguments, _normalize_tuple +from holidays.types import CategoryArg, DateLike, SpecialHoliday, SubstitutedHoliday, YearArg class HolidayBase(Dict[date, str]): @@ -255,6 +244,8 @@ def __init__( state: Optional[str] = None, # Deprecated. language: Optional[str] = None, categories: Optional[CategoryArg] = None, + since: Optional[DateLike] = None, + until: Optional[DateLike] = None, ) -> None: """ :param years: @@ -288,6 +279,14 @@ def __init__( :param categories: Requested holiday categories. + :param since: + The date limiting the lower bound of holidays date range. + The holidays falling on the `since` date will be included too. + + :param until: + The date limiting the upper bound of holidays date range. + The holidays falling on the `until` date will be included too. + :return: A :class:`HolidayBase` object matching the **country**. """ @@ -350,6 +349,13 @@ def __init__( "and `substituted_date_format` attributes set." ) + self.since_date = _convert_to_date(since) if since is not None else since + self.until_date = _convert_to_date(until) if until is not None else until + if self.since_date and self.until_date and self.until_date < self.since_date: + raise ValueError( + "The holidays date range until date mustn't be earlier than since date." + ) + self.categories = categories self.expand = expand self.has_special_holidays = getattr(self, "has_special_holidays", False) @@ -581,37 +587,14 @@ def __keytransform__(self, key: DateLike) -> date: to :class:`datetime.date`, which is how it's stored by the class.""" - # Try to catch `date` and `str` type keys first. - # Using type() here to skip date subclasses. - # Key is `date`. - if type(key) is date: - dt = key - - # Key is `str` instance. - elif isinstance(key, str): - try: - dt = parse(key).date() - except (OverflowError, ValueError): - raise ValueError(f"Cannot parse date from string '{key}'") - - # Key is `datetime` instance. - elif isinstance(key, datetime): - dt = key.date() - - # Must go after the `isinstance(key, datetime)` check as datetime is `date` subclass. - elif isinstance(key, date): - dt = key - - # Key is `float` or `int` instance. - elif isinstance(key, (float, int)): - dt = datetime.fromtimestamp(key, timezone.utc).date() - - # Key is not supported. - else: - raise TypeError(f"Cannot convert type '{type(key)}' to date.") - - # Automatically expand for `expand=True` cases. - if self.expand and dt.year not in self.years: + dt = _convert_to_date(key) + # Automatically expand for `expand=True` cases unless since or until prohibits that. + if ( + self.expand + and dt.year not in self.years + and (self.since_date is None or dt.year >= self.since_date.year) + and (self.until_date is None or dt.year <= self.until_date.year) + ): self.years.add(dt.year) self._populate(dt.year) @@ -742,7 +725,12 @@ def _add_holiday(self, name: str, *args) -> Optional[date]: dt = args if len(args) > 1 else args[0] dt = dt if isinstance(dt, date) else date(self._year, *dt) - if dt.year != self._year: + # Skip dates that don't match current year and since/until range values. + if ( + dt.year != self._year + or (self.since_date and dt < self.since_date) + or (self.until_date and dt > self.until_date) + ): return None self[dt] = self.tr(name) @@ -778,6 +766,8 @@ def _check_weekday(self, weekday: int, *args) -> bool: Returns False otherwise. """ dt = args if len(args) > 1 else args[0] + if dt is None: + return False dt = dt if isinstance(dt, date) else date(self._year, *dt) return dt.weekday() == weekday diff --git a/holidays/observed_holiday_base.py b/holidays/observed_holiday_base.py index 5fc3a7d1a..059c9f8e3 100644 --- a/holidays/observed_holiday_base.py +++ b/holidays/observed_holiday_base.py @@ -14,7 +14,8 @@ from typing import Dict, Optional, Tuple, Set from holidays.calendars.gregorian import MON, TUE, WED, THU, FRI, SAT, SUN, _timedelta -from holidays.holiday_base import DateArg, HolidayBase +from holidays.holiday_base import HolidayBase +from holidays.types import DateArg class ObservedRule(Dict[int, Optional[int]]): @@ -142,6 +143,9 @@ def _add_observed( rule: Optional[ObservedRule] = None, show_observed_label: bool = True, ) -> Tuple[bool, Optional[date]]: + if dt is None: + return False, None + dt = dt if isinstance(dt, date) else date(self._year, *dt) if not self.observed or not self._is_observed(dt): @@ -199,7 +203,7 @@ def _populate_observed(self, dts: Set[date], multiple: bool = False) -> None: When multiple is True, each holiday from a given date has its own observed date. """ for dt in sorted(dts): - if not self._is_observed(dt): + if dt is None or not self._is_observed(dt): continue if multiple: for name in self.get_list(dt): diff --git a/holidays/types.py b/holidays/types.py new file mode 100644 index 000000000..8f814c40a --- /dev/null +++ b/holidays/types.py @@ -0,0 +1,28 @@ +# holidays +# -------- +# A fast, efficient Python library for generating country, province and state +# specific sets of holidays on the fly. It aims to make determining whether a +# specific date is a holiday as fast and flexible as possible. +# +# Authors: Vacanza Team and individual contributors (see AUTHORS file) +# dr-prodigy (c) 2017-2023 +# ryanss (c) 2014-2017 +# Website: https://github.com/vacanza/python-holidays +# License: MIT (see LICENSE file) + +from datetime import date, datetime +from typing import Iterable, Tuple, Union + +CategoryArg = Union[str, Iterable[str]] + +DateArg = Union[date, Tuple[int, int]] + +DateLike = Union[date, datetime, str, float, int] + +SpecialHoliday = Union[Tuple[int, int, str], Tuple[Tuple[int, int, str], ...]] + +SubstitutedHoliday = Union[ + Union[Tuple[int, int, int, int], Tuple[int, int, int, int, int]], + Tuple[Union[Tuple[int, int, int, int], Tuple[int, int, int, int, int]], ...], +] +YearArg = Union[int, Iterable[int]] diff --git a/holidays/utils.py b/holidays/utils.py index f3c368559..5c0434413 100755 --- a/holidays/utils.py +++ b/holidays/utils.py @@ -26,6 +26,7 @@ from holidays.holiday_base import HolidayBase from holidays.registry import EntityLoader +from holidays.types import DateLike def country_holidays( @@ -38,6 +39,8 @@ def country_holidays( state: Optional[str] = None, language: Optional[str] = None, categories: Optional[Tuple[str]] = None, + since: Optional[DateLike] = None, + until: Optional[DateLike] = None, ) -> HolidayBase: """ Returns a new dictionary-like :py:class:`HolidayBase` object for the public @@ -77,6 +80,14 @@ def country_holidays( :param categories: Requested holiday categories. + :param since: + The date limiting the lower bound of holidays date range. + The holidays falling on the `since` date will be included too. + + :param until: + The date limiting the upper bound of holidays date range. + The holidays falling on the `until` date will be included too. + :return: A :py:class:`HolidayBase` object matching the **country**. @@ -194,6 +205,8 @@ def country_holidays( state=state, language=language, categories=categories, + since=since, + until=until, ) except AttributeError: raise NotImplementedError(f"Country {country} not available") @@ -206,6 +219,8 @@ def financial_holidays( expand: bool = True, observed: bool = True, language: Optional[str] = None, + since: Optional[DateLike] = None, + until: Optional[DateLike] = None, ) -> HolidayBase: """ Returns a new dictionary-like :py:class:`HolidayBase` object for the public @@ -236,6 +251,14 @@ def financial_holidays( language translation is not supported the original holiday names will be used. + :param since: + The date limiting the lower bound of holidays date range. + The holidays falling on the `since` date will be included too. + + :param until: + The date limiting the upper bound of holidays date range. + The holidays falling on the `until` date will be included too. + :return: A :py:class:`HolidayBase` object matching the **market**. @@ -256,6 +279,8 @@ def financial_holidays( expand=expand, observed=observed, language=language, + since=since, + until=until, ) except AttributeError: raise NotImplementedError(f"Financial market {market} not available") diff --git a/tests/test_helpers.py b/tests/test_helpers.py new file mode 100644 index 000000000..d6a3b987c --- /dev/null +++ b/tests/test_helpers.py @@ -0,0 +1,45 @@ +# holidays +# -------- +# A fast, efficient Python library for generating country, province and state +# specific sets of holidays on the fly. It aims to make determining whether a +# specific date is a holiday as fast and flexible as possible. +# +# Authors: Vacanza Team and individual contributors (see AUTHORS file) +# dr-prodigy (c) 2017-2023 +# ryanss (c) 2014-2017 +# Website: https://github.com/vacanza/python-holidays +# License: MIT (see LICENSE file) + +import unittest +from datetime import date, datetime + +from holidays.helpers import _convert_to_date + + +class TestConvertToDate(unittest.TestCase): + def test_date(self): + dt = date(2014, 1, 1) + self.assertEqual(_convert_to_date(dt), dt) + + def test_date_subclass(self): + class CustomDateType(date): + pass + + self.assertEqual(CustomDateType(2014, 1, 1), date(2014, 1, 1)) + + def test_datetime(self): + self.assertEqual(_convert_to_date(datetime(2014, 1, 1, 13, 45)), date(2014, 1, 1)) + + def test_exception(self): + self.assertRaises((TypeError, ValueError), lambda: _convert_to_date("invalid string")) + self.assertRaises((TypeError, ValueError), lambda: _convert_to_date("abc123")) + self.assertRaises(TypeError, lambda: _convert_to_date({"123"})) + self.assertRaises((TypeError, ValueError), lambda: _convert_to_date([])) + + def test_string(self): + self.assertEqual(_convert_to_date("2014-01-03"), date(2014, 1, 3)) + self.assertEqual(_convert_to_date("01/03/2014"), date(2014, 1, 3)) + + def test_timestamp(self): + self.assertEqual(_convert_to_date(1388552400), date(2014, 1, 1)) + self.assertEqual(_convert_to_date(1388552400.01), date(2014, 1, 1)) diff --git a/tests/test_holiday_base.py b/tests/test_holiday_base.py index 0d1f3075c..b0eb1e6d1 100644 --- a/tests/test_holiday_base.py +++ b/tests/test_holiday_base.py @@ -178,6 +178,24 @@ def test_observed(self): self.assertIn("2012-01-01", hb) self.assertNotIn("2012-01-02", hb) + def test_since_until(self): + self.assertIsNone(HolidayBase().since_date) + self.assertIsNone(HolidayBase().until_date) + + dt_2010_12_31 = date(2010, 12, 31) + hb_2010_12_31 = HolidayBase(since=dt_2010_12_31, until=dt_2010_12_31) + self.assertEqual(hb_2010_12_31.since_date, dt_2010_12_31) + self.assertEqual(hb_2010_12_31.until_date, dt_2010_12_31) + + self.assertEqual(HolidayBase(since="20101231").since_date, dt_2010_12_31) + self.assertEqual(HolidayBase(until="20101231").until_date, dt_2010_12_31) + + self.assertEqual(HolidayBase(since="2010-12-31").since_date, dt_2010_12_31) + self.assertEqual(HolidayBase(until="2010-12-31").until_date, dt_2010_12_31) + + self.assertRaises(ValueError, lambda: HolidayBase(since="2020-12-01", until="2020-01-01")) + self.assertRaises(ValueError, lambda: HolidayBase(since="20210101", until="20201231")) + def test_subdivision(self): self.assertEqual(CountryStub1(subdiv="Subdiv 1").subdiv, "Subdiv 1") self.assertEqual(CountryStub1(subdiv=3).subdiv, "3") @@ -540,7 +558,109 @@ def test_add_holiday(self): (JAN, 5, "Test 1", True), ("Test", "Test"), ): - self.assertRaises(TypeError, lambda: self.hb._add_holiday(*args)) + self.assertRaises(TypeError, lambda a=args: self.hb._add_holiday(*a)) + + def test_add_holiday_since_until(self): + class SinceUntilHolidays(HolidayBase): + country = "SU" + + def _populate(self, year: int): + super()._populate(year) + + self._add_holiday_may_15("May 15") + self._add_holiday_may_16("May 16") + self._add_holiday_may_17("May 17") + + self._add_holiday_sep_1("Sep 1") + self._add_holiday_sep_2("Sep 2") + self._add_holiday_sep_3("Sep 3") + + may_holidays = ("2024-05-15", "2024-05-16", "2024-05-17") + sep_holidays = ("2024-09-01", "2024-09-02", "2024-09-03") + + # Holidays exist tests. + + # One day range. + since_2024_05_15_until_2024_05_15 = SinceUntilHolidays( + years=2024, since="2024-05-15", until="2024-05-15" + ) + self.assertIn("2024-05-15", since_2024_05_15_until_2024_05_15) + self.assertEqual(len(since_2024_05_15_until_2024_05_15), 1) + + # One month range. + since_2024_05_01_until_2024_05_31 = SinceUntilHolidays( + years=2024, since="2024-05-01", until="2024-05-31" + ) + for dt in may_holidays: + self.assertIn(dt, since_2024_05_01_until_2024_05_31) + for dt in sep_holidays: + self.assertNotIn(dt, since_2024_05_01_until_2024_05_31) + self.assertEqual(len(since_2024_05_01_until_2024_05_31), 3) + + # One year range. + since_2024_01_01_until_2024_12_31 = SinceUntilHolidays( + years=2024, since="2024-01-01", until="2024-12-31" + ) + for dt in may_holidays + sep_holidays: + self.assertIn(dt, since_2024_01_01_until_2024_12_31) + self.assertEqual(len(since_2024_01_01_until_2024_12_31), 6) + + # No holidays exist tests. + + # One day range. + since_2024_05_14_until_2024_05_14 = SinceUntilHolidays( + years=2024, since="2024-05-14", until="2024-05-14" + ) + for dt in may_holidays + sep_holidays: + self.assertNotIn(dt, since_2024_05_14_until_2024_05_14) + self.assertNotIn("2023-05-15", since_2024_05_14_until_2024_05_14) + self.assertEqual(len(since_2024_05_14_until_2024_05_14), 0) + + # One month range. + since_2024_04_01_until_2024_04_30 = SinceUntilHolidays( + years=2025, since="2024-04-01", until="2024-04-30" + ) + for dt in may_holidays + sep_holidays: + self.assertNotIn(dt, since_2024_04_01_until_2024_04_30) + self.assertEqual(len(since_2024_04_01_until_2024_04_30), 0) + + # Out of range since. + since_2024_09_04 = SinceUntilHolidays(years=2024, since="2024-09-04") + for dt in may_holidays + sep_holidays: + self.assertNotIn(dt, since_2024_09_04) + self.assertNotIn("2023-05-15", since_2024_09_04) + self.assertNotIn("2023-09-01", since_2024_09_04) + self.assertEqual(len(since_2024_09_04), 0) + + self.assertIn("2025-05-15", since_2024_09_04) + self.assertIn("2025-09-01", since_2024_09_04) + self.assertEqual(len(since_2024_09_04), 6) + + since_2024_09_04._populate(2000) + self.assertEqual(len(since_2024_09_04), 6) + since_2024_09_04._populate(2026) + self.assertEqual(len(since_2024_09_04), 12) + + # Out of range until. + until_2024_05_14 = SinceUntilHolidays(years=2024, until="2024-05-14") + for dt in may_holidays + sep_holidays: + self.assertNotIn(dt, until_2024_05_14) + self.assertNotIn("2025-05-15", until_2024_05_14) + self.assertNotIn("2025-09-01", until_2024_05_14) + self.assertEqual(len(until_2024_05_14), 0) + + self.assertIn("2023-05-15", until_2024_05_14) + self.assertIn("2023-09-01", until_2024_05_14) + self.assertEqual(len(until_2024_05_14), 6) + + until_2024_05_14._populate(2026) + self.assertEqual(len(until_2024_05_14), 6) + until_2024_05_14._populate(2022) + self.assertEqual(len(until_2024_05_14), 12) + + def test_check_weekday(self): + self.assertFalse(self.hb._is_sunday(None)) + self.assertFalse(self.hb._check_weekday(MON, None)) def test_is_leap_year(self): self.hb._populate(1999) diff --git a/tests/test_utils.py b/tests/test_utils.py index 660ea21f2..dc86ef577 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -88,7 +88,7 @@ def test_exceptions(self): self.assertRaises(NotImplementedError, lambda: financial_holidays("NYSE", subdiv="XXXX")) -class TestAllInSameYear(unittest.TestCase): +class TestYears(unittest.TestCase): """Test that only holidays in the year(s) requested are returned.""" years = set(range(1950, 2051)) @@ -114,6 +114,16 @@ def test_all_countries(self, unused_rglob_mock): for dt in country_holidays(country, years=year): self.assertEqual(dt.year, year) self.assertEqual(type(dt), date) + + # Test since/until range. + for dt in country_holidays( + country, years=range(1990, 2010), since="1999-01-01", until="2001-12-31" + ): + self.assertIn(dt.year, {1999, 2000, 2001}) + + self.assertFalse(country_holidays(country, years=2023, since="2024-01-01")) + self.assertFalse(country_holidays(country, years=2025, until="2024-12-31")) + self.assertEqual(self.years, country_holidays(country, years=self.years).years) @pytest.mark.skipif( @@ -137,6 +147,16 @@ def test_all_financial(self, unused_rglob_mock): for dt in financial_holidays(market, years=year): self.assertEqual(dt.year, year) self.assertEqual(type(dt), date) + + # Test since/until range. + for dt in financial_holidays( + market, years=range(2015, 2025), since="2020-01-01", until="2022-12-31" + ): + self.assertIn(dt.year, {2020, 2021, 2022}) + + self.assertFalse(country_holidays(market, years=2023, since="2024-01-01")) + self.assertFalse(country_holidays(market, years=2025, until="2024-12-31")) + self.assertEqual(self.years, financial_holidays(market, years=self.years).years)