Repository: peopledoc/workalendar
Pull request #639 (2021-03-12 13:53:43)

Multiple bugfixes for Bulgaria calendar
Bulgaria holidays are now computed using the Orthodox calendar, include shifting rules for holidays that fall on a week-end.

refs #596

- [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 db5f7b2..ff8eff4 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -2,7 +2,7 @@
 
 ## master (unreleased)
 
-Nothing to see here.
+- Bugfix: Bulgaria holidays are now computed using the Orthodox calendar, include shifting rules for holidays that fall on a week-end (#596).
 
 ## v15.0.2 (2021-03-05)
 
diff --git a/workalendar/europe/bulgaria.py b/workalendar/europe/bulgaria.py
index aa8ec2f..8d7c2fa 100644
--- a/workalendar/europe/bulgaria.py
+++ b/workalendar/europe/bulgaria.py
@@ -1,19 +1,20 @@
-from ..core import WesternCalendar
+from copy import copy
+from datetime import timedelta, date
+
+from ..core import OrthodoxCalendar, SAT, SUN
 from ..registry_tools import iso_register
 
 
 @iso_register('BG')
-class Bulgaria(WesternCalendar):
+class Bulgaria(OrthodoxCalendar):
     'Bulgaria'
 
-    FIXED_HOLIDAYS = WesternCalendar.FIXED_HOLIDAYS + (
+    FIXED_HOLIDAYS = OrthodoxCalendar.FIXED_HOLIDAYS + (
         (3, 3, "Liberation Day"),  # Ден на Освобождението на Б
         (5, 6, "Saint George's Day"),  # Гергьовден, ден на храброс
         (5, 24, "Saints Cyril & Methodius Day"),  # Ден на българската просвет
         (9, 6, "Unification Day"),  # Ден на Съединението
         (9, 22, "Independence Day"),  # Ден на независимостта на Б
-        # wikipedia says Non-attendance day for schools, otherwise a working da
-        # (11, 1, "National Awakening Day"),  # Ден на народните будители
     )
 
     # Civil holidays
@@ -22,12 +23,74 @@ class Bulgaria(WesternCalendar):
     labour_day_label = "International Workers' Day"
 
     # Christian holidays
+    include_good_friday = True
+    include_easter_saturday = True
     include_easter_sunday = True
     include_easter_monday = True
     include_christmas_eve = True  # Бъдни вечер
     include_christmas = True  # Рождество Христово
     include_boxing_day = True
+    # despite being an Orthodox calendar, don't observe Orthodox XMas
+    include_orthodox_christmas = False
 
     # wikipedia says The Bulgarians have two days of Christmas,
     # both called Christmas Day
     boxing_day_label = "Christmas"
+
+    def get_shifted_holidays(self, days):
+        for holiday, label in days:
+            if holiday.weekday() == SUN:
+                yield (
+                    holiday + timedelta(days=1),
+                    f'{label} shift'
+                )
+            elif holiday.weekday() == SAT:
+                yield (
+                    holiday + timedelta(days=2),
+                    f'{label} shift'
+                )
+
+    def get_fixed_holidays(self, year):
+        """
+        Return fixed holidays, with shifts computed accordingly.
+        """
+        # 2021 exception.
+        # Because May 1st is both International Workers' day and Easter
+        self.include_labour_day = (year != 2021)
+
+        # Unshifted days are here:
+        days = super().get_fixed_holidays(year)
+        days_to_inspect = copy(days)
+        for day_shifted in self.get_shifted_holidays(days_to_inspect):
+            days.append(day_shifted)
+
+        # 2021 exception.
+        # Because May 1st is both International Workers' day and Easter
+        if year == 2021:
+            days.append((date(2021, 5, 4), self.labour_day_label))
+        return days
+
+    def shift_christmas_boxing_days(self, year):
+        """
+        Return Christmas shifts.
+
+        They usually follow the same rule as used in the UK, but the fact that
+        there's a second Christmas holiday requires an override.
+        """
+        days = super().shift_christmas_boxing_days(year=year)
+        # In this case we'll need an extra day for the Second day of XMas
+        if date(year, 12, 25).weekday() == SUN:
+            days.append(
+                (date(year, 12, 28), "Christmas (in lieu)")
+            )
+        return days
+
+    def get_variable_days(self, year):
+        """
+        Return variable holidays, with Christmas shifts computed accordingly.
+        """
+        days = super().get_variable_days(year)
+        # Boxing day & XMas shift
+        shifts = self.shift_christmas_boxing_days(year=year)
+        days.extend(shifts)
+        return days


Test patch:
diff --git a/workalendar/tests/test_europe.py b/workalendar/tests/test_europe.py
index 0dfc004..9cde33c 100644
--- a/workalendar/tests/test_europe.py
+++ b/workalendar/tests/test_europe.py
@@ -112,29 +112,104 @@ class BelarusTest(GenericCalendarTest):
 class BulgariaTest(GenericCalendarTest):
     cal_class = Bulgaria
 
-    def test_year_2016(self):
-        holidays = self.cal.holidays_set(2016)
-        self.assertIn(date(2016, 1, 1), holidays)   # New Year's Day
-        self.assertIn(date(2016, 3, 3), holidays)   # Liberation Day
-        self.assertIn(date(2016, 3, 27), holidays)   # Easter Sun
-        self.assertIn(date(2016, 3, 28), holidays)   # Easter Mon
-        self.assertIn(date(2016, 5, 1), holidays)   # International Workers'
-        self.assertIn(date(2016, 5, 6), holidays)   # St George's Day
-        self.assertIn(date(2016, 5, 24), holidays)   # St Cyril & Methodius
-        self.assertIn(date(2016, 9, 6), holidays)   # Unification Day
-        self.assertIn(date(2016, 9, 22), holidays)   # Independence Day
-        self.assertIn(date(2016, 12, 24), holidays)   # Christmas Eve
-        self.assertIn(date(2016, 12, 25), holidays)   # Christmas 1
-        self.assertIn(date(2016, 12, 26), holidays)   # Christmas 2
-        # Non-attendance day for schools, otherwise a working day.
-        self.assertNotIn(date(2016, 11, 1), holidays)   # National Awakening
-
     def test_labour_day_label(self):
         holidays = self.cal.holidays(2020)
         holidays = dict(holidays)
         self.assertEqual(
             holidays[date(2020, 5, 1)], "International Workers' Day")
 
+    def test_holidays_2019(self):
+        holidays = self.cal.holidays_set(2019)
+        self.assertIn(date(2019, 1, 1), holidays)  # New Year's Day
+        self.assertIn(date(2019, 3, 3), holidays)  # Liberation Day
+        self.assertIn(date(2019, 3, 4), holidays)  # Liberation Day (shift)
+
+        self.assertIn(date(2019, 4, 26), holidays)  # Good Friday
+        self.assertIn(date(2019, 4, 27), holidays)  # Holy Saturday
+        self.assertIn(date(2019, 4, 28), holidays)  # Easter day (sunday)
+        self.assertIn(date(2019, 4, 29), holidays)  # Easter Monday
+
+        self.assertIn(date(2019, 5, 1), holidays)  # International Workers' day
+        self.assertIn(date(2019, 5, 6), holidays)  # St George's Day
+        self.assertIn(date(2019, 5, 24), holidays)  # St Cyril & Methodius
+        self.assertIn(date(2019, 9, 6), holidays)  # Unification Day
+
+        self.assertIn(date(2019, 9, 22), holidays)  # Independence Day
+        self.assertIn(date(2019, 9, 23), holidays)  # Independence Day (shift)
+
+        self.assertIn(date(2019, 12, 24), holidays)  # Christmas Eve
+        self.assertIn(date(2019, 12, 25), holidays)  # Christmas 1 (FRI)
+        self.assertIn(date(2019, 12, 26), holidays)  # Christmas 2 (SAT)
+
+    def test_holidays_2020(self):
+        holidays = self.cal.holidays_set(2020)
+        self.assertIn(date(2020, 1, 1), holidays)  # New Year's Day
+        self.assertIn(date(2020, 3, 3), holidays)  # Liberation Day
+        self.assertIn(date(2020, 4, 17), holidays)  # Good Friday
+        self.assertIn(date(2020, 4, 18), holidays)  # Holy Saturday
+        self.assertIn(date(2020, 4, 19), holidays)  # Easter day (sunday)
+        self.assertIn(date(2020, 4, 20), holidays)  # Easter Monday
+
+        self.assertIn(date(2020, 5, 1), holidays)  # International Workers' day
+        self.assertIn(date(2020, 5, 6), holidays)  # St George's Day
+        self.assertIn(date(2020, 5, 24), holidays)  # St Cyril & Methodius
+        self.assertIn(date(2020, 5, 25), holidays)  # (shift)
+        self.assertIn(date(2020, 9, 6), holidays)  # Unification Day
+        self.assertIn(date(2020, 9, 7), holidays)  # Unification Day (shift)
+
+        self.assertIn(date(2020, 9, 22), holidays)  # Independence Day
+
+        self.assertIn(date(2020, 12, 24), holidays)  # Christmas Eve
+        self.assertIn(date(2020, 12, 25), holidays)  # Christmas 1 (FRI)
+        self.assertIn(date(2020, 12, 26), holidays)  # Christmas 2 (SAT)
+        self.assertIn(date(2020, 12, 28), holidays)  # Christmas 2b (shift)
+
+    def test_holidays_2021(self):
+        holidays = self.cal.holidays_set(2021)
+        self.assertIn(date(2021, 1, 1), holidays)  # New Year's Day
+        self.assertIn(date(2021, 3, 3), holidays)  # Liberation Day
+        self.assertIn(date(2021, 4, 30), holidays)  # Good Friday
+        self.assertIn(date(2021, 5, 1), holidays)  # Holy Saturday
+        self.assertIn(date(2021, 5, 2), holidays)  # Easter day (sunday)
+        self.assertIn(date(2021, 5, 3), holidays)  # Easter Monday
+        # International Workers' day (in lieu, because easter)
+        self.assertIn(date(2021, 5, 4), holidays)
+        self.assertIn(date(2021, 5, 6), holidays)  # St George's Day
+
+        self.assertIn(date(2021, 5, 24), holidays)  # St Cyril & Methodius
+        self.assertIn(date(2021, 9, 6), holidays)  # Unification Day
+        self.assertIn(date(2021, 9, 22), holidays)  # Independence Day
+
+        self.assertIn(date(2021, 12, 24), holidays)  # Christmas Eve
+        self.assertIn(date(2021, 12, 25), holidays)  # Christmas 1 (SAT)
+        self.assertIn(date(2021, 12, 26), holidays)  # Christmas 2
+        self.assertIn(date(2021, 12, 27), holidays)  # Christmas 2b
+        self.assertIn(date(2021, 12, 28), holidays)  # Christmas 2c
+
+    def test_holidays_2022(self):
+        holidays = self.cal.holidays_set(2022)
+        self.assertIn(date(2022, 1, 1), holidays)  # New Year's Day
+        self.assertIn(date(2022, 1, 3), holidays)  # New Year's Day (shift)
+        self.assertIn(date(2022, 3, 3), holidays)  # Liberation Day
+        self.assertIn(date(2022, 4, 22), holidays)  # Good Friday
+        self.assertIn(date(2022, 4, 23), holidays)  # Holy Saturday
+        self.assertIn(date(2022, 4, 24), holidays)  # Easter day (sunday)
+        self.assertIn(date(2022, 4, 25), holidays)  # Easter Monday
+
+        self.assertIn(date(2022, 5, 1), holidays)  # International Workers' day
+        self.assertIn(date(2022, 5, 2), holidays)  # Labour day Shift
+        self.assertIn(date(2022, 5, 6), holidays)  # St George's Day
+
+        self.assertIn(date(2022, 5, 24), holidays)  # St Cyril & Methodius
+        self.assertIn(date(2022, 9, 6), holidays)  # Unification Day
+        self.assertIn(date(2022, 9, 22), holidays)  # Independence Day
+
+        self.assertIn(date(2022, 12, 24), holidays)  # Christmas Eve
+        self.assertIn(date(2022, 12, 25), holidays)  # Christmas 1 (SAT)
+        self.assertIn(date(2022, 12, 26), holidays)  # Christmas 2
+        self.assertIn(date(2022, 12, 27), holidays)  # Christmas 2b
+        self.assertIn(date(2022, 12, 28), holidays)  # Christmas 2c
+
 
 class CaymanIslandsTest(GenericCalendarTest):
     cal_class = CaymanIslands