Skip to content

Commit

Permalink
Update working day related calculations (#2010)
Browse files Browse the repository at this point in the history
Co-authored-by: Arkadii Yakovets <[email protected]>
Co-authored-by: Arkadii Yakovets <[email protected]>
  • Loading branch information
3 people authored Sep 25, 2024
1 parent 2e8bc56 commit 041681a
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 59 deletions.
36 changes: 36 additions & 0 deletions docs/source/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,42 @@ To get a list of other categories holidays (for countries that support them):
2023-12-25 Christmas Day
2023-12-26 Bank Holiday
Working day-related calculations
--------------------------------

To check if the specified date is a working day:

.. code-block:: python
>>> us_holidays = holidays.US(years=2024) # Weekends in the US are Saturday and Sunday.
>>> us_holidays.is_working_day("2024-01-01") # Monday, New Year's Day.
False
>>> us_holidays.is_working_day("2024-01-02") # Tuesday, ordinary day.
True
>>> us_holidays.is_working_day("2024-01-06") # Saturday, ordinary day.
False
>>> us_holidays.is_working_day("2024-01-15") # Monday, Martin Luther King Jr. Day.
False
To find the nth working day after the specified date:

.. code-block:: python
>>> us_holidays.get_nth_working_day("2024-12-20", 5)
datetime.date(2024, 12, 30)
Here we calculate the 5th working day after December 20, 2024. Working days are 23 (Mon),
24 (Tue), 26 (Thu), 27 (Fri), 30 (Mon); 21-22, 28-29 - weekends, 25 - Christmas Day.

To calculate the number or working days between two specified dates:

.. code-block:: python
>>> us_holidays.get_working_days_count("2024-04-01", "2024-06-30")
63
Here we calculate the number of working days in Q2 2024.

Date from holiday name
----------------------

Expand Down
30 changes: 19 additions & 11 deletions holidays/holiday_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -961,30 +961,38 @@ def get_named(

raise AttributeError(f"Unknown lookup type: {lookup}")

def get_nth_workday(self, key: DateLike, n: int) -> date:
def get_nth_working_day(self, key: DateLike, n: int) -> date:
"""Return n-th working day from provided date (if n is positive)
or n-th working day before provided date (if n is negative).
"""
direction = +1 if n > 0 else -1
dt = self.__keytransform__(key)
for _ in range(abs(n)):
dt = _timedelta(dt, direction)
while not self.is_workday(dt):
while not self.is_working_day(dt):
dt = _timedelta(dt, direction)
return dt

def get_workdays_number(self, key1: DateLike, key2: DateLike) -> int:
"""Return the number of working days between two dates (not including the start date)."""
dt1 = self.__keytransform__(key1)
dt2 = self.__keytransform__(key2)
if dt1 == dt2:
return 0
def get_working_days_count(self, start: DateLike, end: DateLike) -> int:
"""Return the number of working days between two dates.
The date range works in a closed interval fashion [start, end] so both
endpoints are included.
:param start:
The range start date.
:param end:
The range end date.
"""
dt1 = self.__keytransform__(start)
dt2 = self.__keytransform__(end)
if dt1 > dt2:
dt1, dt2 = dt2, dt1
days = (dt2 - dt1).days + 1
return sum(self.is_working_day(_timedelta(dt1, n)) for n in range(days))

return sum(self.is_workday(_timedelta(dt1, n)) for n in range(1, (dt2 - dt1).days + 1))

def is_workday(self, key: DateLike) -> bool:
def is_working_day(self, key: DateLike) -> bool:
"""Return True if date is a working day (not a holiday or a weekend)."""
dt = self.__keytransform__(key)
return dt in self.weekend_workdays if self._is_weekend(dt) else dt not in self
Expand Down
96 changes: 48 additions & 48 deletions tests/test_holiday_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1140,51 +1140,51 @@ class TestWorkdays(unittest.TestCase):
def setUp(self):
self.hb = CountryStub6(years=2024)

def test_is_workday(self):
self.assertTrue(self.hb.is_workday("2024-02-12"))
self.assertFalse(self.hb.is_workday("2024-02-17"))
self.assertFalse(self.hb.is_workday("2024-02-19"))
self.assertTrue(self.hb.is_workday("2024-02-24"))

self.assertTrue(self.hb.is_workday("2024-04-30"))
self.assertFalse(self.hb.is_workday("2024-05-01"))
self.assertFalse(self.hb.is_workday("2024-05-02"))
self.assertTrue(self.hb.is_workday("2024-05-03"))

def test_get_nth_workday(self):
self.assertEqual(self.hb.get_nth_workday("2024-01-04", 0), date(2024, 1, 4))
self.assertEqual(self.hb.get_nth_workday("2024-01-04", +1), date(2024, 1, 5))
self.assertEqual(self.hb.get_nth_workday("2024-01-04", +3), date(2024, 1, 9))
self.assertEqual(self.hb.get_nth_workday("2024-01-06", +1), date(2024, 1, 8))
self.assertEqual(self.hb.get_nth_workday("2024-01-26", -10), date(2024, 1, 12))
self.assertEqual(self.hb.get_nth_workday("2024-01-21", -1), date(2024, 1, 19))

self.assertEqual(self.hb.get_nth_workday("2024-02-15", +4), date(2024, 2, 22))
self.assertEqual(self.hb.get_nth_workday("2024-02-15", +5), date(2024, 2, 23))
self.assertEqual(self.hb.get_nth_workday("2024-02-15", +6), date(2024, 2, 24))
self.assertEqual(self.hb.get_nth_workday("2024-02-15", +7), date(2024, 2, 26))
self.assertEqual(self.hb.get_nth_workday("2024-02-26", -7), date(2024, 2, 15))
self.assertEqual(self.hb.get_nth_workday("2024-02-25", -7), date(2024, 2, 15))

self.assertEqual(self.hb.get_nth_workday("2024-04-29", +1), date(2024, 4, 30))
self.assertEqual(self.hb.get_nth_workday("2024-04-29", +2), date(2024, 5, 3))
self.assertEqual(self.hb.get_nth_workday("2024-04-29", +3), date(2024, 5, 6))
self.assertEqual(self.hb.get_nth_workday("2024-04-29", +4), date(2024, 5, 7))
self.assertEqual(self.hb.get_nth_workday("2024-05-10", -10), date(2024, 4, 24))
self.assertEqual(self.hb.get_nth_workday("2024-05-10", -7), date(2024, 4, 29))
self.assertEqual(self.hb.get_nth_workday("2024-05-10", -5), date(2024, 5, 3))

def test_get_workdays_number(self):
self.assertEqual(self.hb.get_workdays_number("2024-01-03", "2024-01-23"), 14)
self.assertEqual(self.hb.get_workdays_number("2024-01-23", "2024-01-03"), 14)
self.assertEqual(self.hb.get_workdays_number("2024-01-06", "2024-01-07"), 0)
self.assertEqual(self.hb.get_workdays_number("2024-01-16", "2024-01-16"), 0)

self.assertEqual(self.hb.get_workdays_number("2024-02-08", "2024-02-15"), 5)
self.assertEqual(self.hb.get_workdays_number("2024-02-15", "2024-02-22"), 4)
self.assertEqual(self.hb.get_workdays_number("2024-02-22", "2024-02-29"), 6)

self.assertEqual(self.hb.get_workdays_number("2024-04-29", "2024-05-03"), 2)
self.assertEqual(self.hb.get_workdays_number("2024-04-29", "2024-05-04"), 2)
self.assertEqual(self.hb.get_workdays_number("2024-04-29", "2024-05-05"), 2)
self.assertEqual(self.hb.get_workdays_number("2024-04-29", "2024-05-06"), 3)
def test_is_working_day(self):
self.assertTrue(self.hb.is_working_day("2024-02-12"))
self.assertFalse(self.hb.is_working_day("2024-02-17"))
self.assertFalse(self.hb.is_working_day("2024-02-19"))
self.assertTrue(self.hb.is_working_day("2024-02-24"))

self.assertTrue(self.hb.is_working_day("2024-04-30"))
self.assertFalse(self.hb.is_working_day("2024-05-01"))
self.assertFalse(self.hb.is_working_day("2024-05-02"))
self.assertTrue(self.hb.is_working_day("2024-05-03"))

def test_get_nth_working_day(self):
self.assertEqual(self.hb.get_nth_working_day("2024-01-04", 0), date(2024, 1, 4))
self.assertEqual(self.hb.get_nth_working_day("2024-01-04", +1), date(2024, 1, 5))
self.assertEqual(self.hb.get_nth_working_day("2024-01-04", +3), date(2024, 1, 9))
self.assertEqual(self.hb.get_nth_working_day("2024-01-06", +1), date(2024, 1, 8))
self.assertEqual(self.hb.get_nth_working_day("2024-01-26", -10), date(2024, 1, 12))
self.assertEqual(self.hb.get_nth_working_day("2024-01-21", -1), date(2024, 1, 19))

self.assertEqual(self.hb.get_nth_working_day("2024-02-15", +4), date(2024, 2, 22))
self.assertEqual(self.hb.get_nth_working_day("2024-02-15", +5), date(2024, 2, 23))
self.assertEqual(self.hb.get_nth_working_day("2024-02-15", +6), date(2024, 2, 24))
self.assertEqual(self.hb.get_nth_working_day("2024-02-15", +7), date(2024, 2, 26))
self.assertEqual(self.hb.get_nth_working_day("2024-02-26", -7), date(2024, 2, 15))
self.assertEqual(self.hb.get_nth_working_day("2024-02-25", -7), date(2024, 2, 15))

self.assertEqual(self.hb.get_nth_working_day("2024-04-29", +1), date(2024, 4, 30))
self.assertEqual(self.hb.get_nth_working_day("2024-04-29", +2), date(2024, 5, 3))
self.assertEqual(self.hb.get_nth_working_day("2024-04-29", +3), date(2024, 5, 6))
self.assertEqual(self.hb.get_nth_working_day("2024-04-29", +4), date(2024, 5, 7))
self.assertEqual(self.hb.get_nth_working_day("2024-05-10", -10), date(2024, 4, 24))
self.assertEqual(self.hb.get_nth_working_day("2024-05-10", -7), date(2024, 4, 29))
self.assertEqual(self.hb.get_nth_working_day("2024-05-10", -5), date(2024, 5, 3))

def test_get_working_days_count(self):
self.assertEqual(self.hb.get_working_days_count("2024-01-03", "2024-01-23"), 15)
self.assertEqual(self.hb.get_working_days_count("2024-01-23", "2024-01-03"), 15)
self.assertEqual(self.hb.get_working_days_count("2024-01-06", "2024-01-07"), 0)
self.assertEqual(self.hb.get_working_days_count("2024-01-16", "2024-01-16"), 1)

self.assertEqual(self.hb.get_working_days_count("2024-02-08", "2024-02-15"), 6)
self.assertEqual(self.hb.get_working_days_count("2024-02-15", "2024-02-22"), 5)
self.assertEqual(self.hb.get_working_days_count("2024-02-22", "2024-02-29"), 7)

self.assertEqual(self.hb.get_working_days_count("2024-04-29", "2024-05-03"), 3)
self.assertEqual(self.hb.get_working_days_count("2024-04-29", "2024-05-04"), 3)
self.assertEqual(self.hb.get_working_days_count("2024-04-29", "2024-05-05"), 3)
self.assertEqual(self.hb.get_working_days_count("2024-04-29", "2024-05-06"), 4)

0 comments on commit 041681a

Please sign in to comment.