Repository: peopledoc/workalendar
Pull request #313 (2019-01-24 12:53:32)

Add new calendar: China
closes #304 

* replacing #304 to have a cleaner history,
* rearranged the docstring to fit the "label" value,
* added a test in the ISO registry tests

----

- [x] Tests with a significant number of years to be tested for your calendar.
- [x] Docstrings for the Calendar class and specific methods.
- [x] Calendar country / label added to the README.rst file,
- [x] Changelog amended with a mention like: "Added ``<country>`` by ``@pseudo`` (#)"



Patch:
diff --git a/Changelog.md b/Changelog.md
index 867ed7e..a973acd 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -2,12 +2,13 @@
 
 ## master (unreleased)
 
-* Solved the incompatibility between `pandas` latest version and Python 3.4. Upgraded travis distro to Xenial/16.04 LTS (#307).
+- Solved the incompatibility between `pandas` latest version and Python 3.4. Upgraded travis distro to Xenial/16.04 LTS (#307).
 
 ### New Calendars
 
-- Added New Zealand, by @johnguant (#306)
-* Added Paraguay calendar, following the work of @reichert (#268).
+- Added New Zealand, by @johnguant (#306).
+- Added Paraguay calendar, following the work of @reichert (#268).
+- Added China calendar, by @iamsk (#304).
 
 ## v3.2.1 (2018-12-06)
 
@@ -29,6 +30,12 @@
 
 - Fixed ISO 3166-1 code for the `Slovenia` calendar (#291, thx @john-sandall).
 
+## v3.2.0 (2018-12-25)
+
+### New Calendars
+
+- Added China, by @iamsk.
+
 ## v3.1.0 (2018-10-25)
 
 ### Major changes
diff --git a/README.rst b/README.rst
index 794bfb9..5139403 100644
--- a/README.rst
+++ b/README.rst
@@ -128,6 +128,7 @@ America
 Asia
 ----
 
+* China
 * Hong Kong
 * Japan
 * Malaysia
diff --git a/workalendar/asia/__init__.py b/workalendar/asia/__init__.py
index ed51afd..4856a1f 100644
--- a/workalendar/asia/__init__.py
+++ b/workalendar/asia/__init__.py
@@ -1,4 +1,5 @@
 # -*- coding: utf-8 -*-
+from .china import China
 from .hong_kong import HongKong
 from .japan import Japan
 from .malaysia import Malaysia
@@ -9,6 +10,7 @@ from .taiwan import Taiwan
 
 
 __all__ = (
+    'China',
     'HongKong',
     'Japan',
     'Malaysia',
diff --git a/workalendar/asia/china.py b/workalendar/asia/china.py
new file mode 100644
index 0000000..1c1bb30
--- /dev/null
+++ b/workalendar/asia/china.py
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+from __future__ import (absolute_import, division, print_function,
+                        unicode_literals)
+from datetime import date
+import warnings
+
+from ..core import ChineseNewYearCalendar, WesternCalendar
+from ..registry import iso_register
+from ..exceptions import CalendarError
+
+holidays = {
+    2018:
+        {
+            'Ching Ming Festival': [(4, 5), (4, 6), (4, 7)],
+            'Labour Day Holiday': [(4, 29), (4, 30), (5, 1)],
+            'Dragon Boat Festival': [(6, 18)],
+            'Mid-Autumn Festival': [(9, 24)],
+            'New year': [(12, 30), (12, 31)]
+        },
+    2019:
+        {
+            'Ching Ming Festival': [(4, 5)],
+            'Labour Day Holiday': [(5, 1)],
+            'Dragon Boat Festival': [(6, 7)],
+            'Mid-Autumn Festival': [(9, 13)]
+        },
+}
+
+workdays = {
+    2018:
+        {
+            'Spring Festival Shift': [(2, 11), (2, 24)],
+            'Ching Ming Festival Shift': [(4, 8)],
+            'Labour Day Holiday Shift': [(4, 28)],
+            'National Day Shift': [(9, 29), (9, 30)],
+            'New year Shift': [(12, 29)]
+        },
+    2019:
+        {
+            'Spring Festival Shift': [(2, 2), (2, 3)],
+            'National Day Shift': [(9, 29), (10, 12)],
+        },
+}
+
+
+@iso_register('CN')
+class China(ChineseNewYearCalendar, WesternCalendar):
+    "China"
+    # WARNING: Support 2018, 2019 currently, need update every year.
+    # National Days, 10.1 - 10.7
+    national_days = [(10, i, "National Day") for i in range(1, 8)]
+    FIXED_HOLIDAYS = WesternCalendar.FIXED_HOLIDAYS + tuple(national_days)
+
+    include_chinese_new_year_eve = True
+
+    extra_working_days = []
+    for year, data in workdays.items():
+        for holiday_name, day_list in data.items():
+            for v in day_list:
+                extra_working_days.append(date(year, v[0], v[1]))
+
+    def get_calendar_holidays(self, year):
+        warnings.warn("Support 2018, 2019 currently, need update every year.")
+        if year not in holidays.keys():
+            msg = "Need configure {} for China.".format(year)
+            raise CalendarError(msg)
+        return super(China, self).get_calendar_holidays(year)
+
+    def get_variable_days(self, year):
+        days = super(China, self).get_variable_days(year)
+        # Spring Festival, eve, 1.1, and 1.2 - 1.6 in lunar day
+        for i in range(2, 7):
+            days.append((ChineseNewYearCalendar.lunar(year, 1, i),
+                         "Spring Festival"))
+        # other holidays
+        for holiday_name, day_list in holidays[year].items():
+            for v in day_list:
+                days.append((date(year, v[0], v[1]), holiday_name))
+        return days
+
+    def is_working_day(self, day,
+                       extra_working_days=None, extra_holidays=None):
+        extra_working_days = extra_working_days or self.extra_working_days
+        return super(China, self).is_working_day(day, extra_working_days,
+                                                 extra_holidays)
+
+    def add_working_days(self, day, delta,
+                         extra_working_days=None, extra_holidays=None,
+                         keep_datetime=False):
+        extra_working_days = extra_working_days or self.extra_working_days
+        return super(China, self).add_working_days(day, delta,
+                                                   extra_working_days,
+                                                   extra_holidays,
+                                                   keep_datetime)
+
+    def sub_working_days(self, day, delta,
+                         extra_working_days=None, extra_holidays=None,
+                         keep_datetime=False):
+        extra_working_days = extra_working_days or self.extra_working_days
+        return super(China, self).sub_working_days(day, delta,
+                                                   extra_working_days,
+                                                   extra_holidays,
+                                                   keep_datetime)


Test patch:
diff --git a/workalendar/tests/test_asia.py b/workalendar/tests/test_asia.py
index dbcf215..3c85468 100644
--- a/workalendar/tests/test_asia.py
+++ b/workalendar/tests/test_asia.py
@@ -2,7 +2,30 @@ from datetime import date
 from workalendar.tests import GenericCalendarTest
 
 from workalendar.asia import HongKong, Japan, Qatar, Singapore
-from workalendar.asia import SouthKorea, Taiwan, Malaysia
+from workalendar.asia import SouthKorea, Taiwan, Malaysia, China
+
+
+class ChinaTest(GenericCalendarTest):
+
+    cal_class = China
+
+    def test_year_2018(self):
+        holidays = self.cal.holidays_set(2018)
+        self.assertIn(date(2018, 1, 1), holidays)    # New year
+        self.assertIn(date(2018, 2, 15), holidays)   # Spring Festival
+        self.assertIn(date(2018, 2, 21), holidays)   # Spring Festival
+        self.assertIn(date(2018, 10, 1), holidays)   # National Day
+        self.assertIn(date(2018, 10, 7), holidays)   # National Day
+
+        # Variable days
+        self.assertIn(date(2018, 4, 5), holidays)    # Ching Ming Festival
+        self.assertIn(date(2018, 4, 7), holidays)    # Ching Ming Festival
+        self.assertIn(date(2018, 4, 29), holidays)   # Labour Day Holiday
+        self.assertIn(date(2018, 5, 1), holidays)    # Labour Day Holiday
+        self.assertIn(date(2018, 6, 18), holidays)   # Dragon Boat Festival
+        self.assertIn(date(2018, 9, 24), holidays)   # Mid-Autumn Festival
+        self.assertIn(date(2018, 12, 30), holidays)  # New year
+        self.assertIn(date(2018, 12, 31), holidays)  # New year
 
 
 class HongKongTest(GenericCalendarTest):
diff --git a/workalendar/tests/test_registry_asia.py b/workalendar/tests/test_registry_asia.py
index f4da73b..9773b3a 100644
--- a/workalendar/tests/test_registry_asia.py
+++ b/workalendar/tests/test_registry_asia.py
@@ -2,6 +2,7 @@
 from unittest import TestCase
 
 from workalendar.asia import (
+    China,
     HongKong,
     Japan,
     Malaysia,
@@ -18,6 +19,7 @@ class RegistryAsia(TestCase):
     def test_asia(self):
         classes = (v for k, v in registry.region_registry.items())
         classes = list(classes)
+        self.assertIn(China, classes)
         self.assertIn(HongKong, classes)
         self.assertIn(Japan, classes)
         self.assertIn(Malaysia, classes)