Repository: peopledoc/workalendar
Pull request #392 (2019-07-22 13:45:07)

Removed ``EphemMixin`` in favor of astronomical functions
This Mixin was barely a mixin at all: none of its method was using ``self``.

it refers to #302, but not essentially. The goal of this refactoring is to simplify classes hierarchy, and it might also simplify the swap between `ephem` and `skyfield`.


- [x] Tests with a significant number of years to be tested for your calendar.
- [x] Changelog amended with a mention describing your changes. **Note** *Please do NOT change the version number here. It's the project maintainers' duty.*



Patch:
diff --git a/Changelog.md b/Changelog.md
index 9bda400..147d934 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -5,6 +5,7 @@
 **Deprecation Notice:** *The global ISO registry now returns plain `dict` objects from its various methods.*
 
 - Global registry now returns plain built-in dicts (#375).
+- Removed `EphemMixin` in favor of astronomical functions (#302).
 
 ## v5.2.3 (2019-07-11)
 
diff --git a/workalendar/asia/hong_kong.py b/workalendar/asia/hong_kong.py
index 12eddc4..0e2ffb3 100644
--- a/workalendar/asia/hong_kong.py
+++ b/workalendar/asia/hong_kong.py
@@ -4,14 +4,13 @@ from __future__ import (absolute_import, division, print_function,
 
 from datetime import timedelta
 
-from ..core import ChineseNewYearCalendar, WesternCalendar
-from ..core import ChristianMixin, EphemMixin
+from ..core import ChineseNewYearCalendar, WesternCalendar, ChristianMixin
+from ..astronomy import solar_term
 from ..registry_tools import iso_register
 
 
 @iso_register('HK')
-class HongKong(EphemMixin, WesternCalendar,
-               ChineseNewYearCalendar, ChristianMixin):
+class HongKong(WesternCalendar, ChineseNewYearCalendar, ChristianMixin):
     "Hong Kong"
     include_good_friday = True
     include_easter_saturday = True
@@ -45,7 +44,7 @@ class HongKong(EphemMixin, WesternCalendar,
             self.shift_start_cny_sunday = True
 
         days = super(HongKong, self).get_variable_days(year)
-        chingming = EphemMixin.solar_term(self, year, 15, 'Asia/Hong_Kong')
+        chingming = solar_term(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
diff --git a/workalendar/asia/japan.py b/workalendar/asia/japan.py
index 8ecb0b0..4aae101 100644
--- a/workalendar/asia/japan.py
+++ b/workalendar/asia/japan.py
@@ -3,13 +3,14 @@ from __future__ import (absolute_import, division, print_function,
                         unicode_literals)
 from datetime import date
 
-from ..core import MON, EphemMixin
+from ..core import MON
 from ..core import WesternCalendar
+from ..astronomy import calculate_equinoxes
 from ..registry_tools import iso_register
 
 
 @iso_register('JP')
-class Japan(WesternCalendar, EphemMixin):
+class Japan(WesternCalendar):
     "Japan"
 
     FIXED_HOLIDAYS = WesternCalendar.FIXED_HOLIDAYS + (
@@ -59,7 +60,7 @@ class Japan(WesternCalendar, EphemMixin):
     def get_variable_days(self, year):
         # usual variable days
         days = super(Japan, self).get_variable_days(year)
-        equinoxes = self.calculate_equinoxes(year, 'Asia/Tokyo')
+        equinoxes = calculate_equinoxes(year, 'Asia/Tokyo')
         coming_of_age_day = Japan.get_nth_weekday_in_month(year, 1, MON, 2)
         marine_day = Japan.get_nth_weekday_in_month(year, 7, MON, 3)
         respect_for_the_aged = Japan.get_nth_weekday_in_month(year, 9, MON, 3)
diff --git a/workalendar/asia/taiwan.py b/workalendar/asia/taiwan.py
index 9bd0f61..13015d3 100644
--- a/workalendar/asia/taiwan.py
+++ b/workalendar/asia/taiwan.py
@@ -3,12 +3,12 @@ from __future__ import (absolute_import, division, print_function,
                         unicode_literals)
 
 from ..core import ChineseNewYearCalendar, WesternCalendar
-from ..core import EphemMixin
+from ..astronomy import solar_term
 from ..registry_tools import iso_register
 
 
 @iso_register('TW')
-class Taiwan(EphemMixin, ChineseNewYearCalendar, WesternCalendar):
+class Taiwan(ChineseNewYearCalendar, WesternCalendar):
     "Taiwan (Republic of China)"
     FIXED_HOLIDAYS = (
         WesternCalendar.FIXED_HOLIDAYS +
@@ -25,7 +25,7 @@ class Taiwan(EphemMixin, ChineseNewYearCalendar, WesternCalendar):
         days = super(Taiwan, self).get_variable_days(year)
         # Qingming begins when the sun reaches the celestial
         # longitude of 15° (usually around April 4th or 5th)
-        qingming = EphemMixin.solar_term(self, year, 15, 'Asia/Taipei')
+        qingming = solar_term(year, 15, 'Asia/Taipei')
 
         days.extend([
             (
diff --git a/workalendar/astronomy.py b/workalendar/astronomy.py
new file mode 100644
index 0000000..296bf6d
--- /dev/null
+++ b/workalendar/astronomy.py
@@ -0,0 +1,65 @@
+"""
+Astronomical functions
+"""
+from math import pi
+import pytz
+import ephem
+
+
+def calculate_equinoxes(year, timezone='UTC'):
+    """ calculate equinox with time zone """
+    tz = pytz.timezone(timezone)
+
+    d1 = ephem.next_equinox(str(year))
+    d = ephem.Date(str(d1))
+    equinox1 = d.datetime() + tz.utcoffset(d.datetime())
+
+    d2 = ephem.next_equinox(d1)
+    d = ephem.Date(str(d2))
+    equinox2 = d.datetime() + tz.utcoffset(d.datetime())
+
+    return (equinox1.date(), equinox2.date())
+
+
+def solar_term(year, degrees, timezone='UTC'):
+    """
+    Returns the date of the solar term for the given longitude
+    and the given year.
+
+    Solar terms are used for Chinese and Taiwanese holidays
+    (e.g. Qingming Festival in Taiwan).
+
+    More information:
+    - https://en.wikipedia.org/wiki/Solar_term
+    - https://en.wikipedia.org/wiki/Qingming
+
+    This function is adapted from the following topic:
+    https://answers.launchpad.net/pyephem/+question/110832
+    """
+    twopi = 2 * pi
+    tz = pytz.timezone(timezone)
+
+    # Find out the sun's current longitude.
+    sun = ephem.Sun(ephem.Date(str(year)))
+    # > Both hlon and hlat have a special meaning for the Sun and Moon.
+    # > For a Sun body, they give the Earth's heliocentric longitude and
+    # > latitude.
+    current_longitude = sun.hlon - pi
+
+    # Find approximately the right time of year.
+
+    target_longitude = degrees * ephem.degree
+    difference = (target_longitude - current_longitude) % twopi
+    t0 = ephem.Date(str(year)) + 365.25 * difference / twopi
+
+    # Zero in on the exact moment.
+
+    def f(t):
+        sun.compute(t)
+        longitude = sun.hlon - pi
+        return ephem.degrees(target_longitude - longitude).znorm
+
+    d = ephem.Date(ephem.newton(f, t0, t0 + ephem.minute))
+    solar_term = d.datetime() + tz.utcoffset(d.datetime())
+
+    return solar_term.date()
diff --git a/workalendar/core.py b/workalendar/core.py
index ac5ca91..7c67ade 100644
--- a/workalendar/core.py
+++ b/workalendar/core.py
@@ -6,10 +6,7 @@ from copy import copy
 import warnings
 from calendar import monthrange
 from datetime import date, timedelta, datetime
-from math import pi
 
-import ephem
-import pytz
 from calverter import Calverter
 from dateutil import easter
 from lunardate import LunarDate
@@ -663,64 +660,6 @@ class ChineseNewYearCalendar(LunarCalendar):
         return days
 
 
-class EphemMixin(LunarCalendar):
-    def calculate_equinoxes(self, year, timezone='UTC'):
-        """ calculate equinox with time zone """
-
-        tz = pytz.timezone(timezone)
-
-        d1 = ephem.next_equinox(str(year))
-        d = ephem.Date(str(d1))
-        equinox1 = d.datetime() + tz.utcoffset(d.datetime())
-
-        d2 = ephem.next_equinox(d1)
-        d = ephem.Date(str(d2))
-        equinox2 = d.datetime() + tz.utcoffset(d.datetime())
-
-        return (equinox1.date(), equinox2.date())
-
-    def solar_term(self, year, degrees, timezone='UTC'):
-        """
-        Returns the date of the solar term for the given longitude
-        and the given year.
-
-        Solar terms are used for Chinese and Taiwanese holidays
-        (e.g. Qingming Festival in Taiwan).
-
-        More information:
-        - https://en.wikipedia.org/wiki/Solar_term
-        - https://en.wikipedia.org/wiki/Qingming
-
-        This function is adapted from the following topic:
-        https://answers.launchpad.net/pyephem/+question/110832
-        """
-        twopi = 2 * pi
-        tz = pytz.timezone(timezone)
-
-        # Find out the sun's current longitude.
-
-        sun = ephem.Sun(ephem.Date(str(year)))
-        current_longitude = sun.hlong - pi
-
-        # Find approximately the right time of year.
-
-        target_longitude = degrees * ephem.degree
-        difference = (target_longitude - current_longitude) % twopi
-        t0 = ephem.Date(str(year)) + 365.25 * difference / twopi
-
-        # Zero in on the exact moment.
-
-        def f(t):
-            sun.compute(t)
-            longitude = sun.hlong - pi
-            return ephem.degrees(target_longitude - longitude).znorm
-
-        d = ephem.Date(ephem.newton(f, t0, t0 + ephem.minute))
-        solar_term = d.datetime() + tz.utcoffset(d.datetime())
-
-        return solar_term.date()
-
-
 class CalverterMixin(Calendar):
     conversion_method = None
     ISLAMIC_HOLIDAYS = ()


Test patch:
diff --git a/workalendar/tests/test_astronomy.py b/workalendar/tests/test_astronomy.py
new file mode 100644
index 0000000..5e56e7d
--- /dev/null
+++ b/workalendar/tests/test_astronomy.py
@@ -0,0 +1,29 @@
+from datetime import date
+
+from workalendar.astronomy import calculate_equinoxes, solar_term
+
+
+def test_calculate_some_equinoxes():
+    assert calculate_equinoxes(2010) == (date(2010, 3, 20), date(2010, 9, 23))
+    assert calculate_equinoxes(2010, 'Asia/Taipei') == (
+        date(2010, 3, 21), date(2010, 9, 23)
+    )
+    assert calculate_equinoxes(2013) == (date(2013, 3, 20), date(2013, 9, 22))
+    assert calculate_equinoxes(2014) == (date(2014, 3, 20), date(2014, 9, 23))
+    assert calculate_equinoxes(2020) == (date(2020, 3, 20), date(2020, 9, 22))
+
+
+def test_qingming_festivals():
+    assert solar_term(2001, 15) == date(2001, 4, 4)
+    assert solar_term(2001, 15, 'Asia/Taipei') == date(2001, 4, 5)
+    assert solar_term(2011, 15) == date(2011, 4, 5)
+    assert solar_term(2014, 15) == date(2014, 4, 4)
+    assert solar_term(2016, 15) == date(2016, 4, 4)
+    assert solar_term(2017, 15) == date(2017, 4, 4)
+
+
+def test_qingming_festivals_hk():
+    assert solar_term(2018, 15, 'Asia/Hong_Kong') == date(2018, 4, 5)
+    assert solar_term(2019, 15, 'Asia/Hong_Kong') == date(2019, 4, 5)
+    assert solar_term(2020, 15, 'Asia/Hong_Kong') == date(2020, 4, 4)
+    assert solar_term(2021, 15, 'Asia/Hong_Kong') == date(2021, 4, 4)
diff --git a/workalendar/tests/test_core.py b/workalendar/tests/test_core.py
index 6210fc8..f1442a3 100644
--- a/workalendar/tests/test_core.py
+++ b/workalendar/tests/test_core.py
@@ -8,7 +8,6 @@ from workalendar.tests import GenericCalendarTest
 from workalendar.core import MON, TUE, THU, FRI, WED, SAT, SUN
 from workalendar.core import Calendar, LunarCalendar, WesternCalendar
 from workalendar.core import IslamicMixin, JalaliMixin, ChristianMixin
-from workalendar.core import EphemMixin
 from workalendar.exceptions import UnsupportedDateType
 
 
@@ -254,50 +253,6 @@ class JalaliMixinTest(GenericCalendarTest):
         self.assertEquals(len(days), 365)
 
 
-class EphemMixinTest(GenericCalendarTest):
-    cal_class = EphemMixin
-
-    def test_calculate_some_equinoxes(self):
-        self.assertEquals(
-            self.cal.calculate_equinoxes(2010),
-            (date(2010, 3, 20), date(2010, 9, 23))
-        )
-        self.assertEquals(
-            self.cal.calculate_equinoxes(2010, 'Asia/Taipei'),
-            (date(2010, 3, 21), date(2010, 9, 23))
-        )
-        self.assertEquals(
-            self.cal.calculate_equinoxes(2013),
-            (date(2013, 3, 20), date(2013, 9, 22))
-        )
-        self.assertEquals(
-            self.cal.calculate_equinoxes(2014),
-            (date(2014, 3, 20), date(2014, 9, 23))
-        )
-        self.assertEquals(
-            self.cal.calculate_equinoxes(2020),
-            (date(2020, 3, 20), date(2020, 9, 22))
-        )
-
-    def test_qingming_festivals(self):
-        self.assertEquals(
-            self.cal.solar_term(2001, 15),
-            date(2001, 4, 4)
-        )
-        self.assertEquals(
-            self.cal.solar_term(2001, 15, 'Asia/Taipei'),
-            date(2001, 4, 5)
-        )
-        self.assertEquals(
-            self.cal.solar_term(2011, 15),
-            date(2011, 4, 5)
-        )
-        self.assertEquals(
-            self.cal.solar_term(2014, 15),
-            date(2014, 4, 4)
-        )
-
-
 class MockChristianCalendar(WesternCalendar, ChristianMixin):
     pass