diff --git a/lang/morocco/ar/holidays.json b/lang/morocco/ar/holidays.json new file mode 100644 index 000000000..6a09257cf --- /dev/null +++ b/lang/morocco/ar/holidays.json @@ -0,0 +1,19 @@ +{ + "New Year\\'s Day": "رأس السنة الميلادية", + "Proclamation of Independence Day": "تقديم وثيقة الاستقلال", + "Amazigh New Year (ⵉⴹ ⵏ ⵢⵉⵏⵏⴰⵢⵔ)": "رأس السنة الأمازيغية", + "Labour Day": "عيد الشغل", + "Throne Day": "عيد العرش", + "Oued Ed-Dahab Day": "استرجاع إقليم وادي الذهب", + "Revolution Day": "ثورة الملك والشعب", + "Youth Day": "عيد الشباب", + "Green March": "ذكرى المسيرة الخضراء", + "Independence Day": "عيد الاستقلال", + "Islamic New Year": "رأس السنة الهجرية", + "Birthday of the Prophet Muhammad": "عيد المولد النبوي", + "Birthday of the Prophet Muhammad 2": "تاني ايام عيد المولد النبوي", + "Eid al-Fitr": "عيد الفطر", + "Eid al-Fitr 2": "تاني ايام عيد الفطر", + "Eid al-Adha": "عيد الأضحى", + "Eid al-Adha 2": "تاني ايام عيد الأضحى" +} \ No newline at end of file diff --git a/src/Countries/Morocco.php b/src/Countries/Morocco.php new file mode 100644 index 000000000..2d0ed9df2 --- /dev/null +++ b/src/Countries/Morocco.php @@ -0,0 +1,153 @@ + '01-01', + 'Proclamation of Independence Day' => '01-11', + 'Amazigh New Year (ⵉⴹ ⵏ ⵢⵉⵏⵏⴰⵢⵔ)' => '01-14', + 'Labour Day' => '05-01', + 'Throne Day' => '07-30', + 'Oued Ed-Dahab Day' => '08-14', + 'Revolution Day' => '08-20', + 'Youth Day' => '08-21', + 'Green March' => '11-06', + 'Independence Day' => '11-18', + ], $this->variableHolidays($year)); + } + + /** @return array */ + protected function variableHolidays(int $year): array + { + // Calculate the current Hijri year based on the Gregorian year + $currentHijriYear = 1444 + ($year - 2022); + + /** + * The following holidays are considered public holidays in Morocco. However, their dates vary each year, + * as they are based on the Islamic Hijri (lunar) calendar. These holidays do not have a fixed date and + * occur based on the lunar calendar sequence. The order listed reflects the chronological occurrence + * of these holidays throughout the year. + */ + + // Define Islamic holidays on the Hijri calendar + $islamicHolidaysOnHijri = [ + 'Islamic New Year' => '01-01', + 'Birthday of the Prophet Muhammad' => '03-12', + 'Birthday of the Prophet Muhammad 2' => '03-13', + 'Eid al-Fitr' => '10-01', + 'Eid al-Fitr 2' => '10-02', + 'Eid al-Adha' => '12-10', + 'Eid al-Adha 2' => '12-11', + ]; + + $islamicHolidaysOnGregorian = []; + // Convert Hijri dates to Gregorian and filter based on the input year + foreach ($islamicHolidaysOnHijri as $holidayTitle => $hijriHolidayDate) { + [$hijriHolidayMonth, $hijriHolidayDay] = explode('-', $hijriHolidayDate); + $vlideYear = null; + + $GregorianDate = $this->islamicToGregorian($currentHijriYear, (int) $hijriHolidayMonth, (int) $hijriHolidayDay); + $vlideYear = $GregorianDate['year']; + $tempCurrentHijriYear = $currentHijriYear; + while ($vlideYear != $year) { + // Convert the current Hijri holiday to Gregorian + $GregorianDate = $this->islamicToGregorian($tempCurrentHijriYear--, (int) $hijriHolidayMonth, (int) $hijriHolidayDay); + $vlideYear = $GregorianDate['year']; + if ($vlideYear < 1976) { + throw InvalidYear::yearTooLow(1976); + } + } + // Store the Gregorian date of the Islamic holiday + $islamicHolidaysOnGregorian[$holidayTitle] = CarbonImmutable::createFromFormat('Y-m-d', sprintf('%s-%s-%s', $GregorianDate['year'], $GregorianDate['month'], $GregorianDate['day'])); + } + + return $islamicHolidaysOnGregorian; + } + + /** + * Converts a Hijri date to the corresponding Gregorian date. + * This function is adapted from the conversion tool used on the Moroccan + * Minister of Endowments and Islamic Affairs official website. + * https://www.habous.gov.ma/محول-التاريخ + * + * @param int $y The Hijri year. + * @param int $m The Hijri month. + * @param int $d The Hijri day. + * @return array{year: int, month: int, day: int} An array containing the corresponding Gregorian date in the format ['year' => YYYY, 'month' => MM, 'day' => DD]. + */ + private function islamicToGregorian(int $y, int $m, int $d): array + { + $delta = 0; + $jd = $this->intPart((11 * $y + 3) / 30) + 354 * $y + 30 * $m - $this->intPart(($m - 1) / 2) + $d + 1948440 - 385 + $delta; + if ($jd > 2299160) { + $l = $jd + 68569; + $n = $this->intPart((4 * $l) / 146097); + $l = $l - $this->intPart((146097 * $n + 3) / 4); + $i = $this->intPart((4000 * ($l + 1)) / 1461001); + $l = $l - $this->intPart((1461 * $i) / 4) + 31; + $j = $this->intPart((80 * $l) / 2447); + $d = $l - $this->intPart((2447 * $j) / 80); + $l = $this->intPart($j / 11); + $m = $j + 2 - 12 * $l; + $y = 100 * ($n - 49) + $i + $l; + } else { + $j = $jd + 1402; + $k = $this->intPart(($j - 1) / 1461); + $l = $j - 1461 * $k; + $n = $this->intPart(($l - 1) / 365) - $this->intPart($l / 1461); + $i = $l - 365 * $n + 30; + $j = $this->intPart((80 * $i) / 2447); + $d = $i - $this->intPart((2447 * $j) / 80); + $i = $this->intPart($j / 11); + $m = $j + 2 - 12 * $i; + $y = 4 * $k + $n + $i - 4716; + } + + return [ + 'year' => (int) $y, + 'month' => (int) $m, + 'day' => (int) $d, + ]; + } + + /** + * Rounds a floating-point number to the nearest integer. + * If the floating-point number is negative, it uses ceil function. + * If the floating-point number is positive, it uses floor function. + * + * @param float $floatNum The floating-point number to be rounded. + * @return float The rounded integer value. + */ + private function intPart($floatNum) + { + // Check if the floating-point number is negative + if ($floatNum < -0.0000001) { + // If negative, round up using ceil + return ceil($floatNum - 0.0000001); + } + + // If positive or zero, round down using floor + return floor($floatNum + 0.0000001); + } +} diff --git a/tests/.pest/snapshots/Countries/MoroccoTest/it_can_calculate_morocco_holidays_2023.snap b/tests/.pest/snapshots/Countries/MoroccoTest/it_can_calculate_morocco_holidays_2023.snap new file mode 100644 index 000000000..cd561ee93 --- /dev/null +++ b/tests/.pest/snapshots/Countries/MoroccoTest/it_can_calculate_morocco_holidays_2023.snap @@ -0,0 +1,70 @@ +[ + { + "name": "New Year's Day", + "date": "2023-01-01" + }, + { + "name": "Proclamation of Independence Day", + "date": "2023-01-11" + }, + { + "name": "Amazigh New Year (\u2d49\u2d39 \u2d4f \u2d62\u2d49\u2d4f\u2d4f\u2d30\u2d62\u2d54)", + "date": "2023-01-14" + }, + { + "name": "Eid al-Fitr", + "date": "2023-04-22" + }, + { + "name": "Eid al-Fitr 2", + "date": "2023-04-23" + }, + { + "name": "Labour Day", + "date": "2023-05-01" + }, + { + "name": "Eid al-Adha", + "date": "2023-06-29" + }, + { + "name": "Eid al-Adha 2", + "date": "2023-06-30" + }, + { + "name": "Islamic New Year", + "date": "2023-07-19" + }, + { + "name": "Throne Day", + "date": "2023-07-30" + }, + { + "name": "Oued Ed-Dahab Day", + "date": "2023-08-14" + }, + { + "name": "Revolution Day", + "date": "2023-08-20" + }, + { + "name": "Youth Day", + "date": "2023-08-21" + }, + { + "name": "Birthday of the Prophet Muhammad", + "date": "2023-09-27" + }, + { + "name": "Birthday of the Prophet Muhammad 2", + "date": "2023-09-28" + }, + { + "name": "Green March", + "date": "2023-11-06" + }, + { + "name": "Independence Day", + "date": "2023-11-18" + } +] \ No newline at end of file diff --git a/tests/.pest/snapshots/Countries/MoroccoTest/it_can_calculate_morocco_holidays_2024.snap b/tests/.pest/snapshots/Countries/MoroccoTest/it_can_calculate_morocco_holidays_2024.snap new file mode 100644 index 000000000..8f3c17ee1 --- /dev/null +++ b/tests/.pest/snapshots/Countries/MoroccoTest/it_can_calculate_morocco_holidays_2024.snap @@ -0,0 +1,70 @@ +[ + { + "name": "New Year's Day", + "date": "2024-01-01" + }, + { + "name": "Proclamation of Independence Day", + "date": "2024-01-11" + }, + { + "name": "Amazigh New Year (\u2d49\u2d39 \u2d4f \u2d62\u2d49\u2d4f\u2d4f\u2d30\u2d62\u2d54)", + "date": "2024-01-14" + }, + { + "name": "Eid al-Fitr", + "date": "2024-04-10" + }, + { + "name": "Eid al-Fitr 2", + "date": "2024-04-11" + }, + { + "name": "Labour Day", + "date": "2024-05-01" + }, + { + "name": "Eid al-Adha", + "date": "2024-06-17" + }, + { + "name": "Eid al-Adha 2", + "date": "2024-06-18" + }, + { + "name": "Islamic New Year", + "date": "2024-07-08" + }, + { + "name": "Throne Day", + "date": "2024-07-30" + }, + { + "name": "Oued Ed-Dahab Day", + "date": "2024-08-14" + }, + { + "name": "Revolution Day", + "date": "2024-08-20" + }, + { + "name": "Youth Day", + "date": "2024-08-21" + }, + { + "name": "Birthday of the Prophet Muhammad", + "date": "2024-09-16" + }, + { + "name": "Birthday of the Prophet Muhammad 2", + "date": "2024-09-17" + }, + { + "name": "Green March", + "date": "2024-11-06" + }, + { + "name": "Independence Day", + "date": "2024-11-18" + } +] \ No newline at end of file diff --git a/tests/.pest/snapshots/Countries/MoroccoTest/it_can_calculate_morocco_holidays_2025.snap b/tests/.pest/snapshots/Countries/MoroccoTest/it_can_calculate_morocco_holidays_2025.snap new file mode 100644 index 000000000..c0f36341f --- /dev/null +++ b/tests/.pest/snapshots/Countries/MoroccoTest/it_can_calculate_morocco_holidays_2025.snap @@ -0,0 +1,70 @@ +[ + { + "name": "New Year's Day", + "date": "2025-01-01" + }, + { + "name": "Proclamation of Independence Day", + "date": "2025-01-11" + }, + { + "name": "Amazigh New Year (\u2d49\u2d39 \u2d4f \u2d62\u2d49\u2d4f\u2d4f\u2d30\u2d62\u2d54)", + "date": "2025-01-14" + }, + { + "name": "Eid al-Fitr", + "date": "2025-03-31" + }, + { + "name": "Eid al-Fitr 2", + "date": "2025-04-01" + }, + { + "name": "Labour Day", + "date": "2025-05-01" + }, + { + "name": "Eid al-Adha", + "date": "2025-06-07" + }, + { + "name": "Eid al-Adha 2", + "date": "2025-06-08" + }, + { + "name": "Islamic New Year", + "date": "2025-06-27" + }, + { + "name": "Throne Day", + "date": "2025-07-30" + }, + { + "name": "Oued Ed-Dahab Day", + "date": "2025-08-14" + }, + { + "name": "Revolution Day", + "date": "2025-08-20" + }, + { + "name": "Youth Day", + "date": "2025-08-21" + }, + { + "name": "Birthday of the Prophet Muhammad", + "date": "2025-09-05" + }, + { + "name": "Birthday of the Prophet Muhammad 2", + "date": "2025-09-06" + }, + { + "name": "Green March", + "date": "2025-11-06" + }, + { + "name": "Independence Day", + "date": "2025-11-18" + } +] \ No newline at end of file diff --git a/tests/.pest/snapshots/Countries/MoroccoTest/it_can_calculate_morocco_holidays_2026.snap b/tests/.pest/snapshots/Countries/MoroccoTest/it_can_calculate_morocco_holidays_2026.snap new file mode 100644 index 000000000..648855f93 --- /dev/null +++ b/tests/.pest/snapshots/Countries/MoroccoTest/it_can_calculate_morocco_holidays_2026.snap @@ -0,0 +1,70 @@ +[ + { + "name": "New Year's Day", + "date": "2026-01-01" + }, + { + "name": "Proclamation of Independence Day", + "date": "2026-01-11" + }, + { + "name": "Amazigh New Year (\u2d49\u2d39 \u2d4f \u2d62\u2d49\u2d4f\u2d4f\u2d30\u2d62\u2d54)", + "date": "2026-01-14" + }, + { + "name": "Eid al-Fitr", + "date": "2026-03-20" + }, + { + "name": "Eid al-Fitr 2", + "date": "2026-03-21" + }, + { + "name": "Labour Day", + "date": "2026-05-01" + }, + { + "name": "Eid al-Adha", + "date": "2026-05-27" + }, + { + "name": "Eid al-Adha 2", + "date": "2026-05-28" + }, + { + "name": "Islamic New Year", + "date": "2026-06-17" + }, + { + "name": "Throne Day", + "date": "2026-07-30" + }, + { + "name": "Oued Ed-Dahab Day", + "date": "2026-08-14" + }, + { + "name": "Revolution Day", + "date": "2026-08-20" + }, + { + "name": "Youth Day", + "date": "2026-08-21" + }, + { + "name": "Birthday of the Prophet Muhammad", + "date": "2026-08-26" + }, + { + "name": "Birthday of the Prophet Muhammad 2", + "date": "2026-08-27" + }, + { + "name": "Green March", + "date": "2026-11-06" + }, + { + "name": "Independence Day", + "date": "2026-11-18" + } +] \ No newline at end of file diff --git a/tests/Countries/MoroccoTest.php b/tests/Countries/MoroccoTest.php new file mode 100644 index 000000000..d3b251ec7 --- /dev/null +++ b/tests/Countries/MoroccoTest.php @@ -0,0 +1,54 @@ +get(); + + expect($holidays) + ->toBeArray() + ->not()->toBeEmpty(); + + expect(formatDates($holidays))->toMatchSnapshot(); +}); + +it('can calculate morocco holidays 2023', function () { + CarbonImmutable::setTestNowAndTimezone('2023-01-01'); + + $holidays = Holidays::for(country: 'ma')->get(); + + expect($holidays) + ->toBeArray() + ->not()->toBeEmpty(); + + expect(formatDates($holidays))->toMatchSnapshot(); +}); + +it('can calculate morocco holidays 2025', function () { + CarbonImmutable::setTestNowAndTimezone('2025-01-01'); + + $holidays = Holidays::for(country: 'ma')->get(); + + expect($holidays) + ->toBeArray() + ->not()->toBeEmpty(); + + expect(formatDates($holidays))->toMatchSnapshot(); +}); + +it('can calculate morocco holidays 2026', function () { + CarbonImmutable::setTestNowAndTimezone('2026-01-01'); + + $holidays = Holidays::for(country: 'ma')->get(); + + expect($holidays) + ->toBeArray() + ->not()->toBeEmpty(); + + expect(formatDates($holidays))->toMatchSnapshot(); +});