diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index e171b0463..bdfa0fcce 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -12,7 +12,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.2' + php-version: '8.1' coverage: none - name: Install composer dependencies @@ -21,4 +21,4 @@ jobs: COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }} - name: Run PHPStan - run: ./vendor/bin/phpstan --error-format=github + run: ./vendor/bin/phpstan diff --git a/lang/turkey/tr/holidays.json b/lang/turkey/tr/holidays.json new file mode 100644 index 000000000..964fc9e90 --- /dev/null +++ b/lang/turkey/tr/holidays.json @@ -0,0 +1,28 @@ +{ + "New Year's Day": "Yılbaşı", + "Eid al-Fitr Eve": "Ramazan Bayramı Arifesi", + "Eid al-Fitr": "Ramazan Bayramı 1. Gün", + "Eid al-Fitr Day 2": "Ramazan Bayramı 2. Gün", + "Eid al-Fitr Day 3": "Ramazan Bayramı 3. Gün", + "National Sovereignty and Children's Day": "Ulusal Egemenlik ve Çocuk Bayramı", + "Labor and Solidarity Day": "Emek ve Dayanışma Günü", + "Commemoration of Atatürk, Youth and Sports Day": "Atatürk'ü Anma, Gençlik ve Spor Bayramı", + "Eid al-Adha Eve": "Kurban Bayramı Arifesi", + "Eid al-Adha": "Kurban Bayramı 1. Gün", + "Eid al-Adha Day 2": "Kurban Bayramı 2. Gün", + "Eid al-Adha Day 3": "Kurban Bayramı 3. Gün", + "Eid al-Adha Day 4": "Kurban Bayramı 4. Gün", + "Democracy and National Unity Day": "Demokrasi ve Millî Birlik Günü", + "Victory Day": "Zafer Bayramı", + "Republic Day Eve": "Cumhuriyet Bayramı Arifesi", + "Republic Day": "Cumhuriyet Bayramı", + "2. Eid al-Fitr Eve": "2. Ramazan Bayramı Arifesi", + "2. Eid al-Fitr": "2. Ramazan Bayramı 1. Gün", + "2. Eid al-Fitr Day 2": "2. Ramazan Bayramı 2. Gün", + "2. Eid al-Fitr Day 3": "2. Ramazan Bayramı 3. Gün", + "2. Eid al-Adha Eve": "2. Kurban Bayramı Arifesi", + "2. Eid al-Adha": "2. Kurban Bayramı 1. Gün", + "2. Eid al-Adha Day 2": "2. Kurban Bayramı 2. Gün", + "2. Eid al-Adha Day 3": "2. Kurban Bayramı 3. Gün", + "2. Eid al-Adha Day 4": "2. Kurban Bayramı 4. Gün" +} diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 7363751c6..a8a609863 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,13 +1,58 @@ parameters: ignoreErrors: + - + message: "#^Access to undefined constant Spatie\\\\Holidays\\\\Countries\\\\Albania\\:\\:arafat\\.$#" + count: 1 + path: src/Countries/Albania.php + + - + message: "#^Access to undefined constant Spatie\\\\Holidays\\\\Countries\\\\Albania\\:\\:ashura\\.$#" + count: 1 + path: src/Countries/Albania.php + + - + message: "#^Access to undefined constant Spatie\\\\Holidays\\\\Countries\\\\Albania\\:\\:islamicNewYear\\.$#" + count: 1 + path: src/Countries/Albania.php + + - + message: "#^Access to undefined constant Spatie\\\\Holidays\\\\Countries\\\\Albania\\:\\:prophetMuhammadBirthday\\.$#" + count: 1 + path: src/Countries/Albania.php + + - + message: "#^Argument of an invalid type Carbon\\\\CarbonPeriod supplied for foreach, only iterables are supported\\.$#" + count: 1 + path: src/Countries/Country.php + - message: "#^Argument of an invalid type array\\\\|false supplied for foreach, only iterables are supported\\.$#" count: 1 path: src/Countries/Country.php - - message: "#^Ternary operator condition is always true\\.$#" - count: 3 + message: "#^Binary operation \"\\+\" between non\\-falsy\\-string and 1 results in an error\\.$#" + count: 1 + path: src/Countries/Country.php + + - + message: "#^Access to undefined constant Spatie\\\\Holidays\\\\Countries\\\\Turkey\\:\\:arafat\\.$#" + count: 1 + path: src/Countries/Turkey.php + + - + message: "#^Access to undefined constant Spatie\\\\Holidays\\\\Countries\\\\Turkey\\:\\:ashura\\.$#" + count: 1 + path: src/Countries/Turkey.php + + - + message: "#^Access to undefined constant Spatie\\\\Holidays\\\\Countries\\\\Turkey\\:\\:islamicNewYear\\.$#" + count: 1 + path: src/Countries/Turkey.php + + - + message: "#^Access to undefined constant Spatie\\\\Holidays\\\\Countries\\\\Turkey\\:\\:prophetMuhammadBirthday\\.$#" + count: 1 path: src/Countries/Turkey.php - diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 4bfcedd04..cf486a4f4 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -8,3 +8,5 @@ parameters: ignoreErrors: - '#Unsafe usage of new static#' + - '#Cannot call method startOfDay\(\) on Carbon\\CarbonImmutable\|null#' + - '#Cannot call method addDays\(\) on Carbon\\CarbonImmutable\|null#' diff --git a/src/Calendars/IslamicCalendar.php b/src/Calendars/IslamicCalendar.php new file mode 100644 index 000000000..33b6be64f --- /dev/null +++ b/src/Calendars/IslamicCalendar.php @@ -0,0 +1,141 @@ + */ + public function eidAlFitr(int $year, int $totalDays = 3): array + { + return $this->getMultiDayHoliday(self::eidAlFitr, $year, $totalDays); + } + + /** @return array */ + public function eidAlAdha(int $year, int $totalDays = 4): array + { + return $this->getMultiDayHoliday(self::eidAlAdha, $year, $totalDays); + } + + /** @return array */ + protected function ashura(int $year, int $totalDays = 2): array + { + return $this->getMultiDayHoliday(self::ashura, $year, $totalDays); + } + + protected function arafat(int $year): CarbonImmutable + { + return $this->getSingleDayHoliday(self::arafat, $year); + } + + protected function islamicNewYear(int $year): CarbonImmutable + { + return $this->getSingleDayHoliday(self::islamicNewYear, $year); + } + + protected function prophetMuhammadBirthday(int $year): CarbonImmutable + { + return $this->getSingleDayHoliday(self::prophetMuhammadBirthday, $year); + } + + /** @param array $collection */ + protected function getSingleDayHoliday(array $collection, int $year): CarbonImmutable + { + $date = $collection[$year] ?? null; + + if ($date === null) { + throw InvalidYear::range($this->countryCode(), 1970, 2037); + } + + $date = CarbonImmutable::createFromFormat('Y-m-d', "{$year}-{$date}")?->startOfDay(); + + if ($date === null) { + throw new InvalidFormatException('Invalid date for holiday'); + } + + return $date; + } + + /** + * @param array> $collection + * @return array + */ + protected function getMultiDayHoliday(array $collection, int $year, int $totalDays): array + { + $date = $collection[$year] ?? null; + + if ($date === null) { + throw InvalidYear::range($this->countryCode(), 1970, 2037); + } + + $overlap = $this->getOverlapping($collection, $year, $totalDays); + + if ($overlap) { + $period = $this->createPeriod($overlap, $year - 1, $totalDays); + + $date = [$period, $date]; + } + + if (! is_array($date)) { + return [$this->createPeriod($date, $year, $totalDays)]; + } + + // Twice a year + $periods = []; + $dates = $date; + + /** @var CarbonPeriod|string $date */ + foreach ($dates as $date) { + if ($date instanceof CarbonPeriod) { + $periods[] = $date; + + continue; + } + + $periods[] = $this->createPeriod($date, $year, $totalDays); + } + + return $periods; + } + + protected function createPeriod(string $date, int $year, int $totalDays): CarbonPeriod + { + $start = CarbonImmutable::createFromFormat('Y-m-d', "{$year}-{$date}")?->startOfDay(); + $end = $start->addDays($totalDays - 1)->startOfDay(); + + return CarbonPeriod::create($start, '1 day', $end); + } + + /** @param array> $collection */ + protected function getOverlapping(array $collection, int $year, int $totalDays): ?string + { + if ($year === 1970) { + return null; + } + + $date = $collection[$year - 1] ?? null; + + if ($date === null) { + throw InvalidYear::range($this->countryCode(), 1970, 2037); + } + + if (is_array($date)) { + $date = end($date); + } + + $start = CarbonImmutable::createFromFormat('Y-m-d', "{$year}-{$date}")?->startOfDay(); + $end = $start->addDays($totalDays - 1)->startOfDay(); + + if ($end->year !== $year) { + return (string) $date; + } + + return null; + } +} diff --git a/src/Concerns/Translatable.php b/src/Concerns/Translatable.php index 4b33cbb64..afd9f2084 100644 --- a/src/Concerns/Translatable.php +++ b/src/Concerns/Translatable.php @@ -8,22 +8,19 @@ trait Translatable { public function translate(string $country, string $name, ?string $locale = null): string { - if ($locale === null) { - return $name; - - } - if ($locale === $this->defaultLocale()) { return $name; } + $locale = $locale ?? $this->defaultLocale(); + $countryName = strtolower($country); $filePath = __DIR__."/../../lang/{$countryName}/{$locale}/holidays.json"; if (file_exists($filePath)) { $content = file_get_contents($filePath); } else { - throw InvalidLocale::notFound($country, $locale); + return $name; } if ($content === false) { @@ -33,10 +30,6 @@ public function translate(string $country, string $name, ?string $locale = null) /** @var array $data */ $data = json_decode($content, true); - if (! isset($data[$name])) { - return $name; - } - - return $data[$name]; + return $data[$name] ?? $name; } } diff --git a/src/Contracts/Islamic.php b/src/Contracts/Islamic.php new file mode 100644 index 000000000..0ce2696c3 --- /dev/null +++ b/src/Contracts/Islamic.php @@ -0,0 +1,12 @@ + */ + public function islamicHolidays(int $year): array; +} diff --git a/src/Countries/Albania.php b/src/Countries/Albania.php index 65424c769..29f0868f0 100644 --- a/src/Countries/Albania.php +++ b/src/Countries/Albania.php @@ -3,14 +3,45 @@ namespace Spatie\Holidays\Countries; use Carbon\CarbonImmutable; +use Spatie\Holidays\Calendars\IslamicCalendar; use Spatie\Holidays\Concerns\Translatable; use Spatie\Holidays\Contracts\HasTranslations; +use Spatie\Holidays\Contracts\Islamic; use Spatie\Holidays\Exceptions\InvalidYear; -class Albania extends Country implements HasTranslations +class Albania extends Country implements HasTranslations, Islamic { + use IslamicCalendar; use Translatable; + public const eidAlFitr = [ + 2024 => '04-10', + 2025 => '03-30', + 2026 => '03-20', + 2027 => '03-09', + 2028 => '02-26', + 2029 => '02-14', + 2030 => '02-04', + 2031 => '01-24', + 2032 => '01-14', + 2033 => '01-02', + 2034 => '12-12', + ]; + + public const eidAlAdha = [ + 2024 => '06-17', + 2025 => '06-07', + 2026 => '05-27', + 2027 => '05-17', + 2028 => '05-05', + 2029 => '04-24', + 2030 => '04-14', + 2031 => '04-03', + 2032 => '03-22', + 2033 => '03-12', + 2034 => '03-01', + ]; + public function countryCode(): string { return 'al'; @@ -33,7 +64,10 @@ protected function allHolidays(int $year): array 'Dita e Çlirimit' => '11-29', 'Dita Kombëtare e Rinisë' => '12-08', 'Krishtlindja' => '12-25', - ], $this->variableHolidays($year)); + ], + $this->variableHolidays($year), + $this->islamicHolidays($year), + ); } /** @return array */ @@ -42,53 +76,22 @@ protected function variableHolidays(int $year): array return array_filter([ 'E diela e Pashkëve Katolike' => $this->easter($year), 'E diela e Pashkëve Ortodokse' => $this->orthodoxEaster($year), - 'Dita e Bajramit të Madh' => $this->getEidAlFitrHoliday($year), - 'Dita e Kurban Bajramit' => $this->getEidAlAdhaHoliday($year), ]); } - private function getEidAlFitrHoliday(int $year): string + public function islamicHolidays(int $year): array { /** * Provided until 2034 by qppstudio.net. * https://www.qppstudio.net/global-holidays-observances/eid-al-fitr-end-of-ramadan.htm */ - return match ($year) { - 2024 => '04-10', - 2025 => '03-30', - 2026 => '03-20', - 2027 => '03-09', - 2028 => '02-26', - 2029 => '02-14', - 2030 => '02-04', - 2031 => '01-24', - 2032 => '01-14', - 2033 => '01-02', - 2034 => '12-12', - default => throw InvalidYear::range('Albania', 2024, 2034), - }; - } + if ($year < 2024 || $year > 2034) { + throw InvalidYear::range('Albania', 2024, 2034); + } - private function getEidAlAdhaHoliday(int $year): string - { - /** - * Tentative dates. - * Provided until 2034 by timeanddate.com. - * https://www.timeanddate.com/holidays/us/eid-al-adha - */ - return match ($year) { - 2024 => '06-17', - 2025 => '06-07', - 2026 => '05-27', - 2027 => '05-17', - 2028 => '05-05', - 2029 => '04-24', - 2030 => '04-14', - 2031 => '04-03', - 2032 => '03-22', - 2033 => '03-12', - 2034 => '03-01', - default => throw InvalidYear::range('Albania', 2024, 2034), - }; + return [ + 'Dita e Bajramit të Madh' => self::eidAlFitr[$year], + 'Dita e Kurban Bajramit' => self::eidAlAdha[$year], + ]; } } diff --git a/src/Countries/Bahrain.php b/src/Countries/Bahrain.php index 9d9b4d6ba..6fcf009d6 100644 --- a/src/Countries/Bahrain.php +++ b/src/Countries/Bahrain.php @@ -2,17 +2,17 @@ namespace Spatie\Holidays\Countries; -use Carbon\CarbonImmutable; -use Carbon\CarbonInterface; +use Spatie\Holidays\Calendars\IslamicCalendar; use Spatie\Holidays\Concerns\Translatable; use Spatie\Holidays\Contracts\HasTranslations; -use Spatie\Holidays\Exceptions\InvalidYear; +use Spatie\Holidays\Contracts\Islamic; -class Bahrain extends Country implements HasTranslations +class Bahrain extends Country implements HasTranslations, Islamic { + use IslamicCalendar; use Translatable; - protected const EID_AL_FITR_HOLIDAYS = [ + protected const eidAlFitr = [ 2020 => '05-24', 2021 => '05-13', 2022 => '05-02', @@ -33,7 +33,7 @@ class Bahrain extends Country implements HasTranslations 2037 => '11-09', ]; - protected const ARAFAT_DAY_HOLIDAYS = [ + protected const arafat = [ 2020 => '07-30', 2021 => '07-19', 2022 => '07-09', @@ -54,7 +54,7 @@ class Bahrain extends Country implements HasTranslations 2037 => '01-26', ]; - protected const EID_AL_ADHA_HOLIDAYS = [ + protected const eidAlAdha = [ 2020 => '07-31', 2021 => '07-20', 2022 => '07-09', @@ -75,7 +75,7 @@ class Bahrain extends Country implements HasTranslations 2037 => '01-27', ]; - protected const ISLAMIC_NEW_YEAR_HOLIDAYS = [ + protected const islamicNewYear = [ 2020 => '08-20', 2021 => '08-09', 2022 => '07-30', @@ -96,7 +96,7 @@ class Bahrain extends Country implements HasTranslations 2037 => '02-17', ]; - protected const ASHURA_HOLIDAYS = [ + protected const ashura = [ 2020 => '08-30', 2021 => '08-19', 2022 => '08-08', @@ -117,7 +117,7 @@ class Bahrain extends Country implements HasTranslations 2037 => '02-25', ]; - protected const PROPHET_MUHAMMAD_BIRTHDAY_HOLIDAYS = [ + protected const prophetMuhammadBirthday = [ 2020 => '10-29', 2021 => '10-21', 2022 => '10-08', @@ -150,72 +150,32 @@ public function defaultLocale(): string protected function allHolidays(int $year): array { - $variableHolidays = $this->variableHolidays($year); - return array_merge([ 'New Year\'s Day' => '1-1', 'Labour Day' => '5-1', 'National Day' => '12-16', 'National Day 2' => '12-17', - ], $variableHolidays); + ], + $this->islamicHolidays($year) + ); } - /** - * @return array - */ - protected function variableHolidays(int $year): array + public function islamicHolidays(int $year): array { + $eidAlFitr = $this->eidAlFitr($year); + $eidAlAdha = $this->eidAlAdha($year, 3); + $ashura = $this->ashura($year); + $holidays = [ - ['EID_AL_FITR_HOLIDAYS', 'Eid al-Fitr', 3], - ['EID_AL_ADHA_HOLIDAYS', 'Eid al-Adha', 3], - ['ARAFAT_DAY_HOLIDAYS', 'Arafat Day'], - ['ISLAMIC_NEW_YEAR_HOLIDAYS', 'Islamic New Year'], - ['ASHURA_HOLIDAYS', 'Ashura', 2], - ['PROPHET_MUHAMMAD_BIRTHDAY_HOLIDAYS', 'Birthday of the Prophet Muhammad'], + 'Arafat Day' => $this->arafat($year), + 'Islamic New Year' => $this->islamicNewYear($year), + 'Birthday of the Prophet Muhammad' => $this->prophetMuhammadBirthday($year), ]; - $dates = []; - foreach ($holidays as $holiday) { - $dates = array_merge($dates, $this->getIslamicHolidayDatesForYear(constant('self::'.$holiday[0]), $year, $holiday[1], $holiday[2] ?? 1)); - } - - return $dates; - } - - /** - * Prepare holiday dates for the given year. - * - * @param array $holidayDates Array mapping years to dates. - * @param int $year The year for which to prepare holiday dates. - * @param string $holidayName The name of the holiday. - * @param int $duration The duration of the holiday in days. - * @return array An array of holiday dates. - */ - private function getIslamicHolidayDatesForYear(array $holidayDates, int $year, string $holidayName, int $duration = 1): array - { - $dates = []; - - if ($year < 2020) { - throw InvalidYear::yearTooLow(2020); - } - - if (! isset($holidayDates[$year])) { - return $dates; - } - - $startDay = CarbonImmutable::createFromFormat('Y-m-d', sprintf('%s-%s', $year, $holidayDates[$year])); - - if ($duration === 1) { - // For single-day holidays, use the holiday name without "Day" - $dates[$holidayName] = $startDay; - } else { - // For multi-day holidays, append "Day N" for the second day onwards - for ($i = 0; $i < $duration; $i++) { - $dayLabel = $i === 0 ? $holidayName : sprintf('%s Day %d', $holidayName, $i + 1); - $dates[$dayLabel] = $startDay->addDays($i); - } - } - - return $dates; + return array_merge($holidays, + $this->convertPeriods('Eid al-Adha', $year, $eidAlAdha[0]), + $this->convertPeriods('Eid al-Fitr', $year, $eidAlFitr[0]), + $this->convertPeriods('Ashura', $year, $ashura[0]), + ); } } diff --git a/src/Countries/Canada.php b/src/Countries/Canada.php index 021738ac9..dd606a430 100644 --- a/src/Countries/Canada.php +++ b/src/Countries/Canada.php @@ -32,7 +32,7 @@ protected function variableHolidays(int $year): array $easter = $this->easter($year); // the Monday preceding May 25 - $victoriaDay = CarbonImmutable::createFromFormat('Y-m-d', "{$year}-05-25") + $victoriaDay = CarbonImmutable::createFromDate($year, 5, 25) ->previous('Monday'); return [ diff --git a/src/Countries/Country.php b/src/Countries/Country.php index c0a668606..4fbf79b7c 100644 --- a/src/Countries/Country.php +++ b/src/Countries/Country.php @@ -3,6 +3,9 @@ namespace Spatie\Holidays\Countries; use Carbon\CarbonImmutable; +use Carbon\CarbonInterface; +use Carbon\CarbonPeriod; +use Carbon\Exceptions\InvalidFormatException; use Spatie\Holidays\Contracts\HasTranslations; use Spatie\Holidays\Exceptions\InvalidCountry; use Spatie\Holidays\Exceptions\InvalidYear; @@ -31,6 +34,10 @@ public function get(int $year, ?string $locale = null): array } } + if ($date === null) { + throw new InvalidFormatException("Invalid date for holiday `{$name}`"); + } + if ($this instanceof HasTranslations) { $name = $this->translate(basename(str_replace('\\', '/', static::class)), $name, $locale); } @@ -120,4 +127,48 @@ protected function ensureYearCanBeCalculated(int $year): void throw InvalidYear::yearTooHigh(2038); } } + + /** + * Convert holidays that are represented as CarbonPeriods to an array of CarbonImmutable dates. + * This is useful for holidays like `Eid-al-Fitr` that happen on multiple days. + * + * @return array + */ + protected function convertPeriods( + string $name, + int $year, + CarbonPeriod $period, + string $suffix = 'Day', + string $prefix = '', + bool $includeEve = false, + ): array { + $allDays = []; + + if ($includeEve) { + $eve = $period->first()?->subDay(); + + if ($eve && $eve->year === $year) { + $allDays[$name.' Eve'] = $eve->toImmutable(); + } + } + + /** @var CarbonInterface $day */ + foreach ($period as $index => $day) { + if ($day->year !== $year) { + continue; // Lunar based holidays can overlap in 2 years + } + + if ($index > 0) { + $formattedSuffix = " {$suffix} ".$index + 1; + } else { + $formattedSuffix = ''; + } + + $holidayName = "{$prefix}{$name}{$formattedSuffix}"; + + $allDays[$holidayName] = $day->toImmutable(); + } + + return $allDays; + } } diff --git a/src/Countries/Egypt.php b/src/Countries/Egypt.php index d6f8af4ef..bb3e5c743 100644 --- a/src/Countries/Egypt.php +++ b/src/Countries/Egypt.php @@ -4,15 +4,18 @@ use Carbon\CarbonImmutable; use Carbon\CarbonInterface; +use Spatie\Holidays\Calendars\IslamicCalendar; use Spatie\Holidays\Concerns\Translatable; use Spatie\Holidays\Contracts\HasTranslations; +use Spatie\Holidays\Contracts\Islamic; use Spatie\Holidays\Exceptions\InvalidYear; -class Egypt extends Country implements HasTranslations +class Egypt extends Country implements HasTranslations, Islamic { + use IslamicCalendar; use Translatable; - protected const EID_AL_FITR_HOLIDAYS = [ + protected const eidAlFitr = [ 2005 => '11-04', 2006 => '10-24', 2007 => '10-13', @@ -48,7 +51,7 @@ class Egypt extends Country implements HasTranslations 2037 => '11-09', ]; - protected const ARAFAT_DAY_HOLIDAYS = [ + protected const arafat = [ 2005 => '01-21', 2006 => '01-10', 2007 => '01-01', @@ -84,7 +87,7 @@ class Egypt extends Country implements HasTranslations 2037 => '01-26', ]; - protected const EID_AL_ADHA_HOLIDAYS = [ + protected const eidAlAdha = [ 2005 => '01-22', 2006 => '01-11', 2007 => '01-02', @@ -120,7 +123,7 @@ class Egypt extends Country implements HasTranslations 2037 => '01-27', ]; - protected const ISLAMIC_NEW_YEAR_HOLIDAYS = [ + protected const islamicNewYear = [ 2005 => '02-10', 2006 => '01-31', 2007 => '01-20', @@ -156,7 +159,7 @@ class Egypt extends Country implements HasTranslations 2037 => '02-17', ]; - protected const ASHURA_HOLIDAYS = [ + protected const ashura = [ 2005 => '02-19', 2006 => '02-09', 2007 => '01-29', @@ -192,7 +195,7 @@ class Egypt extends Country implements HasTranslations 2037 => '02-25', ]; - protected const PROPHET_MUHAMMAD_BIRTHDAY_HOLIDAYS = [ + protected const prophetMuhammadBirthday = [ 2005 => '04-21', 2006 => '04-11', 2007 => '03-31', @@ -240,9 +243,6 @@ public function defaultLocale(): string protected function allHolidays(int $year): array { - $fixedHolidays = $this->fixedHolidays($year); - $variableHolidays = $this->variableHolidays($year); - return array_merge([ 'New Year\'s Day' => '1-1', 'Flooding of the Nile' => '8-15', @@ -251,71 +251,23 @@ protected function allHolidays(int $year): array 'Nayrouz' => '9-11', 'September Equinox' => '9-22', 'December Solstice' => '12-21', - ], $fixedHolidays, $variableHolidays); + ], + $this->fixedHolidays($year), + $this->variableHolidays($year), + $this->islamicHolidays($year), + ); } - /** - * @return array - */ + /** @return array */ protected function variableHolidays(int $year): array { $orthodoxEaster = $this->orthodoxEaster($year); - $eidAlFitrDates = $this->getIslamicHolidayDatesForYear(self::EID_AL_FITR_HOLIDAYS, $year, 'Eid al-Fitr', 3); - $eidAlAdhaDates = $this->getIslamicHolidayDatesForYear(self::EID_AL_ADHA_HOLIDAYS, $year, 'Eid al-Adha', 4); - $arafatDayDates = $this->getIslamicHolidayDatesForYear(self::ARAFAT_DAY_HOLIDAYS, $year, 'Arafat Day'); - $islamicNewYearDates = $this->getIslamicHolidayDatesForYear(self::ISLAMIC_NEW_YEAR_HOLIDAYS, $year, 'Islamic New Year'); - $ashuraDates = $this->getIslamicHolidayDatesForYear(self::ASHURA_HOLIDAYS, $year, 'Ashura'); - $prophetMuhammadBirthdayDates = $this->getIslamicHolidayDatesForYear(self::PROPHET_MUHAMMAD_BIRTHDAY_HOLIDAYS, $year, 'Birthday of the Prophet Muhammad'); - - return array_merge([ + return [ 'Coptic Good Friday' => $orthodoxEaster->subDays(2)->toImmutable(), 'Coptic Holy Saturday' => $orthodoxEaster->subDays()->toImmutable(), 'Coptic Easter Sunday' => $orthodoxEaster->toImmutable(), - ], $eidAlFitrDates, $eidAlAdhaDates, $arafatDayDates, $islamicNewYearDates, $ashuraDates, $prophetMuhammadBirthdayDates); - } - - /** - * Prepare holiday dates for the given year. - * - * @param array $holidayDates Array mapping years to dates. - * @param int $year The year for which to prepare holiday dates. - * @param string $holidayName The name of the holiday. - * @param int $duration The duration of the holiday in days. - * @return array An array of holiday dates. - */ - private function getIslamicHolidayDatesForYear(array $holidayDates, int $year, string $holidayName, int $duration = 1): array - { - $dates = []; - - /** - * No reliable sources exist for Islamic holidays observed in Egypt prior to 2005. - * So we'll only calculate holidays from 2005 onwards. - * - * @see https://www.timeanddate.com/holidays/egypt - */ - if ($year < 2005) { - throw InvalidYear::yearTooLow(2005); - } - - if (! isset($holidayDates[$year])) { - return $dates; - } - - $startDay = CarbonImmutable::createFromFormat('Y-m-d', sprintf('%s-%s', $year, $holidayDates[$year])); - - if ($duration === 1) { - // For single-day holidays, use the holiday name without "Day" - $dates[$holidayName] = $startDay; - } else { - // For multi-day holidays, append "Day N" for the second day onwards - for ($i = 0; $i < $duration; $i++) { - $dayLabel = $i === 0 ? $holidayName : sprintf('%s Day %d', $holidayName, $i + 1); - $dates[$dayLabel] = $startDay->addDays($i); - } - } - - return $dates; + ]; } /** @@ -341,9 +293,35 @@ private function fixedHolidays(int $year): array return $holidays; } - /** - * @return array - */ + public function islamicHolidays(int $year): array + { + /** + * No reliable sources exist for Islamic holidays observed in Egypt prior to 2005. + * So we'll only calculate holidays from 2005 onwards. + * + * @see https://www.timeanddate.com/holidays/egypt + */ + if ($year < 2005) { + throw InvalidYear::yearTooLow(2005); + } + + $eidAlFitr = $this->eidAlFitr($year); + $eidAlAdha = $this->eidAlAdha($year); + $ashura = $this->ashura($year, 1); + + return array_merge( + [ + 'Arafat Day' => $this->arafat($year), + 'Islamic New Year' => $this->islamicNewYear($year), + 'Birthday of the Prophet Muhammad' => $this->prophetMuhammadBirthday($year), + ], + $this->convertPeriods('Eid al-Adha', $year, $eidAlAdha[0]), + $this->convertPeriods('Eid al-Fitr', $year, $eidAlFitr[0]), + $this->convertPeriods('Ashura', $year, $ashura[0]), + ); + } + + /** @return array */ private function adjustForWeekend(string $name, CarbonImmutable $date): array { $adjustedHolidays = []; diff --git a/src/Countries/Greece.php b/src/Countries/Greece.php index 095405980..290a34f10 100644 --- a/src/Countries/Greece.php +++ b/src/Countries/Greece.php @@ -35,7 +35,7 @@ protected function variableHolidays(int $year): array $megaloSavvato = $orthodoxEaster->copy()->subDay(); $deuteraPasha = $orthodoxEaster->copy()->addDay(); - $protomagia = CarbonImmutable::createFromFormat('Y-m-d', "{$year}-05-01"); + $protomagia = CarbonImmutable::createFromDate($year, 5, 1); $moveProtomagia = [$megaliParaskevi, $megaloSavvato, $orthodoxEaster, $deuteraPasha]; if (in_array($protomagia, $moveProtomagia, true)) { diff --git a/src/Countries/Netherlands.php b/src/Countries/Netherlands.php index 34f3b37fc..0e9fecb54 100644 --- a/src/Countries/Netherlands.php +++ b/src/Countries/Netherlands.php @@ -24,7 +24,7 @@ protected function allHolidays(int $year): array /** @return array */ protected function variableHolidays(int $year): array { - $koningsDag = CarbonImmutable::createFromFormat('Y-m-d', "{$year}-04-27"); + $koningsDag = CarbonImmutable::createFromDate($year, 4, 27); if ($koningsDag->isSunday()) { $koningsDag = $koningsDag->subDay(); diff --git a/src/Countries/Panama.php b/src/Countries/Panama.php index 98e10eb1d..c8229257d 100644 --- a/src/Countries/Panama.php +++ b/src/Countries/Panama.php @@ -3,6 +3,7 @@ namespace Spatie\Holidays\Countries; use Carbon\CarbonImmutable; +use Carbon\Exceptions\InvalidFormatException; class Panama extends Country { @@ -80,11 +81,14 @@ protected function calculateBridgeDays(array $fixedHolidays, int $year): array foreach ($fixedHolidays as $name => $date) { $holiday = CarbonImmutable::createFromFormat('Y-m-d', "{$year}-{$date}"); - if ($holiday !== false) { - $holidays[$name] = $holiday; - if ($holiday->isSunday()) { - $holidays[$name.' (Puente)'] = $holiday->addDay(); - } + if ($holiday === null) { + throw new InvalidFormatException("Invalid date format for holiday: {$name}"); + } + + $holidays[$name] = $holiday; + + if ($holiday->isSunday()) { + $holidays[$name.' (Puente)'] = $holiday->addDay(); } } diff --git a/src/Countries/Syria.php b/src/Countries/Syria.php index 51d9e5f68..89d278b99 100644 --- a/src/Countries/Syria.php +++ b/src/Countries/Syria.php @@ -3,11 +3,14 @@ namespace Spatie\Holidays\Countries; use Carbon\CarbonImmutable; +use Spatie\Holidays\Calendars\IslamicCalendar; use Spatie\Holidays\Concerns\Translatable; use Spatie\Holidays\Contracts\HasTranslations; +use Spatie\Holidays\Contracts\Islamic; -class Syria extends Country implements HasTranslations +class Syria extends Country implements HasTranslations, Islamic { + //use IslamicCalendar; use Translatable; public function countryCode(): string @@ -22,6 +25,7 @@ public function defaultLocale(): string protected function allHolidays(int $year): array { + // @todo the islamic holidays should be calculated return array_merge([ "New Year\n's Day" => '01-01', "Mother\n's Day" => '03-21', @@ -37,13 +41,20 @@ protected function allHolidays(int $year): array 'The commemoration of the birth of the Prophet Muhammad' => '09-15', 'The October Liberation War' => '10-06', 'Christmas' => '12-25', - ], $this->variableHolidays($year)); + ], + $this->variableHolidays($year), + $this->islamicHolidays($year), + ); } /** @return array */ protected function variableHolidays(int $year): array { - // The variable holidays all follow the lunar calendar, so their dates are not confirmed. return []; } + + public function islamicHolidays(int $year): array + { + return []; // @todo + } } diff --git a/src/Countries/Turkey.php b/src/Countries/Turkey.php index 5957d6fd3..aafd8184e 100644 --- a/src/Countries/Turkey.php +++ b/src/Countries/Turkey.php @@ -2,11 +2,16 @@ namespace Spatie\Holidays\Countries; -use Carbon\CarbonImmutable; -use RuntimeException; +use Spatie\Holidays\Calendars\IslamicCalendar; +use Spatie\Holidays\Concerns\Translatable; +use Spatie\Holidays\Contracts\HasTranslations; +use Spatie\Holidays\Contracts\Islamic; -class Turkey extends Country +class Turkey extends Country implements HasTranslations, Islamic { + use IslamicCalendar; + use Translatable; + /** * No library or built-in php intl functions convert dates properly for all years or all country including * “geniusts/hijri-dates”. It is most logical to prepare the dates between 1970 and 2037 as a constant property @@ -16,7 +21,7 @@ class Turkey extends Country * Ramadan and Sacrifice holidays vary for Turkey and other countries. * A converter algorithm that will cover all years does not seem possible. */ - public const ramadanHolidays = [ + public const eidAlFitr = [ 1970 => '12-01', 1971 => '11-20', 1972 => '11-08', @@ -93,7 +98,7 @@ class Turkey extends Country 2037 => '11-09', ]; - public const sacrificeHolidays = [ + public const eidAlAdha = [ 1970 => '02-17', 1971 => '02-16', 1972 => '01-27', @@ -175,127 +180,55 @@ public function countryCode(): string return 'tr'; } + public function defaultLocale(): string + { + return 'tr'; + } + protected function allHolidays(int $year): array { $newHolidays = []; if ($year >= 2009) { - $newHolidays['Emek ve Dayanışma Günü'] = '05-01'; + $newHolidays['Labor and Solidarity Day'] = '05-01'; } if ($year >= 2017) { - $newHolidays['Demokrasi ve Millî Birlik Günü'] = '07-15'; + $newHolidays['Democracy and National Unity Day'] = '07-15'; } return array_merge([ - 'Yılbaşı' => '01-01', - 'Ulusal Egemenlik ve Çocuk Bayramı' => '04-23', - 'Atatürk\'ü Anma, Gençlik ve Spor Bayramı' => '05-19', - 'Zafer Bayramı' => '08-30', - 'Cumhuriyet Bayramı Arifesi' => '10-28', - 'Cumhuriyet Bayramı' => '10-29', - ], $newHolidays, $this->variableHolidays($year)); + "New Year's Day" => '01-01', + "National Sovereignty and Children's Day" => '04-23', + 'Commemoration of Atatürk, Youth and Sports Day' => '05-19', + 'Victory Day' => '08-30', + 'Republic Day Eve' => '10-28', + 'Republic Day' => '10-29', + ], $newHolidays, $this->islamicHolidays($year)); } - /** @return array */ - protected function variableHolidays(int $year): array + public function islamicHolidays(int $year): array { - return array_merge([ - - ], $this->getIslamicHolidays( - year: $year, - holidays: self::ramadanHolidays, - label: 'Ramazan Bayramı', - day: 3 - ), $this->getIslamicHolidays( - year: $year, - holidays: self::sacrificeHolidays, - label: 'Kurban Bayramı', - day: 4 - )); - } - - /** - * @param array> $holidays - * @return array - */ - protected function getIslamicHolidays( - int $year, - array $holidays, - string $label, - int $day, - ): array { - $islamicHolidays = []; - $counter = 0; - - if ($year != 1970) { - $previousHoliday = is_array($holidays[$year - 1]) ? $holidays[$year - 1][1] : $holidays[$year - 1]; - - $previousHoliday = CarbonImmutable::createFromFormat('Y-m-d', ($year - 1).'-'.$previousHoliday) - ?: throw new RuntimeException('Date could not be created.'); - - if ($previousHoliday->addDays($day - 1)->year == $year) { - $islamicHolidays = $this->prepareHolidays( - holiday: $previousHoliday, - day: $day, - label: $label, - filterYear: $year - ); - $counter++; - } - } - - $currentYearHolidays = is_array($holidays[$year]) ? $holidays[$year] : [$holidays[$year]]; - - foreach ($currentYearHolidays as $currentYearHoliday) { - $currentYearHoliday = CarbonImmutable::createFromFormat('Y-m-d', "{$year}-{$currentYearHoliday}") - ?: throw new RuntimeException('Date could not be created.'); - - $islamicHolidays = array_merge($islamicHolidays, $this->prepareHolidays( - holiday: $currentYearHoliday, - day: $day, - label: $label, - filterYear: $year, - prefix: $counter ? ($counter + 1).'. ' : '' - )); - $counter++; - } - - if ($year != 2037) { - $nextHoliday = is_array($holidays[$year + 1]) ? $holidays[$year + 1][1] : $holidays[$year + 1]; - - $nextHoliday = CarbonImmutable::createFromFormat('Y-m-d', ($year + 1).'-'.$nextHoliday) - ?: throw new RuntimeException('Date could not be created.'); - - if ($nextHoliday->addDays(-1)->year == $year) { - $islamicHolidays = array_merge($islamicHolidays, $this->prepareHolidays( - holiday: $nextHoliday, - day: $day, - label: $label, - filterYear: $year, - prefix: $counter ? ($counter + 1).'. ' : '' - )); - } + $eidAlFitr = $this->eidAlFitr($year); + $eidAlAdha = $this->eidAlAdha($year); + + $holidays = array_merge( + $this->convertPeriods('Eid al-Adha', $year, $eidAlAdha[0], includeEve: true), + $this->convertPeriods('Eid al-Fitr', $year, $eidAlFitr[0], includeEve: true), + ); + + if (count($eidAlAdha) > 1) { + $holidays = array_merge($holidays, + $this->convertPeriods('2. Eid al-Adha', $year, $eidAlAdha[1], includeEve: true), + ); } - return $islamicHolidays; - } - - /** @return array */ - protected function prepareHolidays( - CarbonImmutable $holiday, - int $day, - string $label, - int $filterYear, - string $prefix = '' - ): array { - $holidays = []; - - $holidays[$prefix.$label.' Arifesi'] = $holiday->addDays(-1); - foreach (range(1, $day) as $range) { - $holidays[$prefix.$label.' '.$range.'. Gün'] = $holiday->addDays($range - 1); + if (count($eidAlFitr) > 1) { + $holidays = array_merge($holidays, + $this->convertPeriods('2. Eid al-Fitr', $year, $eidAlFitr[1], includeEve: true), + ); } - return array_filter($holidays, fn ($holiday) => $holiday->year == $filterYear); + return $holidays; } } diff --git a/tests/HolidaysTest.php b/tests/HolidaysTest.php index 99f8a792a..6c83275b9 100644 --- a/tests/HolidaysTest.php +++ b/tests/HolidaysTest.php @@ -4,7 +4,6 @@ use Spatie\Holidays\Countries\Belgium; use Spatie\Holidays\Countries\Netherlands; use Spatie\Holidays\Exceptions\InvalidCountry; -use Spatie\Holidays\Exceptions\InvalidLocale; use Spatie\Holidays\Exceptions\InvalidYear; use Spatie\Holidays\Holidays; @@ -100,6 +99,16 @@ expect($result[0]['name'])->toBe('Jour de l\'An'); }); -it('cannot get translated holiday names for unsupported locales', function () { - Holidays::for(country: 'be', year: 2020, locale: 'en')->get(); -})->throws(InvalidLocale::class, 'Locale `en` is not supported for country `Belgium`.'); +it('default when the locale file is missing', function () { + CarbonImmutable::setTestNow('2024-01-01'); + + // so we don't need to have a translation file for the language in the Country class + $holidays = Holidays::for(country: 'be', locale: 'en')->get(); + + expect($holidays) + ->toBeArray() + ->not()->toBeEmpty(); + + expect($holidays[0]['name'])->toBe('Nieuwjaar'); + expect($holidays[0]['date']->format('Y-m-d'))->toBe('2024-01-01'); +});