Repository: peopledoc/workalendar
Pull request #508 (2020-06-05 16:18:44)

Moving Fat Tuesday calculation into the `workalendar.core` module
Also known as Mardi Gras, Carnaval, etc. it's used in at least 3 countries and some States / Counties in the USA.

- [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 9069c09..fbca79c 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -3,6 +3,7 @@
 ## master (unreleased)
 
 - Small fixes (docstrings, use of extends, etc) on Cayman Islands calendar (#507).
+- Moving Carnaval / Mardi Gras / Fat Tuesday calculation into the `workalendar.core` module, because it's used in at least 3 countries and some States / Counties in the USA.
 
 ## v10.0.0 (2020-06-05)
 
diff --git a/workalendar/africa/angola.py b/workalendar/africa/angola.py
index c58db3f..9172c91 100644
--- a/workalendar/africa/angola.py
+++ b/workalendar/africa/angola.py
@@ -1,12 +1,13 @@
-from datetime import timedelta
 from ..core import WesternCalendar, ChristianMixin
 from ..registry_tools import iso_register
 
 
 @iso_register('AO')
 class Angola(WesternCalendar, ChristianMixin):
-    """Angola"""
+    "Angola"
 
+    include_fat_tuesday = True
+    fat_tuesday_label = "Dia de Carnaval"
     include_good_friday = True
     include_easter_sunday = True
     include_christmas = True
@@ -20,12 +21,3 @@ class Angola(WesternCalendar, ChristianMixin):
         (9, 17, "Dia do Fundador da Nação e do Herói Nacional"),
         (11, 11, "Dia da Independência Nacional"),
     )
-
-    def get_variable_entrudo(self, year):
-        easter_sunday = self.get_easter_sunday(year)
-        return easter_sunday - timedelta(days=47)
-
-    def get_variable_days(self, year):
-        days = super().get_variable_days(year)
-        days.append((self.get_variable_entrudo(year), "Dia de Carnaval"))
-        return days
diff --git a/workalendar/america/argentina.py b/workalendar/america/argentina.py
index b684b3c..b07d523 100644
--- a/workalendar/america/argentina.py
+++ b/workalendar/america/argentina.py
@@ -10,6 +10,8 @@ from ..registry_tools import iso_register
 class Argentina(WesternCalendar, ChristianMixin):
     'Argentina'
 
+    include_fat_tuesday = True
+    fat_tuesday_label = "Carnival"
     include_good_friday = True
     include_easter_saturday = True
     include_easter_sunday = True
@@ -130,10 +132,6 @@ class Argentina(WesternCalendar, ChristianMixin):
             (self.get_easter_sunday(year) - timedelta(days=48),
                 "Carnival Lunes"))
 
-        days.append(
-            (self.get_easter_sunday(year) - timedelta(days=47),
-                "Carnival"))
-
         days.append(
             self.get_malvinas_day(year))
 
diff --git a/workalendar/america/brazil.py b/workalendar/america/brazil.py
index dd1d728..2fb57c7 100644
--- a/workalendar/america/brazil.py
+++ b/workalendar/america/brazil.py
@@ -33,14 +33,6 @@ class Brazil(WesternCalendar, ChristianMixin):
     include_nossa_senhora_conceicao = False
     include_easter_sunday = True
 
-    def get_carnaval(self, year):
-        """
-        Return the third day of Carnaval (Tuesday)
-
-        This day is shared holidays by several Brazil states.
-        """
-        return self.get_easter_sunday(year) - timedelta(days=47)
-
     def get_variable_days(self, year):
         days = super().get_variable_days(year)
         if self.include_sao_jose:
@@ -231,6 +223,8 @@ class BrazilRioDeJaneiro(Brazil):
         (4, 23, "Dia de São Jorge"),
         (3, 1, "Aniversário da Cidade do Rio de Janeiro"),
     )
+    include_fat_tuesday = True
+    fat_tuesday_label = "Carnaval"
     include_servidor_publico = True
     servidor_publico_label = "Dia do Funcionário Público"
     include_consciencia_negra = True
@@ -247,7 +241,6 @@ class BrazilRioDeJaneiro(Brazil):
 
     def get_variable_days(self, year):
         days = super().get_variable_days(year)
-        days.append((self.get_carnaval(year), "Carnaval"))
         days.append((self.get_dia_do_comercio(year), "Dia do Comércio"))
         return days
 
