1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
|
import itertools
import functools
from datetime import date, timedelta
from more_itertools import recipes
from dateutil import relativedelta as rd
class Holiday(date):
"""
A named holiday with an indicated date, name, and additional keyword
attributes.
>>> nyd = Holiday(date(2014, 1, 1), "New year")
But if New Year's Eve is also a holiday, and it too falls on a weekend,
many calendars will have that holiday fall back to the previous friday:
>>> from dateutil import relativedelta as rd
>>> nye = Holiday(date(2014, 12, 31), "New year's eve",
... observance_shift=dict(weekday=rd.FR(-1)))
For compatibility, a Holiday may be treated like a tuple of (date, label)
>>> nyd[0] == date(2014, 1, 1)
True
>>> nyd[1]
'New year'
>>> d, label = nyd
"""
def __new__(cls, date, *args, **kwargs):
return super().__new__(
cls, date.year, date.month, date.day)
def __init__(self, date, name='Holiday', **kwargs):
self.name = name
vars(self).update(kwargs)
def __getitem__(self, n):
"""
for compatibility as a two-tuple
"""
tp = self, self.name
return tp[n]
def __iter__(self):
"""
for compatibility as a two-tuple
"""
tp = self, self.name
return iter(tp)
@property
def _orig(self):
return date(self.year, self.month, self.day)
def replace(self, **kwargs):
return Holiday(self._orig.replace(**kwargs), **vars(self))
def __add__(self, other):
return Holiday(self._orig + other, **vars(self))
def __sub__(self, other):
return Holiday(self._orig - other, **vars(self))
def nearest_weekday(self, calendar):
"""
Return the nearest weekday to self.
"""
weekend_days = calendar.get_weekend_days()
deltas = (timedelta(n) for n in itertools.count())
candidates = recipes.flatten(
(self - delta, self + delta)
for delta in deltas
)
matches = (
day for day in candidates
if day.weekday() not in weekend_days
)
return next(matches)
@classmethod
def _from_fixed_definition(cls, item):
"""For backward compatibility, load Holiday object from an item of
FIXED_HOLIDAYS class property, which might be just a tuple of
month, day, label.
"""
if isinstance(item, tuple):
month, day, label = item
any_year = 2000
item = Holiday(date(any_year, month, day), label)
return item
@classmethod
def _from_resolved_definition(cls, item, **kwargs):
"""For backward compatibility, load Holiday object from a two-tuple
or existing Holiday instance.
"""
if isinstance(item, tuple):
item = Holiday(*item, **kwargs)
return item
@functools.lru_cache()
def get_observed_date(self, calendar):
"""
The date the holiday is observed for the calendar. If the holiday
occurs on a weekend, it may be observed on another day as indicated by
the observance_shift.
The holiday may also specify an 'observe_after' such that it is always
shifted after a preceding holiday. For example, Boxing day is always
observed after Christmas Day is observed.
"""
# observance_shift may be overridden in the holiday itself
shift = getattr(self, 'observance_shift', calendar.observance_shift)
if callable(shift):
shifted = shift(self, calendar)
else:
shift = shift or {}
delta = rd.relativedelta(**shift)
try:
weekend_days = calendar.get_weekend_days()
except NotImplementedError:
weekend_days = ()
should_shift = self.weekday() in weekend_days
shifted = self + delta if should_shift else self
precedent = getattr(self, 'observe_after', None)
while precedent and shifted <= precedent.get_observed_date(calendar):
shifted += timedelta(days=1)
return shifted
class SeriesShiftMixin:
"""
"Series" holidays like the two Islamic Eid's or Chinese Spring Festival span
multiple days. If one of these days encounters a non-zero observance_shift,
apply that shift to all subsequent members of the series.
"""
def get_calendar_holidays(self, year):
"""
Ensure that all events are observed in the order indicated.
"""
days = super().get_calendar_holidays(year)
holidays = sorted(map(Holiday._from_resolved_definition, days))
from more_itertools import pairwise
for a, b in pairwise(holidays):
b.observe_after = a
return holidays
|