Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Hong Kong calendar. Factor out the Christmas/Boxing Day Sunday s… #235

Merged
merged 5 commits into from Jul 3, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG
Expand Up @@ -4,6 +4,7 @@ CHANGELOG
master (unreleased)
-------------------

- Added Hong Kong, by @nedlowe (#235).
- Splitted `africa.py` file into an `africa/` module (#236).

2.0.0 (2017-06-23)
Expand Down
1 change: 1 addition & 0 deletions README.rst
Expand Up @@ -88,6 +88,7 @@ Europe
* France (Alsace / Moselle)
* Germany
* Greece
* Hong Kong
* Hungary
* Iceland
* Ireland
Expand Down
14 changes: 8 additions & 6 deletions workalendar/asia/__init__.py
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
from .hong_kong import HongKong
from .japan import Japan
from .malaysia import Malaysia
from .qatar import Qatar
Expand All @@ -8,10 +9,11 @@


__all__ = (
Japan,
Malaysia,
Qatar,
Singapore,
SouthKorea,
Taiwan,
'HongKong',
'Japan',
'Malaysia',
'Qatar',
'Singapore',
'SouthKorea',
'Taiwan',
)
62 changes: 62 additions & 0 deletions workalendar/asia/hong_kong.py
@@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-

from datetime import timedelta

from workalendar.core import ChineseNewYearCalendar, WesternCalendar
from workalendar.core import ChristianMixin, EphemMixin


class HongKong(EphemMixin, WesternCalendar,
ChineseNewYearCalendar, ChristianMixin):
"Hong Kong"
include_good_friday = True
include_easter_saturday = True
include_easter_monday = True
include_boxing_day = True

FIXED_HOLIDAYS = WesternCalendar.FIXED_HOLIDAYS + (
(5, 1, "Labour Day"),
(7, 1, "SAR Establishment Day"),
(10, 1, "National Day"),
)

chinese_new_year_label = "Chinese Lunar New Year's Day"
include_chinese_second_day = True
chinese_second_day_label = "Second day of Chinese Lunar New Year"
include_chinese_third_day = True
chinese_third_day_label = "Third day of Chinese Lunar New Year"
shift_sunday_holidays = True # Except CNY which rolls to Saturday
shift_start_cny_sunday = False # Prior to 2011 this was True

def get_variable_days(self, year):
"""
Hong Kong variable days
"""

# Prior to the "General Holidays and Employment Legislation
# (Substitution of Holidays)(Amendment) Ordinance 2011", the first day
# of CNY rolled to a Sat if it was on a Sun. After the Amendment, it
# rolls to the following Wed
if year < 2011:
self.shift_start_cny_sunday = True

days = super(HongKong, self).get_variable_days(year)
chingming = EphemMixin.solar_term(self, year, 15, 'Asia/Hong_Kong')
dupe_holiday = [chingming for day in days if chingming == day[0]]
if dupe_holiday:
# Roll Chingming forward a day as it clashes with another holiday
chingming = chingming + timedelta(days=1)
mid_autumn_label = "Day After Mid-Autumn Festival"
days.extend([
(ChineseNewYearCalendar.lunar(year, 4, 8), "Buddha's Birthday"),
(chingming, "Ching Ming Festival"),
(ChineseNewYearCalendar.lunar(year, 5, 5), "Tuen Ng Festival"),
(ChineseNewYearCalendar.lunar(year, 8, 16), mid_autumn_label),
(ChineseNewYearCalendar.lunar(year, 9, 9), "Chung Yeung Festival"),
])

# Boxing day & XMas shift
shifts = self.shift_christmas_boxing_days(year=year)
days.extend(shifts)

return days
52 changes: 48 additions & 4 deletions workalendar/core.py
Expand Up @@ -327,6 +327,23 @@ def get_whit_sunday(self, year):
def get_corpus_christi(self, year):
return self.get_easter_sunday(year) + timedelta(days=60)

def shift_christmas_boxing_days(self, year):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can add a docstring here, for more clarity. it's not a blocking point, though

""" When Christmas and/or Boxing Day falls on a weekend, it is rolled
forward to the next weekday.
"""
christmas = date(year, 12, 25)
boxing_day = date(year, 12, 26)
boxing_day_label = "{} Shift".format(self.boxing_day_label)
results = []
if christmas.weekday() in self.get_weekend_days():
shift = self.find_following_working_day(christmas)
results.append((shift, "Christmas Shift"))
results.append((shift + timedelta(days=1), boxing_day_label))
elif boxing_day.weekday() in self.get_weekend_days():
shift = self.find_following_working_day(boxing_day)
results.append((shift, boxing_day_label))
return results

def get_variable_days(self, year): # noqa
"Return the christian holidays list according to the mixin"
days = super(ChristianMixin, self).get_variable_days(year)
Expand Down Expand Up @@ -427,7 +444,11 @@ class ChineseNewYearCalendar(LunarCalendar):
# Some countries include the 2nd lunar day as a holiday
include_chinese_second_day = False
chinese_second_day_label = "Chinese New Year (2nd day)"
include_chinese_third_day = False
chinese_third_day_label = "Chinese New Year (3rd day)"
shift_sunday_holidays = False
# Some calendars roll a starting Sunday CNY to Sat
shift_start_cny_sunday = False

def get_chinese_new_year(self, year):
"""
Expand Down Expand Up @@ -467,12 +488,35 @@ def get_chinese_new_year(self, year):
lunar_second_day,
self.chinese_second_day_label
))
if self.shift_sunday_holidays:
if lunar_first_day.weekday() == SUN:
if self.include_chinese_third_day:
lunar_third_day = lunar_first_day + timedelta(days=2)
days.append((
lunar_third_day,
self.chinese_third_day_label
))

if self.shift_sunday_holidays:
if lunar_first_day.weekday() == SUN:
if self.shift_start_cny_sunday:
days.append(
(lunar_first_day - timedelta(days=1),
"Chinese Lunar New Year shift"),
)
else:
if self.include_chinese_third_day:
shift_day = lunar_third_day
else:
shift_day = lunar_second_day
days.append(
(lunar_second_day + timedelta(days=1),
"Second day of Chinese Lunar New Year shift"),
(shift_day + timedelta(days=1),
"Chinese Lunar New Year shift"),
)
if (lunar_second_day.weekday() == SUN
and self.include_chinese_third_day):
days.append(
(lunar_third_day + timedelta(days=1),
"Chinese Lunar New Year shift"),
)
return days

def get_variable_days(self, year):
Expand Down
13 changes: 3 additions & 10 deletions workalendar/europe/united_kingdom.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from datetime import date, timedelta
from datetime import date
from workalendar.core import WesternCalendar, ChristianMixin
from workalendar.core import MON

Expand Down Expand Up @@ -36,15 +36,8 @@ def get_variable_days(self, year):
days.append(self.get_spring_bank_holiday(year))
days.append(self.get_late_summer_bank_holiday(year))
# Boxing day & XMas shift
christmas = date(year, 12, 25)
boxing_day = date(year, 12, 26)
if christmas.weekday() in self.get_weekend_days():
shift = self.find_following_working_day(christmas)
days.append((shift, "Christmas Shift"))
days.append((shift + timedelta(days=1), "Boxing Day Shift"))
elif boxing_day.weekday() in self.get_weekend_days():
shift = self.find_following_working_day(boxing_day)
days.append((shift, "Boxing Day Shift"))
shifts = self.shift_christmas_boxing_days(year=year)
days.extend(shifts)
return days


Expand Down
152 changes: 132 additions & 20 deletions workalendar/tests/test_asia.py
@@ -1,33 +1,114 @@
from datetime import date
from workalendar.tests import GenericCalendarTest

from workalendar.asia import Japan, Qatar, Singapore
from workalendar.asia import HongKong, Japan, Qatar, Singapore
from workalendar.asia import SouthKorea, Taiwan, Malaysia


class SouthKoreaTest(GenericCalendarTest):

cal_class = SouthKorea
class HongKongTest(GenericCalendarTest):

cal_class = HongKong

def test_year_2010(self):
""" Interesting because Christmas fell on a Saturday and CNY fell
on a Sunday, so didn't roll, and Ching Ming was on the same day
as Easter Monday """
holidays = self.cal.holidays_set(2010)
self.assertIn(date(2010, 1, 1), holidays) # New Year
self.assertIn(date(2010, 2, 13), holidays) # Chinese new year (shift)
self.assertIn(date(2010, 2, 15), holidays) # Chinese new year
self.assertIn(date(2010, 2, 16), holidays) # Chinese new year
self.assertNotIn(date(2010, 2, 17), holidays) # Not Chinese new year
self.assertIn(date(2010, 4, 2), holidays) # Good Friday
self.assertIn(date(2010, 4, 3), holidays) # Day after Good Friday
self.assertIn(date(2010, 4, 5), holidays) # Easter Monday
self.assertIn(date(2010, 4, 6), holidays) # Ching Ming (shifted)
self.assertIn(date(2010, 5, 1), holidays) # Labour Day
self.assertIn(date(2010, 5, 21), holidays) # Buddha's Birthday
self.assertIn(date(2010, 6, 16), holidays) # Tuen Ng Festival
self.assertIn(date(2010, 7, 1), holidays) # HK SAR Establishment Day
self.assertIn(date(2010, 9, 23), holidays) # Day after Mid-Autumn
self.assertIn(date(2010, 10, 1), holidays) # National Day
self.assertIn(date(2010, 10, 16), holidays) # Chung Yeung Festival
self.assertIn(date(2010, 12, 25), holidays) # Christmas Day
self.assertIn(date(2010, 12, 27), holidays) # Boxing Day (shifted)

def test_year_2013(self):
holidays = self.cal.holidays_set(2013)
self.assertIn(date(2013, 1, 1), holidays) # new year
self.assertIn(date(2013, 3, 1), holidays) # Independence day
self.assertIn(date(2013, 5, 5), holidays) # children's day
self.assertIn(date(2013, 6, 6), holidays) # Memorial day
self.assertIn(date(2013, 8, 15), holidays) # Liberation day
self.assertIn(date(2013, 10, 3), holidays) # National Foundation Day
self.assertIn(date(2013, 10, 9), holidays) # Hangul Day
self.assertIn(date(2013, 12, 25), holidays) # Christmas
self.assertIn(date(2013, 1, 1), holidays) # New Year
self.assertIn(date(2013, 2, 11), holidays) # Chinese new year
self.assertIn(date(2013, 2, 12), holidays) # Chinese new year
self.assertIn(date(2013, 2, 13), holidays) # Chinese new year
self.assertIn(date(2013, 3, 29), holidays) # Good Friday
self.assertIn(date(2013, 3, 30), holidays) # Day after Good Friday
self.assertIn(date(2013, 4, 1), holidays) # Easter Monday
self.assertIn(date(2013, 4, 4), holidays) # Ching Ming
self.assertIn(date(2013, 5, 1), holidays) # Labour Day
self.assertIn(date(2013, 5, 17), holidays) # Buddha's Birthday
self.assertIn(date(2013, 6, 12), holidays) # Tuen Ng Festival
self.assertIn(date(2013, 7, 1), holidays) # HK SAR Establishment Day
self.assertIn(date(2013, 9, 20), holidays) # Day after Mid-Autumn
self.assertIn(date(2013, 10, 1), holidays) # National Day
self.assertIn(date(2013, 10, 14), holidays) # Chung Yeung Festival
self.assertIn(date(2013, 12, 25), holidays) # Christmas Day
self.assertIn(date(2013, 12, 26), holidays) # Boxing Day

# Variable days
self.assertIn(date(2013, 2, 9), holidays)
self.assertIn(date(2013, 2, 10), holidays)
self.assertIn(date(2013, 2, 11), holidays)
self.assertIn(date(2013, 5, 17), holidays)
self.assertIn(date(2013, 9, 18), holidays)
self.assertIn(date(2013, 9, 19), holidays)
self.assertIn(date(2013, 9, 20), holidays)
def test_year_2016(self):
holidays = self.cal.holidays_set(2016)
self.assertIn(date(2016, 1, 1), holidays) # New Year
self.assertIn(date(2016, 2, 8), holidays) # Chinese new year
self.assertIn(date(2016, 2, 9), holidays) # Chinese new year
self.assertIn(date(2016, 2, 10), holidays) # Chinese new year
self.assertIn(date(2016, 3, 25), holidays) # Good Friday
self.assertIn(date(2016, 3, 26), holidays) # Day after Good Friday
self.assertIn(date(2016, 3, 28), holidays) # Easter Monday
self.assertIn(date(2016, 4, 4), holidays) # Ching Ming
self.assertIn(date(2016, 5, 2), holidays) # Labour Day (shifted)
self.assertIn(date(2016, 5, 14), holidays) # Buddha's Birthday
self.assertIn(date(2016, 6, 9), holidays) # Tuen Ng Festival
self.assertIn(date(2016, 7, 1), holidays) # HK SAR Establishment Day
self.assertIn(date(2016, 9, 16), holidays) # Day after Mid-Autumn
self.assertIn(date(2016, 10, 1), holidays) # National Day
self.assertIn(date(2016, 10, 10), holidays) # Chung Yeung Festival
self.assertIn(date(2016, 12, 26), holidays) # Christmas Day (shifted)
self.assertIn(date(2016, 12, 27), holidays) # Boxing Day (shifted)

def test_year_2017(self):
holidays = self.cal.holidays_set(2017)
self.assertIn(date(2017, 1, 2), holidays) # New Year (shifted)
self.assertIn(date(2017, 1, 28), holidays) # Chinese new year
self.assertIn(date(2017, 1, 30), holidays) # Chinese new year
self.assertIn(date(2017, 1, 31), holidays) # Chinese new year
self.assertIn(date(2017, 4, 4), holidays) # Ching Ming
self.assertIn(date(2017, 4, 14), holidays) # Good Friday
self.assertIn(date(2017, 4, 15), holidays) # Day after Good Friday
self.assertIn(date(2017, 4, 17), holidays) # Easter Monday
self.assertIn(date(2017, 5, 1), holidays) # Labour Day
self.assertIn(date(2017, 5, 3), holidays) # Buddha's Birthday
self.assertIn(date(2017, 5, 30), holidays) # Tuen Ng Festival
self.assertIn(date(2017, 7, 1), holidays) # HK SAR Establishment Day
self.assertIn(date(2017, 10, 2), holidays) # National Day (shifted)
self.assertIn(date(2017, 10, 5), holidays) # Day after Mid-Autumn
self.assertIn(date(2017, 10, 28), holidays) # Chung Yeung Festival
self.assertIn(date(2017, 12, 25), holidays) # Christmas Day
self.assertIn(date(2017, 12, 26), holidays) # Boxing Day

def test_chingming_festival(self):
# This is the same as the Taiwan test, just different spelling
# Could move this into a Core test
self.assertIn(date(2005, 4, 5), self.cal.holidays_set(2005))
self.assertIn(date(2006, 4, 5), self.cal.holidays_set(2006))
self.assertIn(date(2007, 4, 5), self.cal.holidays_set(2007))
self.assertIn(date(2008, 4, 4), self.cal.holidays_set(2008))
self.assertIn(date(2010, 4, 5), self.cal.holidays_set(2010))
self.assertIn(date(2011, 4, 5), self.cal.holidays_set(2011))
self.assertIn(date(2012, 4, 4), self.cal.holidays_set(2012))
self.assertIn(date(2013, 4, 4), self.cal.holidays_set(2013))
self.assertIn(date(2014, 4, 5), self.cal.holidays_set(2014))
self.assertIn(date(2015, 4, 4), self.cal.holidays_set(2015))
self.assertIn(date(2016, 4, 4), self.cal.holidays_set(2016))
self.assertIn(date(2017, 4, 4), self.cal.holidays_set(2017))
self.assertIn(date(2018, 4, 5), self.cal.holidays_set(2018))


class JapanTest(GenericCalendarTest):
Expand Down Expand Up @@ -138,6 +219,12 @@ class SingaporeTest(GenericCalendarTest):

cal_class = Singapore

def test_CNY_2010(self):
holidays = self.cal.holidays_set(2010)
self.assertIn(date(2010, 2, 14), holidays) # CNY1
self.assertIn(date(2010, 2, 15), holidays) # CNY2
self.assertIn(date(2010, 2, 16), holidays) # Rolled day for CNY

def test_year_2013(self):
holidays = self.cal.holidays_set(2013)
self.assertIn(date(2013, 1, 1), holidays) # New Year
Expand Down Expand Up @@ -177,6 +264,31 @@ def test_fixed_holiday_shift(self):
self.assertIn(date(2016, 5, 2), holidays)


class SouthKoreaTest(GenericCalendarTest):

cal_class = SouthKorea

def test_year_2013(self):
holidays = self.cal.holidays_set(2013)
self.assertIn(date(2013, 1, 1), holidays) # new year
self.assertIn(date(2013, 3, 1), holidays) # Independence day
self.assertIn(date(2013, 5, 5), holidays) # children's day
self.assertIn(date(2013, 6, 6), holidays) # Memorial day
self.assertIn(date(2013, 8, 15), holidays) # Liberation day
self.assertIn(date(2013, 10, 3), holidays) # National Foundation Day
self.assertIn(date(2013, 10, 9), holidays) # Hangul Day
self.assertIn(date(2013, 12, 25), holidays) # Christmas

# Variable days
self.assertIn(date(2013, 2, 9), holidays)
self.assertIn(date(2013, 2, 10), holidays)
self.assertIn(date(2013, 2, 11), holidays)
self.assertIn(date(2013, 5, 17), holidays)
self.assertIn(date(2013, 9, 18), holidays)
self.assertIn(date(2013, 9, 19), holidays)
self.assertIn(date(2013, 9, 20), holidays)


class TaiwanTest(GenericCalendarTest):

cal_class = Taiwan
Expand Down