Repository: peopledoc/workalendar (python)

Issue:
BUG: duplicate shifted holiday in California
``` python
from workalendar.usa import California

cal = California()

cal.holidays(2019)
Out[13]: 
[(datetime.date(2019, 1, 1), 'New year'),
 (datetime.date(2019, 1, 21), 'Birthday of Martin Luther King, Jr.'),
 (datetime.date(2019, 2, 18), "Washington's Birthday"),
 (datetime.date(2019, 3, 31), 'Cesar Chavez Day'),
 (datetime.date(2019, 4, 1), 'Cesar Chavez Day (Observed)'),  # <===============
 (datetime.date(2019, 4, 1), 'Cesar Chavez Day (Observed)'),  # <===============
 (datetime.date(2019, 5, 27), 'Memorial Day'),
 (datetime.date(2019, 7, 4), 'Independence Day'),
 (datetime.date(2019, 9, 2), 'Labor Day'),
 (datetime.date(2019, 11, 11), 'Veterans Day'),
 (datetime.date(2019, 11, 28), 'Thanksgiving Day'),
 (datetime.date(2019, 11, 29), 'Thanksgiving Friday'),
 (datetime.date(2019, 12, 25), 'Christmas Day')]
```

**Note that Cesar Chavez Day (Observed) is duplicated.**

This does not occur for 2017, 2018, 2020, and 2021. I've just tested those five years. The same issue occurs with 2019 in the CaliforniaEducation calendar.

I noticed this issue because it breaks a version of my test for PR #527.

Pull request:
Avoid Cesar Chavez Day duplicated shifts
closes #528

- [x] Tests with a significant number of years to be tested for your calendar.
- [x] Changelog amended with a mention describing your changes.


Patch:
diff --git a/Changelog.md b/Changelog.md
index 0f83bde..5e8b274 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -7,6 +7,7 @@
 - Tech: Replace occurrences of `assertEquals` with `assertEqual` to clear warnings (#533).
 - Use `include_immaculate_conception` flag for Portugal, Brazil, Argentina, Paraguay calendars (#529).
 - Added Monaco by @joaopbnogueira
+- Bugfix: Avoid Cesar Chavez Day duplicated shifts by refactoring the California shift rules (#528).
 
 ## v10.3.0 (2020-07-10)
 
diff --git a/workalendar/usa/california.py b/workalendar/usa/california.py
index 89e4801..34e0c1c 100644
--- a/workalendar/usa/california.py
+++ b/workalendar/usa/california.py
@@ -1,5 +1,7 @@
+from datetime import date
+
 from .core import UnitedStates
-from ..core import MON
+from ..core import MON, SUN
 from ..registry_tools import iso_register
 
 
@@ -10,6 +12,19 @@ class California(UnitedStates):
     include_cesar_chavez_day = True
     include_columbus_day = False
 
+    shift_exceptions = [
+        (3, 31)  # Cesar Chavez Day should not be shifted the usual way.
+    ]
+
+    def get_cesar_chavez_days(self, year):
+        """
+        Special shift rules for Cesar Chavez Day in California
+        """
+        days = super().get_cesar_chavez_days(year)
+        if date(year, 3, 31).weekday() == SUN:
+            days.append((date(year, 4, 1), "Cesar Chavez Day (Observed)"))
+        return days
+
 
 class CaliforniaEducation(California):
     """California Education
diff --git a/workalendar/usa/core.py b/workalendar/usa/core.py
index 03b97a3..64c4374 100644
--- a/workalendar/usa/core.py
+++ b/workalendar/usa/core.py
@@ -193,13 +193,12 @@ class UnitedStates(WesternCalendar):
 
     def get_cesar_chavez_days(self, year):
         """
-        Cesar Chavez day is on 31st of March, float to 1st April if Monday.
+        Cesar Chavez day is on 31st of March
 
-        Will return a list of days.
+        Will return a list of days, because in some states (California),
+        it can float to MON if it happens on SUN.
         """
         days = [(date(year, 3, 31), "Cesar Chavez Day")]
-        if date(year, 3, 31).weekday() == SUN:
-            days.append((date(year, 4, 1), "Cesar Chavez Day (Observed)"))
         return days
 
     def get_patriots_day(self, year):


Test patch:
diff --git a/workalendar/tests/test_usa.py b/workalendar/tests/test_usa.py
index c5aa23f..b1e012d 100644
--- a/workalendar/tests/test_usa.py
+++ b/workalendar/tests/test_usa.py
@@ -454,11 +454,27 @@ class CaliforniaTest(NoColumbus, UnitedStatesTest):
         self.assertIn(date(2015, 3, 31), holidays)  # Cesar Chavez Day
         self.assertIn(date(2015, 11, 27), holidays)  # Thanksgiving Friday
 
+    def test_state_year_2018(self):
+        holidays = self.cal.holidays_set(2018)
+        self.assertIn(date(2018, 3, 31), holidays)  # Cesar Chavez Day
+        # Happens on SAT, but is not shifted
+        self.assertNotIn(date(2018, 3, 30), holidays)
+        self.assertIn(date(2018, 11, 23), holidays)  # Thanksgiving Friday
+
     def test_state_year_2019(self):
         holidays = self.cal.holidays_set(2019)
         self.assertIn(date(2019, 3, 31), holidays)  # Cesar Chavez Day
+        self.assertIn(date(2019, 4, 1), holidays)  # Cesar Chavez Day Shift
         self.assertIn(date(2019, 11, 29), holidays)  # Thanksgiving Friday
 
+    def test_chavez_no_duplicates(self):
+        # See issue #528
+        holidays = self.cal.holidays(2019)
+        days = [item[0] for item in holidays]
+        assert days
+        for day in days:
+            assert days.count(day) == 1, "{} is duplicated".format(day)
+
 
 class CaliforniaEducationTest(CaliforniaTest):
     cal_class = CaliforniaEducation