Repository: peopledoc/workalendar
Pull request #457 (2020-01-24 15:34:46)

Fix Georgia miscalculation of several state holidays
refs #451

* Fix Robert E. Lee's Birthday label rules (renamed as of year 2016 into the neutral "State Holiday")
* (hopefully) Fixed Washington's Birthday observance around Christmas time
* Fix Confederate Memorial Day label rules (renamed as of year 2016 into the neutral "State Holiday")

For the record:

Confederate Memorial Day, which was renamed into "State Holiday" used to happen on the 4th Monday of April each year, but for an unknown reason, in 2020, this state holiday happens on April 10th. And it looks like it's "just like that" and has no particular obvious reason to break the "4th Monday rule".
After thorough research  we've almost concluded that yes: it's "Governor's fiat" and I'll have to make a new research later this year

- [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 0f203a7..3d383c8 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -2,7 +2,7 @@
 
 ## master (unreleased)
 
-Nothing here yet.
+- Fix several miscalculations in Georgia (USA) calendar (#451).
 
 ## v8.0.1 (2020-01-24)
 
diff --git a/workalendar/usa/core.py b/workalendar/usa/core.py
index 62a452a..2ad8e0f 100644
--- a/workalendar/usa/core.py
+++ b/workalendar/usa/core.py
@@ -149,7 +149,7 @@ class UnitedStates(WesternCalendar, ChristianMixin):
 
     def get_confederate_day(self, year):
         """
-        Confederation memorial day is on the 4th MON of April.
+        Confederate memorial day is on the 4th MON of April.
         """
         day = self.get_nth_weekday_in_month(year, 4, MON, 4)
         return (day, "Confederate Memorial Day")
diff --git a/workalendar/usa/georgia.py b/workalendar/usa/georgia.py
index b03fb9b..c776145 100644
--- a/workalendar/usa/georgia.py
+++ b/workalendar/usa/georgia.py
@@ -11,6 +11,7 @@ class Georgia(UnitedStates):
     include_confederation_day = True
     include_federal_presidents_day = False
     label_washington_birthday_december = "Washington's Birthday (Observed)"
+    thanksgiving_friday_label = "State Holiday"
 
     def get_washington_birthday_december(self, year):
         """
@@ -19,15 +20,21 @@ class Georgia(UnitedStates):
         It's only observed in Georgia.
         """
         warnings.warn(
-            "Washington birthday rules for Georgia State are confusing. "
+            "Washington birthday rules for Georgia State are possibly wrong. "
             "Use this calendar with care")
         christmas_day = date(year, 12, 25).weekday()
-        if christmas_day == MON:
+
+        # Special case: 2011 / Source:
+        # Christmas day is SUN, but doesn't follow the same rule as year 2016.
+        # https://web.archive.org/web/20110927122533/http://www.georgia.gov/00/channel_modifieddate/0,2096,4802_64437763,00.html  # noqa
+        if year == 2011:
+            day = date(year, 12, 26)
+        elif christmas_day == MON:
             day = date(year, 12, 26)  # TUE
         elif christmas_day == TUE:
             day = date(year, 12, 24)  # MON
         elif christmas_day == WED:
-            day = date(year, 12, 26)  # TUE
+            day = date(year, 12, 24)  # TUE
         elif christmas_day == THU:
             day = date(year, 12, 26)  # FRI
         elif christmas_day == FRI:
@@ -35,19 +42,39 @@ class Georgia(UnitedStates):
         elif christmas_day == SAT:
             day = date(year, 12, 23)  # THU
         else:  # christmas_day == SUN:
-            day = date(year, 12, 23)  # FRI
+            day = date(year, 12, 27)  # FRI
         return (day, self.label_washington_birthday_december)
 
+    def get_confederate_day(self, year):
+        """
+        Confederate memorial day is on the 4th MON of April.
+
+        Exception: Year 2020, when it happened on April 10th.
+        """
+        if year >= 2016:
+            label = "State Holiday"
+        else:
+            label = "Confederate Memorial Day"
+
+        # At the moment, it's the only exception we know about.
+        if year == 2020:
+            return (date(year, 4, 10), label)
+
+        day = self.get_nth_weekday_in_month(year, 4, MON, 4)
+        return (day, label)
+
     def get_robert_lee_birthday(self, year):
         """
         Robert E. Lee's birthday.
 
-        Happens on the 4 Friday of November.
+        Happens on the day after Thanksgiving.
         """
-        return (
-            self.get_nth_weekday_in_month(year, 11, FRI, 4),
-            "Robert E. Lee's Birthday (Observed)"
-        )
+        if year < 2016:
+            label = "Robert E. Lee's Birthday (Observed)"
+        else:
+            label = "State Holiday"
+        date, _ = self.get_thanksgiving_friday(year)
+        return (date, label)
 
     def get_variable_days(self, year):
         days = super().get_variable_days(year)


Test patch:
diff --git a/workalendar/tests/test_usa.py b/workalendar/tests/test_usa.py
index bbf7198..fdc616f 100644
--- a/workalendar/tests/test_usa.py
+++ b/workalendar/tests/test_usa.py
@@ -770,44 +770,115 @@ class GeorgiaTest(NoPresidentialDay, UnitedStatesTest):
     def test_state_year_2014(self):
         holidays = self.cal.holidays_set(2014)
         self.assertIn(date(2014, 4, 28), holidays)  # Confederate Memorial
-        # FIXME: this holiday rule is Confusing, probably false
         self.assertIn(date(2014, 12, 26), holidays)  # Washington bday
 
     def test_state_year_2015(self):
         holidays = self.cal.holidays_set(2015)
         self.assertIn(date(2015, 4, 27), holidays)  # Confederate Memorial
-        # FIXME: this holiday rule is Confusing, probably false
         self.assertIn(date(2015, 12, 24), holidays)  # Washington bday
 
-    @skip("Confusing Rule, it's impossible to decide")
     def test_washington_birthday(self):
-        # Source: https://georgia.gov/popular-topic/observing-state-holidays
+        # Sources:
+        # * https://georgia.gov/popular-topic/observing-state-holidays
+        # * https://georgia.gov/popular-topic/state-holidays
+        day, _ = self.cal.get_washington_birthday_december(2020)
+        self.assertEqual(day, date(2020, 12, 24))
+
+        day, _ = self.cal.get_washington_birthday_december(2019)
+        self.assertEqual(day, date(2019, 12, 24))
+
+        day, _ = self.cal.get_washington_birthday_december(2018)
+        self.assertEqual(day, date(2018, 12, 24))
+
         day, _ = self.cal.get_washington_birthday_december(2017)
         self.assertEqual(day, date(2017, 12, 26))
 
         day, _ = self.cal.get_washington_birthday_december(2016)
-        self.assertEqual(
-            day,
-            date(2016, 12, 27),
-        )
+        self.assertEqual(day, date(2016, 12, 27))
 
         day, _ = self.cal.get_washington_birthday_december(2015)
-        self.assertEqual(
-            day,
-            date(2015, 12, 24),
-        )
+        self.assertEqual(day, date(2015, 12, 24))
 
         day, _ = self.cal.get_washington_birthday_december(2014)
-        self.assertEqual(
-            day,
-            date(2014, 12, 26),
-        )
+        self.assertEqual(day, date(2014, 12, 26))
 
         day, _ = self.cal.get_washington_birthday_december(2013)
-        self.assertEqual(
-            day,
-            date(2013, 12, 24),
-        )
+        self.assertEqual(day, date(2013, 12, 24))
+
+        day, _ = self.cal.get_washington_birthday_december(2012)
+        self.assertEqual(day, date(2012, 12, 24))
+
+        # Source:
+        # https://web.archive.org/web/20110927122533/http://www.georgia.gov/00/channel_modifieddate/0,2096,4802_64437763,00.html  # noqa
+        day, _ = self.cal.get_washington_birthday_december(2011)
+        self.assertEqual(day, date(2011, 12, 26))
+
+        # Source:
+        # https://web.archive.org/web/20100304032739/http://www.georgia.gov/00/channel_modifieddate/0,2096,4802_64437763,00.html  # noqa
+        day, _ = self.cal.get_washington_birthday_december(2010)
+        self.assertEqual(day, date(2010, 12, 23))
+
+    def test_year_2019(self):
+        holidays = self.cal.holidays_set(2019)
+        self.assertIn(date(2019, 1, 1), holidays)   # New Year
+        self.assertIn(date(2019, 1, 21), holidays)  # MLK
+        self.assertIn(date(2019, 4, 22), holidays)  # state holiday
+        self.assertIn(date(2019, 5, 27), holidays)  # memorial day
+        self.assertIn(date(2019, 7, 4), holidays)  # Independance day
+        self.assertIn(date(2019, 9, 2), holidays)  # Labor day
+        self.assertIn(date(2019, 10, 14), holidays)  # Columbus
+        self.assertIn(date(2019, 11, 11), holidays)  # Veterans
+        self.assertIn(date(2019, 11, 28), holidays)  # Thanksgiving
+        self.assertIn(date(2019, 11, 29), holidays)  # State Holiday
+        # Washington's Birthday switched to XMAS eve
+        self.assertIn(date(2019, 12, 24), holidays)
+        self.assertIn(date(2019, 12, 25), holidays)  # XMAS
+
+    def test_year_2020(self):
+        holidays = self.cal.holidays_set(2020)
+        self.assertIn(date(2020, 1, 1), holidays)   # New Year
+        self.assertIn(date(2020, 1, 20), holidays)  # MLK
+
+        # State holiday special case
+        # Confederate memorial day has been shifted to April 10th.
+        # Reason is unknown, so we're adding a single exception in the
+        # `get_confederate_day`
+        self.assertNotIn(date(2020, 4, 26), holidays)
+        self.assertIn(date(2020, 4, 10), holidays)
+
+        self.assertIn(date(2020, 5, 25), holidays)  # memorial day
+        self.assertIn(date(2020, 7, 3), holidays)  # Independance day (OBS)
+        self.assertIn(date(2020, 7, 4), holidays)  # Independance day
+        self.assertIn(date(2020, 9, 7), holidays)  # Labor day
+        self.assertIn(date(2020, 10, 12), holidays)  # Columbus
+        self.assertIn(date(2020, 11, 11), holidays)  # Veterans
+        self.assertIn(date(2020, 11, 26), holidays)  # Thanksgiving
+        self.assertIn(date(2020, 11, 27), holidays)  # State Holiday
+        self.assertIn(date(2020, 12, 24), holidays)  # Washington B'day
+        self.assertIn(date(2020, 12, 25), holidays)  # XMAS
+
+    def test_thanksgiving_friday_label(self):
+        # Overwrite UnitedStatesTest.test_thanksgiving_friday_label
+
+        # Until 2016, the 4th FRI of november was labelled
+        # "Robert E. Lee's Birthday (Observed)"
+        for year in (2013, 2014, 2015,):
+            _, label = self.cal.get_robert_lee_birthday(year)
+            self.assertEqual(label, "Robert E. Lee's Birthday (Observed)")
+
+        for year in (2016, 2017, 2018, 2019, 2020):
+            _, label = self.cal.get_robert_lee_birthday(year)
+            self.assertEqual(label, "State Holiday")
+
+    def test_get_confederate_day_label(self):
+        # Until 2016, it was labelled "Confederate Memorial Day"
+        for year in (2013, 2014, 2015,):
+            _, label = self.cal.get_confederate_day(year)
+            self.assertEqual(label, "Confederate Memorial Day")
+
+        for year in (2016, 2017, 2018, 2019, 2020):
+            _, label = self.cal.get_confederate_day(year)
+            self.assertEqual(label, "State Holiday")
 
 
 class HawaiiTest(ElectionDayEvenYears, NoColumbus, UnitedStatesTest):