Skip to content

Commit

Permalink
Merge pull request #24 from jaraco/feature/simple-series
Browse files Browse the repository at this point in the history
All holidays observed after earlier holidays.
  • Loading branch information
jaraco authored Sep 3, 2022
2 parents f52ca5e + fb16c49 commit 25399ce
Show file tree
Hide file tree
Showing 6 changed files with 20 additions and 67 deletions.
4 changes: 0 additions & 4 deletions calendra/asia/china.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,6 @@ class China(SeriesShiftMixin, ChineseNewYearCalendar):
FIXED_HOLIDAYS = tuple(national_days)

include_chinese_new_year_eve = True
series_requiring_shifts = ['Spring Festival', 'Ching Ming Festival',
'Labour Day Holiday', 'Dragon Boat Festival',
'Mid-Autumn Festival', 'New year',
'National Day']

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand Down
1 change: 0 additions & 1 deletion calendra/asia/south_korea.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ class SouthKorea(SeriesShiftMixin, ChineseNewYearCalendar):
chinese_new_year_eve_label = "Korean New Year's Day"
include_chinese_second_day = True
chinese_second_day_label = "Korean New Year's Day"
series_requiring_shifts = ["Korean New Year's Day", "Midautumn Festival"]

def get_variable_days(self, year):
days = super().get_variable_days(year)
Expand Down
9 changes: 3 additions & 6 deletions calendra/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from datetime import date, timedelta, datetime
from pathlib import Path
import sys
import functools

import convertdate
from dateutil import easter
Expand Down Expand Up @@ -232,7 +233,7 @@ def get_variable_days(self, year): # noqa
date(year, 12, 26),
self.boxing_day_label,
indication="Day after Christmas",
observe_after=christmas
observe_after=christmas,
)
days.append(boxing_day)
if self.include_ascension:
Expand Down Expand Up @@ -527,11 +528,6 @@ def get_islamic_holidays(self): # noqa: C901
" to compute it. You'll have to add it manually.")
return tuple(days)

def get_calendar_holidays(self, year):
self.series_requiring_shifts = [self.eid_al_fitr_label,
self.day_of_sacrifice_label]
return super().get_calendar_holidays(year)


class CoreCalendar:

Expand Down Expand Up @@ -597,6 +593,7 @@ def get_holiday_label(self, day):
day = cleaned_date(day)
return {day: label for day, label in self.holidays(day.year)}.get(day)

@functools.lru_cache()
def get_observed_date(self, holiday):
"""
The date the holiday is observed for this calendar. If the holiday
Expand Down
17 changes: 9 additions & 8 deletions calendra/europe/netherlands.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from ..core import WesternCalendar, SUN, ISO_SAT
from ..core import SeriesShiftMixin
from ..registry_tools import iso_register
from ..core import Holiday


@iso_register('NL')
Expand All @@ -15,7 +16,6 @@ class Netherlands(SeriesShiftMixin, WesternCalendar):
include_whit_sunday = True
include_whit_monday = True
include_boxing_day = True
series_requiring_shifts = ["Carnival"]

FIXED_HOLIDAYS = WesternCalendar.FIXED_HOLIDAYS + (
(5, 5, "Liberation Day"),
Expand Down Expand Up @@ -120,11 +120,6 @@ class NetherlandsWithSchoolHolidays(Netherlands):
Data source and regulating body:
https://www.rijksoverheid.nl/onderwerpen/schoolvakanties/overzicht-schoolvakanties-per-schooljaar
"""
series_requiring_shifts = [
"Carnival", "Carnival holiday",
"Fall holiday", "Christmas holiday", "Spring holiday", "May holiday",
"Summer holiday"
]

def __init__(self, region, carnival_instead_of_spring=False, **kwargs):
""" Set up a calendar incl. school holidays for a specific region
Expand Down Expand Up @@ -166,7 +161,13 @@ def get_fall_holidays(self, year):
(start + timedelta(days=i), "Fall holiday") for i in range(n_days)
]

def get_christmas_holidays(self, year, in_december=True, in_january=True):
def get_christmas_holidays(self, year):
return [
Holiday._from_resolved_definition(defn, observance_shift=None)
for defn in self._get_christmas_holidays(year)
]

def _get_christmas_holidays(self, year, in_december=True, in_january=True):
"""
Return Christmas holidays
Expand All @@ -188,7 +189,7 @@ def get_christmas_holidays(self, year, in_december=True, in_january=True):

if in_january:
dates.extend(
self.get_christmas_holidays(year, in_december=False)
self._get_christmas_holidays(year, in_december=False)
)
return dates

Expand Down
1 change: 0 additions & 1 deletion calendra/europe/serbia.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,3 @@ class Serbia(SeriesShiftMixin, OrthodoxCalendar):
include_easter_sunday = True
include_easter_monday = True
include_christmas = False
series_requiring_shifts = ["Statehood Day"]
55 changes: 8 additions & 47 deletions calendra/holiday.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import itertools
from datetime import date, timedelta
from typing import Optional, List

from more_itertools import recipes

Expand Down Expand Up @@ -92,67 +91,29 @@ def _from_fixed_definition(cls, item):
return item

@classmethod
def _from_resolved_definition(cls, item):
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)
item = Holiday(*item, **kwargs)
return item


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,
we need to apply that shift to all subsequent members of the series.
Packagin as a standalone Mixin ensure that the logic can be applied as
needed *after* any default shift is applied.
"""
series_requiring_shifts: Optional[List[str]] = None
"""
A list of all holiday labels that require series shifting to be applied.
apply that shift to all subsequent members of the series.
"""

def get_calendar_holidays(self, year):
"""
The point at which any shift occurs is year-specific.
Ensure that all events are observed in the order indicated.
"""
days = super().get_calendar_holidays(year)
series_shift = {series: None for series in self.series_requiring_shifts}
holidays = []
for holiday, label in days:
#
# Make a year-specific copy in case we have to attach a shift.
#
holiday = Holiday(holiday, label)
#
# For either Eid series, apply the shift to all days in the
# series after the first shift.
#
if label in series_shift:
shifted = self.get_observed_date(holiday)
if series_shift[holiday.name] is None and shifted.day != holiday.day:

def observance_shift_for_series(holiday, calendar):
"""
Taking an existing holiday, return a 'shifted' day based
on delta in the current year's closure.
"""
return holiday + delta

delta = date(shifted.year, shifted.month, shifted.day) - \
date(holiday.year, holiday.month, holiday.day)
#
# Learn the observance_shift for all subsequent days in the
# series.
#
series_shift[holiday.name] = observance_shift_for_series
elif series_shift[holiday.name] is not None:
#
# Apply the learned observance_shift.
#
holiday.observance_shift = series_shift[holiday.name]
holidays.append(holiday)
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

0 comments on commit 25399ce

Please sign in to comment.