@@ -309,6 +302,8 @@ class BrazilSaoPauloCity(BrazilSaoPauloState):
     FIXED_HOLIDAYS = BrazilSaoPauloState.FIXED_HOLIDAYS + (
         (1, 25, "Anniversary of the city of São Paulo"),
     )
+    include_fat_tuesday = True
+    fat_tuesday_label = "Carnaval"
     include_easter_sunday = True
     include_corpus_christi = True
     include_good_friday = True
@@ -316,11 +311,6 @@ class BrazilSaoPauloCity(BrazilSaoPauloState):
     include_consciencia_negra = True
     consciencia_negra_label = "Dia da Consciência Negra"
 
-    def get_variable_days(self, year):
-        days = super().get_variable_days(year)
-        days.append((self.get_carnaval(year), "Carnaval"))
-        return days
-
 
 @iso_register('BR-SE')
 class BrazilSergipe(Brazil):
@@ -388,6 +378,8 @@ class BrazilSerraCity(BrazilEspiritoSanto):
     FIXED_HOLIDAYS = BrazilEspiritoSanto.FIXED_HOLIDAYS + (
         (12, 26, "Dia do Serrano"),
     )
+    include_fat_tuesday = True
+    fat_tuesday_label = "Carnaval"
     include_ash_wednesday = True
     ash_wednesday_label = "Quarta-feira de cinzas"
     include_good_friday = True
@@ -397,9 +389,8 @@ class BrazilSerraCity(BrazilEspiritoSanto):
 
     def get_variable_days(self, year):
         days = super().get_variable_days(year)
-        carnaval_tuesday = self.get_carnaval(year)
+        carnaval_tuesday = self.get_fat_tuesday(year)
         days.append((carnaval_tuesday - timedelta(days=1), "Carnaval Monday"))
-        days.append((carnaval_tuesday, "Carnaval"))
         return days
 
 
@@ -583,6 +574,8 @@ class BrazilBankCalendar(Brazil):
     Calendar that considers only working days for bank transactions
     for companies and the general public
     """
+    include_fat_tuesday = True
+    fat_tuesday_label = "Tuesday carnaval"
     include_good_friday = True
     include_ash_wednesday = True
     include_corpus_christi = True
@@ -610,22 +603,17 @@ class BrazilBankCalendar(Brazil):
         day for only internal bank transactions
         """
         days = super().get_variable_days(year)
-        tuesday_carnaval = self.get_carnaval(year)
+        tuesday_carnaval = self.get_fat_tuesday(year)
         monday_carnaval = tuesday_carnaval - timedelta(days=1)
 
-        carnaval_days = [
-            (monday_carnaval, "Monday carnaval"),
-            (tuesday_carnaval, "Tuesday carnaval"),
-        ]
+        days.append((monday_carnaval, "Monday carnaval"))
 
-        non_working_days = [
-            (
-                self.get_last_day_of_year_for_only_internal_bank_trans(year),
-                "Last day of year for only internal bank transactions"
-            )
-        ]
+        days.append((
+            self.get_last_day_of_year_for_only_internal_bank_trans(year),
+            "Last day of year for only internal bank transactions"
+        ))
 
