Skip to content

Commit

Permalink
Merge pull request #235 from nedlowe/hong-kong
Browse files Browse the repository at this point in the history
Add Hong Kong calendar.

Added a refactor for Christmas and Boxing day shift in UK and other countries.
  • Loading branch information
brunobord committed Jul 3, 2017
2 parents 8073608 + 38896d9 commit 3bb079a
Show file tree
Hide file tree
Showing 8 changed files with 268 additions and 40 deletions.
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).
- Added Alabama Counties - Baldwin County, Mobile County, Perry County. Refactored UnitedStates classes to have a parameter to include the "Mardi Gras" day (#214).

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):
""" 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

0 comments on commit 3bb079a

Please sign in to comment.