diff --git a/README.md b/README.md index f38d2d9a29..d959c643f7 100644 --- a/README.md +++ b/README.md @@ -310,9 +310,9 @@ any) in brackets, available languages and additional holiday categories. All cou Belgium BE - +Regions: VLG, WBR, GER de, en_US, fr, nl, uk -BANK +BANK, SCHOOL Belize diff --git a/docs/holiday_categories.md b/docs/holiday_categories.md index 4bd1506a9f..7ccb128a7e 100644 --- a/docs/holiday_categories.md +++ b/docs/holiday_categories.md @@ -299,6 +299,29 @@ for date, name in sorted(germany_catholic.items()): print(f"{date}: {name}") ``` +### School Holidays Example (Belgium) + +Belgium provides school holidays that depend on the educational community. +Currently supported subdivisions are: + +- `VLG` → Flemish Community +- `WBR` → French Community (Wallonia + Brussels French schools) +- `GER` → German-speaking Community + +To retrieve only school holidays, use the `SCHOOL` category. + +#### Basic Example + +```python +import holidays +from holidays.constants import SCHOOL + +# Flemish Community school holidays +be_flemish_schools = holidays.Belgium(subdiv="WBR", categories=SCHOOL, years=2025) +for date, name in sorted(be_flemish_schools.items()): + print(date, name) +``` + ### Unofficial Holidays Example Get unofficial holidays in the United States: @@ -307,7 +330,7 @@ Get unofficial holidays in the United States: import holidays from holidays.constants import UNOFFICIAL -us_unofficial = holidays.UnitedStates(categories=UNOFFICIAL, years=2024) +us_unofficial = holidays.UnitedStates(categories=UNOFFICIAL, years=2022) for date, name in sorted(us_unofficial.items()): print(f"{date}: {name}") ``` diff --git a/holidays/countries/belgium.py b/holidays/countries/belgium.py index c4473459d1..0d63f6a042 100644 --- a/holidays/countries/belgium.py +++ b/holidays/countries/belgium.py @@ -10,28 +10,62 @@ # Website: https://github.com/vacanza/holidays # License: MIT (see LICENSE file) +from datetime import date, timedelta from gettext import gettext as tr -from holidays.constants import BANK, PUBLIC +from holidays.calendars.gregorian import ( + JAN, + APR, + JUN, + JUL, + AUG, + SEP, + NOV, + DEC, + MON, + SAT, + _timedelta, + _get_nth_weekday_from, + _get_nth_weekday_of_month, +) +from holidays.constants import BANK, PUBLIC, SCHOOL from holidays.groups import ChristianHolidays, InternationalHolidays from holidays.holiday_base import HolidayBase class Belgium(HolidayBase, ChristianHolidays, InternationalHolidays): - """Belgium holidays. + """ + Belgium holidays. References: * * * * + * Official education calendars: + * [Flemish Community (Vlaanderen)](https://www.vlaanderen.be/onderwijs-en-vorming/wat-mag-en-moet-op-school/schoolvakanties-vrije-dagen-en-afwezigheden/schoolvakanties) + * [French Community (Fédération Wallonie-Bruxelles)](http://www.enseignement.be/index.php?page=23953) + * [German-speaking Community (Deutschsprachige Gemeinschaft)](https://ostbelgienbildung.be/desktopdefault.aspx/tabid-2212/4397_read-31727/) + + Notes: + * Belgium has three school systems with different vacation rules: + * VLG: Flemish Community + * WBR: French Community (Wallonia + Brussels French schools) + * GER: German-speaking Community (Ostbelgien) + * School holiday rules are based on official education calendars. """ country = "BE" default_language = "nl" - supported_categories = (BANK, PUBLIC) + supported_categories = (BANK, PUBLIC, SCHOOL) supported_languages = ("de", "en_US", "fr", "nl", "uk") + subdivisions = ( + "VLG", # Flemish Community. + "WBR", # French Community (Wallonia + Brussels French schools) + "GER", # German-speaking Community. + ) + def __init__(self, *args, **kwargs): ChristianHolidays.__init__(self) InternationalHolidays.__init__(self) @@ -84,6 +118,276 @@ def _populate_bank_holidays(self): # Bank Holiday. self._add_christmas_day_two(tr("Banksluitingsdag")) + def _add_multiday_holiday( + self, start_date: date, duration_days: int, *, name: str | None = None + ) -> set[date]: + """Override to start adding holidays directly from start_date.""" + return super()._add_multiday_holiday( + _timedelta(start_date, -1), duration_days=duration_days, name=name + ) + + def _populate_subdiv_vlg_school_holidays(self): + """ + School holidays for the Flemish Community (Vlaamse Gemeenschap). + + Most vacation periods are rule-based and can be calculated + algorithmically, therefore they are implemented here. + """ + + year = self._year + easter = self._easter_sunday + + # Christmas Break: + # Starts on the Monday of the week containing Christmas, + # or on the following Monday if Christmas falls on a weekend. + # The break lasts two weeks (14 days) and spans December and January, + # therefore the January portion is added separately. + + # Christmas Break. + name = tr("Kerstvakantie") + + # December portion of the current Christmas Break. + christmas = self._christmas_day + christmas_start = _get_nth_weekday_from( + 1 if self._is_weekend(christmas) else -1, MON, christmas + ) + self._add_multiday_holiday(christmas_start, 32 - christmas_start.day, name=name) + + # January portion of the previous Christmas Break. + new_year = date(year, JAN, 1) + january_duration = (4 - new_year.weekday()) % 7 + 3 + self._add_multiday_holiday(new_year, january_duration, name=name) + + # Carnival Break: + # Starts on Carnival Monday, 48 days before Easter. + # The break lasts one week (7 days). + + # Carnival Break. + self._add_multiday_holiday(_timedelta(easter, -48), 7, name=tr("Krokusvakantie")) + + # Easter Break: + # The start date depends on the date of Easter: + # - If Easter falls in March, the break starts on the Monday after Easter. + # - If Easter falls on or after April 16, the break starts 13 days before Easter. + # - Otherwise, the break starts on the first Monday of April. + # The break normally lasts two weeks (14 days), extended to include Easter Monday + # when necessary. + easter_start = _get_nth_weekday_of_month(1, MON, APR, year) + easter_duration = 14 + if easter.month == 3: + easter_start = _timedelta(easter, +1) + elif easter.day >= 16: + easter_start = _timedelta(easter, -13) + easter_duration = 15 + # Easter Break. + self._add_multiday_holiday(easter_start, easter_duration, name=tr("Paasvakantie")) + + # Labor Day. + self._add_labor_day(tr("Dag van de Arbeid")) + + # Ascension Day. + self._add_ascension_thursday(tr("O. L. H. Hemelvaart")) + + # Friday after Ascension Day. + self._add_holiday_40_days_past_easter(tr("Vrijdag na O. L. H. Hemelvaart")) + + # Whit Monday. + self._add_whit_monday(tr("Pinkstermaandag")) + + # Summer Break: + # Fixed period running from July 1 through August 31. + # The break lasts 62 days. + + # Summer Break. + self._add_multiday_holiday(date(year, JUL, 1), 62, name=tr("Zomervakantie")) + + # Autumn Break: + # Starts on the Monday of the week containing November 1, + # or on the following Monday if November 1 falls on Sunday. + # The break lasts one week (7 days). + + nov_1 = date(year, NOV, 1) + autumn_start = _get_nth_weekday_from(1 if self._is_sunday(nov_1) else -1, MON, nov_1) + # Autumn Break. + self._add_multiday_holiday(autumn_start, 7, name=tr("Herfstvakantie")) + + # Armistice Day. + self._add_remembrance_day(tr("Wapenstilstand")) + + def _populate_subdiv_wbr_school_holidays(self): + """ + School holidays for the French Community (Wallonie-Bruxelles). + Based on the official compulsory education calendar. + """ + + if self.categories != {SCHOOL}: + return + year = self._year + easter = self._easter_sunday + + # Christmas Holidays. + # Start on the Monday of the week containing Christmas + # (or the following Monday if Christmas falls on a weekend). + # The holiday period spans late December and continues into January, + # with the January portion added separately. + name = tr("Kerstvakantie") + + # Present year's Christmas Holidays. + christmas = self._christmas_day + christmas_start = _get_nth_weekday_from( + 1 if self._is_weekend(christmas) else -1, MON, christmas + ) + self._add_multiday_holiday(christmas_start, 32 - christmas_start.day, name=name) + + # Previous year's Christmas (for January spillover). + new_year = date(year, JAN, 1) + christmas_last_part_duration = (4 - new_year.weekday()) % 7 + 3 + self._add_multiday_holiday(new_year, christmas_last_part_duration, name=name) + + # Carnival Holidays: + # Start on Carnival Monday (48 days before Easter). + # Duration is two weeks. + + self._add_multiday_holiday(_timedelta(easter, days=-48), 14, name=tr("Krokusvakantie")) + + # Spring Holidays (WBR, post-2022): + # Start on the last Monday of April. + # If this falls before April 25, shift to the following Monday (in May). + + apr_30 = date(year, APR, 30) + + spring_start = _get_nth_weekday_from(-1, MON, apr_30) + + if spring_start.day < 25: + spring_start += timedelta(days=7) + self._add_multiday_holiday(spring_start, 14, name=tr("Lentevakantie")) + + # Summer holidays. + summer_start = _get_nth_weekday_of_month(1, SAT, JUL, year) + # If the first Saturday is July 1 (too early), shift to the next Saturday. + if summer_start.day == 1: + summer_start += timedelta(days=7) + summer_end = _get_nth_weekday_of_month(4, MON, AUG, year) + duration = (summer_end - summer_start).days + self._add_multiday_holiday(summer_start, duration, name=tr("Zomervakantie")) + + # Armistice Day. + self._add_remembrance_day(tr("Wapenstilstand")) + + # Labour Day. + self._add_labor_day(tr("Dag van de Arbeid")) + + # French Community Day. + self._add_holiday(tr("Feestdag van de Franse Gemeenschap"), date(year, SEP, 27)) + + # Easter Monday. + self._add_easter_monday(tr("Paasmaandag")) + + # Ascension Day. + self._add_ascension_thursday(tr("O. L. H. Hemelvaart")) + + # Whit Monday. + self._add_whit_monday(tr("Pinkstermaandag")) + + # Autumn Holidays: + # - Start → second last Monday of October. + # - Before the 2022-2023 school year → duration was 1 week. + # - From the 2022-2023 school year → duration is 2 weeks. + + # Autumn Holidays. + nov_first = date(year, 11, 1) + last_monday = nov_first - timedelta(days=(nov_first.weekday() - MON) % 7) + autumn_start = last_monday - timedelta(days=7) + if year >= 2022: + self._add_multiday_holiday(autumn_start, 14, name=tr("Herfstvakantie")) + else: + self._add_multiday_holiday(autumn_start, 7, name=tr("Herfstvakantie")) + + def _populate_subdiv_ger_school_holidays(self): + """ + School holidays for the German-speaking Community (Deutschsprachige Gemeinschaft). + + Based on official education calendar structure. + Most vacation periods follow patterns similar to other Belgian communities, + with some variations in timing and duration. + """ + + if self.categories != {SCHOOL}: + return + year = self._year + easter = self._easter_sunday + + # Christmas Holidays. + # Start on the Monday of the week containing Christmas + # (or the following Monday if Christmas falls on a weekend). + # The holiday period spans late December and continues into January, + # with the January portion added separately. + name = tr("Kerstvakantie") + + # Present year's Christmas Holidays. + christmas = self._christmas_day + christmas_start = _get_nth_weekday_from( + 1 if self._is_weekend(christmas) else -1, MON, christmas + ) + self._add_multiday_holiday(christmas_start, 32 - christmas_start.day, name=name) + + # Previous year's Christmas (for January spillover). + new_year = date(year, JAN, 1) + christmas_last_part_duration = (4 - new_year.weekday()) % 7 + 3 + self._add_multiday_holiday(new_year, christmas_last_part_duration, name=name) + + # Carnival Holidays. + # Start on Carnival Monday, which occurs 48 days before Easter. + # The vacation lasts for one week. + self._add_multiday_holiday(_timedelta(easter, days=-48), 7, name=tr("Krokusvakantie")) + + # Easter Holidays. + # Usually start on the first Monday of April. + # If Easter is very late (on or after April 18), holidays start on the Monday after Easter. + # Duration is approximately two weeks (12 days). + april_1 = date(year, APR, 1) + first_monday_april = _get_nth_weekday_from(1, MON, april_1) + + # If Easter is very late → shift after Easter + if easter.month == 4 and easter.day >= 18: + easter_start = _timedelta(easter, days=1) + else: + easter_start = first_monday_april + + # Duration is approximately two weeks (12 calendar days). + self._add_multiday_holiday(easter_start, 12, name=tr("Paasvakantie")) + + # Summer holidays. + self._add_multiday_holiday(date(year, JUN, 30), 63, name=tr("Zomervakantie")) + + # Autumn Holidays. + # Start on the Monday of the week containing November 1. + # Duration is one week in the German-speaking Community calendar. + nov_1 = date(year, NOV, 1) + autumn_start = _get_nth_weekday_from(1 if self._is_sunday(nov_1) else -1, MON, nov_1) + self._add_multiday_holiday(autumn_start, 7, name=tr("Herfstvakantie")) + + # Armistice Day. + self._add_remembrance_day(tr("Wapenstilstand")) + + # Labour Day. + self._add_labor_day(tr("Dag van de Arbeid")) + + # German Community Day. + self._add_holiday(tr("Feestdag van de Duitstalige Gemeenschap"), date(year, NOV, 15)) + + # Easter Monday. + self._add_easter_monday(tr("Paasmaandag")) + + # Ascension Day. + self._add_ascension_thursday(tr("O. L. H. Hemelvaart")) + + # Whit Monday. + self._add_whit_monday(tr("Pinkstermaandag")) + + # Christmas Eve. + self._add_holiday(tr("Heiligavond"), date(year, DEC, 24)) + class BE(Belgium): pass diff --git a/holidays/locale/de/LC_MESSAGES/BE.po b/holidays/locale/de/LC_MESSAGES/BE.po index e467e0f6c1..df1f07453a 100644 --- a/holidays/locale/de/LC_MESSAGES/BE.po +++ b/holidays/locale/de/LC_MESSAGES/BE.po @@ -87,3 +87,39 @@ msgstr "Freitag nach Christi Himmelfahrt" #. Bank Holiday. msgid "Banksluitingsdag" msgstr "Bankschlusstag" + +#. Christmas Holidays. +msgid "Kerstvakantie" +msgstr "Weihnachtsferien" + +#. Carnival Holidays. +msgid "Krokusvakantie" +msgstr "Karnevalsferien" + +#. Easter Holidays. +msgid "Paasvakantie" +msgstr "Osterferien" + +#. Summer Holidays. +msgid "Zomervakantie" +msgstr "Sommerferien" + +#. Autumn Holidays. +msgid "Herfstvakantie" +msgstr "Herbstferien" + +#. French Community Day. +msgid "Feestdag van de Franse Gemeenschap" +msgstr "Feiertag der Französischen Gemeinschaft" + +#. German-speaking Community Day. +msgid "Feestdag van de Duitstalige Gemeenschap" +msgstr "Feiertag der Deutschsprachigen Gemeinschaft" + +#. Christmas Eve. +msgid "Heiligavond" +msgstr "Heiligabend" + +#. Spring Day. +msgid "Lentevakantie" +msgstr "Frühlingsferien" diff --git a/holidays/locale/en_US/LC_MESSAGES/BE.po b/holidays/locale/en_US/LC_MESSAGES/BE.po index 6d499a4560..1c6c81c6d6 100644 --- a/holidays/locale/en_US/LC_MESSAGES/BE.po +++ b/holidays/locale/en_US/LC_MESSAGES/BE.po @@ -87,3 +87,39 @@ msgstr "Friday after Ascension Day" #. Bank Holiday. msgid "Banksluitingsdag" msgstr "Bank Holiday" + +#. Christmas Holidays. +msgid "Kerstvakantie" +msgstr "Christmas Holidays" + +#. Carnival Holidays. +msgid "Krokusvakantie" +msgstr "Carnival Holidays" + +#. Easter Holidays. +msgid "Paasvakantie" +msgstr "Easter Holidays" + +#. Summer Holidays. +msgid "Zomervakantie" +msgstr "Summer Holidays" + +#. Autumn Holidays. +msgid "Herfstvakantie" +msgstr "Autumn Holidays" + +#. French Community Day. +msgid "Feestdag van de Franse Gemeenschap" +msgstr "French Community Day" + +#. German Community Day. +msgid "Feestdag van de Duitstalige Gemeenschap" +msgstr "German-speaking Community Day" + +#. Christmas Eve. +msgid "Heiligavond" +msgstr "Christmas Eve" + +#. Spring Holidays. +msgid "Lentevakantie" +msgstr "Spring Holidays" diff --git a/holidays/locale/fr/LC_MESSAGES/BE.po b/holidays/locale/fr/LC_MESSAGES/BE.po index 1dbb1eb70d..b6a8397e7b 100644 --- a/holidays/locale/fr/LC_MESSAGES/BE.po +++ b/holidays/locale/fr/LC_MESSAGES/BE.po @@ -87,3 +87,39 @@ msgstr "Vendredi suivant l'Ascension" #. Bank Holiday. msgid "Banksluitingsdag" msgstr "Jour de fermeture bancaire" + +#. Christmas Holidays. +msgid "Kerstvakantie" +msgstr "Vacances de Noël" + +#. Carnival Holidays. +msgid "Krokusvakantie" +msgstr "Vacances de carnaval" + +#. Easter Holidays. +msgid "Paasvakantie" +msgstr "Vacances de Pâques" + +#. Summer Holidays. +msgid "Zomervakantie" +msgstr "Vacances d'été" + +#. Autumn Holidays. +msgid "Herfstvakantie" +msgstr "Vacances d'automne" + +#. French Community Day. +msgid "Feestdag van de Franse Gemeenschap" +msgstr "Fête de la Communauté française" + +#. German Community Day. +msgid "Feestdag van de Duitstalige Gemeenschap" +msgstr "Fête de la Communauté germanophone" + +#. Christmas Eve. +msgid "Heiligavond" +msgstr "Réveillon de Noël" + +#. Spring Holidays. +msgid "Lentevakantie" +msgstr "Vacances de printemps" diff --git a/holidays/locale/nl/LC_MESSAGES/BE.po b/holidays/locale/nl/LC_MESSAGES/BE.po index a55e83d6ea..1106a79bae 100644 --- a/holidays/locale/nl/LC_MESSAGES/BE.po +++ b/holidays/locale/nl/LC_MESSAGES/BE.po @@ -87,3 +87,39 @@ msgstr "" #. Bank Holiday. msgid "Banksluitingsdag" msgstr "" + +#. Christmas Holidays. +msgid "Kerstvakantie" +msgstr "" + +#. Carnival Holidays. +msgid "Krokusvakantie" +msgstr "" + +#. Easter Holidays. +msgid "Paasvakantie" +msgstr "" + +#. Summer Holidays. +msgid "Zomervakantie" +msgstr "" + +#. Autumn Holidays. +msgid "Herfstvakantie" +msgstr "" + +#. French Community Day. +msgid "Feestdag van de Franse Gemeenschap" +msgstr "" + +#. German-speaking Community Day. +msgid "Feestdag van de Duitstalige Gemeenschap" +msgstr "" + +#. Christmas Eve. +msgid "Heiligavond" +msgstr "" + +#. Spring Holidays. +msgid "Lentevakantie" +msgstr "" diff --git a/holidays/locale/uk/LC_MESSAGES/BE.po b/holidays/locale/uk/LC_MESSAGES/BE.po index 7ba38640b4..3f98ed97eb 100644 --- a/holidays/locale/uk/LC_MESSAGES/BE.po +++ b/holidays/locale/uk/LC_MESSAGES/BE.po @@ -87,3 +87,39 @@ msgstr "Пʼятниця після Вознесіння Господнього" #. Bank Holiday. msgid "Banksluitingsdag" msgstr "Банківський вихідний" + +#. Christmas Holidays. +msgid "Kerstvakantie" +msgstr "Різдвяні канікули" + +#. Carnival Holidays. +msgid "Krokusvakantie" +msgstr "Карнавальні канікули" + +#. Easter Holidays. +msgid "Paasvakantie" +msgstr "Великодні канікули" + +#. Summer Holidays. +msgid "Zomervakantie" +msgstr "Літні канікули" + +#. Autumn Holidays. +msgid "Herfstvakantie" +msgstr "Осінні канікули" + +#. French Community Day. +msgid "Feestdag van de Franse Gemeenschap" +msgstr "День Французької спільноти" + +#. German-speaking Community Day. +msgid "Feestdag van de Duitstalige Gemeenschap" +msgstr "День Німецькомовної спільноти" + +#. Christmas Eve. +msgid "Heiligavond" +msgstr "Святвечір" + +#. Spring Holidays. +msgid "Lentevakantie" +msgstr "Весняні канікули" diff --git a/tests/countries/test_belgium.py b/tests/countries/test_belgium.py index b54c8a08b3..ab45c0b342 100644 --- a/tests/countries/test_belgium.py +++ b/tests/countries/test_belgium.py @@ -12,6 +12,7 @@ from unittest import TestCase +from holidays.constants import SCHOOL from holidays.countries.belgium import Belgium from tests.common import CommonCountryTests @@ -299,3 +300,48 @@ def test_l10n_uk(self): ("2022-12-25", "Різдво Христове"), ("2022-12-26", "Банківський вихідний"), ) + + def test_vlg_summer_holidays(self): + name = "Zomervakantie" + + self.assertHolidayName( + name, + Belgium(subdiv="VLG", categories=SCHOOL), + "2025-07-05", + "2024-07-06", + "2023-07-01", + "2022-08-07", + "2021-08-18", + ) + + def test_wbr_summer_holidays(self): + name = "Zomervakantie" + + self.assertHolidayName( + name, + Belgium(subdiv="WBR", categories=SCHOOL), + "2025-08-11", + "2024-07-06", + "2023-07-26", + "2022-08-07", + "2021-08-18", + ) + + def test_ger_summer_holidays(self): + name = "Zomervakantie" + + self.assertHolidayName( + name, + Belgium(subdiv="GER", categories=SCHOOL), + "2025-07-07", + "2024-08-30", + "2023-07-20", + "2022-07-10", + "2022-07-14", + "2021-08-04", + ) + + def test_school_not_in_public(self): + + self.assertNoHoliday("2024-07-06") + self.assertNoHoliday("2024-05-02")