File: holiday.py

package info (click to toggle)
python-calendra 7.11.1-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 1,600 kB
  • sloc: python: 16,840; makefile: 6
file content (150 lines) | stat: -rw-r--r-- 4,862 bytes parent folder | download
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