-        return days + carnaval_days + non_working_days
+        return days
 
     def find_following_working_day(self, day):
         """
diff --git a/workalendar/core.py b/workalendar/core.py
index c701b66..8b0a2d0 100644
--- a/workalendar/core.py
+++ b/workalendar/core.py
@@ -10,7 +10,7 @@ from calverter import Calverter
 from dateutil import easter
 from lunardate import LunarDate
 
-from .exceptions import UnsupportedDateType
+from .exceptions import UnsupportedDateType, CalendarError
 
 MON, TUE, WED, THU, FRI, SAT, SUN = range(7)
 
@@ -373,6 +373,10 @@ class ChristianMixin(Calendar):
     include_epiphany = False
     include_clean_monday = False
     include_annunciation = False
+    include_fat_tuesday = False
+    # Fat tuesday forced to `None` to make sure this value is always set
+    # We've seen that there was a wide variety of labels.
+    fat_tuesday_label = None
     include_ash_wednesday = False
     ash_wednesday_label = "Ash Wednesday"
     include_palm_sunday = False
@@ -398,6 +402,14 @@ class ChristianMixin(Calendar):
     boxing_day_label = "Boxing Day"
     include_all_souls = False
 
+    def get_fat_tuesday(self, year):
+        if not self.fat_tuesday_label:
+            raise CalendarError(
+                "Improperly configured: please provide a "
+                "`fat_tuesday_label` value")
+        sunday = self.get_easter_sunday(year)
+        return sunday - timedelta(days=47)
+
     def get_ash_wednesday(self, year):
         sunday = self.get_easter_sunday(year)
         return sunday - timedelta(days=46)
@@ -476,6 +488,10 @@ class ChristianMixin(Calendar):
             days.append((self.get_clean_monday(year), "Clean Monday"))
         if self.include_annunciation:
             days.append((date(year, 3, 25), "Annunciation"))
+        if self.include_fat_tuesday:
+            days.append(
+                (self.get_fat_tuesday(year), self.fat_tuesday_label)
+            )
         if self.include_ash_wednesday:
             days.append(
                 (self.get_ash_wednesday(year), self.ash_wednesday_label)
diff --git a/workalendar/usa/alabama.py b/workalendar/usa/alabama.py
index d0d6f48..785fe64 100644
--- a/workalendar/usa/alabama.py
+++ b/workalendar/usa/alabama.py
@@ -16,12 +16,12 @@ class Alabama(UnitedStates):
 
 class AlabamaBaldwinCounty(Alabama):
     "Baldwin County, Alabama"
-    include_mardi_gras = True
+    include_fat_tuesday = True
 
 
 class AlabamaMobileCounty(Alabama):
     "Mobile County, Alabama"
-    include_mardi_gras = True
+    include_fat_tuesday = True
 
 
 class AlabamaPerryCounty(Alabama):
diff --git a/workalendar/usa/core.py b/workalendar/usa/core.py
index 2ad8e0f..2968ab9 100644
--- a/workalendar/usa/core.py
+++ b/workalendar/usa/core.py
@@ -59,7 +59,8 @@ class UnitedStates(WesternCalendar, ChristianMixin):
     national_memorial_day_label = "Memorial Day"
 
     # Some regional variants
-    include_mardi_gras = False
+    include_fat_tuesday = False
+    fat_tuesday_label = "Mardi Gras"
 
     # Shift day mechanism
     # These days won't be shifted to next MON or previous FRI
@@ -272,13 +273,6 @@ class UnitedStates(WesternCalendar, ChristianMixin):
             self.national_memorial_day_label
         )
 
-    def get_mardi_gras(self, year):
-        """
-        Mardi Gras is the Tuesday happening 47 days before Easter
-        """
-        sunday = self.get_easter_sunday(year)
-        return (sunday - timedelta(days=47), "Mardi Gras")
-
     def get_variable_days(self, year):
         # usual variable days
         days = super().get_variable_days(year)
@@ -295,9 +289,6 @@ class UnitedStates(WesternCalendar, ChristianMixin):
                 "Thanksgiving Day"),
         ])
 
-        if self.include_mardi_gras:
-            days.append(self.get_mardi_gras(year))
-
         if self.include_federal_presidents_day:
             days.append(self.get_presidents_day(year))
 
diff --git a/workalendar/usa/florida.py b/workalendar/usa/florida.py
index 06f88d6..bec0770 100644
--- a/workalendar/usa/florida.py
+++ b/workalendar/usa/florida.py
@@ -78,7 +78,7 @@ class FloridaLegal(Florida):
         (4, 2, 'Pascua Florida Day'),
         (6, 14, 'Flag Day'),
     )
-    include_mardi_gras = True
+    include_fat_tuesday = True
     include_lincoln_birthday = True
     include_federal_presidents_day = True
     include_good_friday = True
diff --git a/workalendar/usa/louisiana.py b/workalendar/usa/louisiana.py
index 6eda673..0f5c349 100644
--- a/workalendar/usa/louisiana.py
+++ b/workalendar/usa/louisiana.py
@@ -8,4 +8,4 @@ class Louisiana(UnitedStates):
     include_good_friday = True
     include_election_day_even = True
     include_columbus_day = False
-    include_mardi_gras = True
+    include_fat_tuesday = True


Test patch:
diff --git a/workalendar/tests/test_africa.py b/workalendar/tests/test_africa.py
index 990dabc..c00ac70 100644
--- a/workalendar/tests/test_africa.py
+++ b/workalendar/tests/test_africa.py
@@ -549,6 +549,12 @@ class AngolaTest(GenericCalendarTest):
         # Dia do Natal – 25 de Dezembro
         self.assertIn(date(2018, 12, 25), holidays)  # Natal
 
+    def test_carnaval_label(self):
+        holidays = self.cal.holidays(2018)
+        holidays_dict = dict(holidays)
+        label_carnaval = holidays_dict[date(2018, 2, 13)]
+        self.assertEqual(label_carnaval, "Dia de Carnaval")
+
 
 class KenyaTest(GenericCalendarTest):
     cal_class = Kenya
diff --git a/workalendar/tests/test_america.py b/workalendar/tests/test_america.py
index 2beaf89..bbea135 100644
--- a/workalendar/tests/test_america.py
+++ b/workalendar/tests/test_america.py
@@ -118,6 +118,12 @@ class ArgentinaTest(GenericCalendarTest):
             "Día Nacional de la Memoria por la Verdad y la Justicia"
         )
 
+    def test_carnival_label(self):
+        holidays = self.cal.holidays(2020)
+        holidays = dict(holidays)
+        label_carnival = holidays[date(2020, 2, 25)]
+        self.assertEqual(label_carnival, "Carnival")
+
 
 class ChileTest(GenericCalendarTest):
     cal_class = Chile
diff --git a/workalendar/tests/test_brazil.py b/workalendar/tests/test_brazil.py
index eb1d50a..94297c0 100644
--- a/workalendar/tests/test_brazil.py
+++ b/workalendar/tests/test_brazil.py
@@ -246,6 +246,12 @@ class BrazilRioDeJaneiroTest(BrazilTest):
         # Dia de Nossa Senhora da Conceição
         self.assertIn(date(2017, 12, 8), holidays)
 
+    def test_carnaval_label(self):
+        holidays = self.cal.holidays(2017)
+        holidays_dict = dict(holidays)
+        label_carnaval = holidays_dict[date(2017, 2, 28)]
+        self.assertEqual(label_carnaval, "Carnaval")
+
 
 class BrazilRioGrandeDoNorteTest(BrazilTest):
     cal_class = BrazilRioGrandeDoNorte
@@ -338,6 +344,12 @@ class SaoPauloCityTest(SaoPauloStateTest):
             "Sexta-feira da Paixão",
         )
 
+    def test_carnaval_label(self):
+        holidays = self.cal.holidays(2013)
+        holidays_dict = dict(holidays)
+        label_carnaval = holidays_dict[date(2013, 2, 12)]
+        self.assertEqual(label_carnaval, "Carnaval")
+
 
 class BrazilSergipeTest(BrazilTest):
     cal_class = BrazilSergipe
@@ -481,6 +493,12 @@ class BrazilSerraCityTest(BrazilEspiritoSantoTest):
             "Paixão do Cristo",
         )
 
+    def test_carnaval_label(self):
+        holidays = self.cal.holidays(2017)
+        holidays_dict = dict(holidays)
+        label_carnaval = holidays_dict[date(2017, 2, 28)]
+        self.assertEqual(label_carnaval, "Carnaval")
+
 
 class BrazilRioBrancoCityTest(BrazilAcreTest):
     """
@@ -899,6 +917,12 @@ class BrazilBankCalendarTest(BrazilTest):
         working_day = self.cal.find_following_working_day(already_working_day)
         self.assertEquals(working_day, date(2017, 7, 25))
 
+    def test_carnaval_label(self):
+        holidays = self.cal.holidays(2017)
+        holidays_dict = dict(holidays)
+        label_carnaval = holidays_dict[date(2017, 2, 28)]
+        self.assertEqual(label_carnaval, "Tuesday carnaval")
+
 
 class TestIBGERegister(TestCase):
 
diff --git a/workalendar/tests/test_usa.py b/workalendar/tests/test_usa.py
index fdc616f..c5aa23f 100644
--- a/workalendar/tests/test_usa.py
+++ b/workalendar/tests/test_usa.py
@@ -207,7 +207,7 @@ class UnitedStatesTest(GenericCalendarTest):
 
     def test_mardi_gras(self):
         year = 2017
-        day, _ = self.cal.get_mardi_gras(year)
+        day = self.cal.get_fat_tuesday(year)
         holidays = self.cal.holidays_set(year)
         self.assertNotIn(day, holidays)
 
@@ -316,9 +316,11 @@ class IncludeMardiGras:
     """
     def test_mardi_gras(self):
         year = 2017
-        day, _ = self.cal.get_mardi_gras(year)
-        holidays = self.cal.holidays_set(year)
-        self.assertIn(day, holidays)
+        day = self.cal.get_fat_tuesday(year)
+        holidays = self.cal.holidays(year)
+        holidays_dict = dict(holidays)
+        self.assertIn(day, holidays_dict)
+        self.assertEqual(holidays_dict[day], "Mardi Gras")
 
 
 class AlabamaTest(UnitedStatesTest):