From 6d8cc159eb4d5c3e407586a67e741c7094c46c7e Mon Sep 17 00:00:00 2001 From: Sacha Telgenhof Date: Mon, 6 Feb 2017 12:57:12 +0900 Subject: [PATCH] Created fallback calculation of the easter_days function in case the PHP extension 'calendar' is not loaded. Adjusted unit tests accordingly (included helper function for easter_days as well). --- src/Yasumi/Provider/ChristianHolidays.php | 61 ++++++++++++++++- tests/Australia/EasterMondayTest.php | 4 +- tests/Australia/GoodFridayTest.php | 3 +- tests/Ireland/EasterMondayTest.php | 4 +- tests/Ireland/EasterTest.php | 4 +- tests/Ireland/GoodFridayTest.php | 3 +- tests/Ireland/PentecostTest.php | 3 +- tests/Ireland/pentecostMondayTest.php | 3 +- tests/NewZealand/EasterMondayTest.php | 4 +- tests/NewZealand/GoodFridayTest.php | 3 +- tests/SouthAfrica/FamilyDayTest.php | 3 +- tests/SouthAfrica/GoodFridayTest.php | 3 +- tests/UnitedKingdom/EasterMondayTest.php | 4 +- tests/YasumiBase.php | 79 +++++++++++++++++++++++ 14 files changed, 153 insertions(+), 28 deletions(-) diff --git a/src/Yasumi/Provider/ChristianHolidays.php b/src/Yasumi/Provider/ChristianHolidays.php index 0dda03677..3539d4d8a 100644 --- a/src/Yasumi/Provider/ChristianHolidays.php +++ b/src/Yasumi/Provider/ChristianHolidays.php @@ -564,9 +564,17 @@ public function reformationDay($year, $timezone, $locale, $type = Holiday::TYPE_ * Easter is a festival and holiday celebrating the resurrection of Jesus Christ from the dead. Easter is celebrated * on a date based on a certain number of days after March 21st. * - * This function uses the standard PHP 'easter_days'. + * This function uses the standard PHP 'easter_days' function if the calendar extension is enabled. In case the + * calendar function is not enabled, a fallback calculation has been implemented that is based on the same + * 'easter_days' c function. * - * @see easter_days + * Note: In calendrical calculations, frequently operations called integer division are used. + * + * @see easter_days + * + * @link https://github.com/php/php-src/blob/c8aa6f3a9a3d2c114d0c5e0c9fdd0a465dbb54a5/ext/calendar/easter.c + * @link http://www.gmarts.org/index.php?go=415#EasterMallen + * @link http://www.tondering.dk/claus/cal/easter.php * * @param int $year the year for which Easter needs to be calculated * @param string $timezone the timezone in which Easter is celebrated @@ -575,8 +583,55 @@ public function reformationDay($year, $timezone, $locale, $type = Holiday::TYPE_ */ protected function calculateEaster($year, $timezone) { + if (extension_loaded('calendar')) { + $easter_days = \easter_days($year); + } else { + $golden = (int)(($year % 19) + 1); // The Golden Number + + // The Julian calendar applies to the original method from 326AD. The Gregorian calendar was first + // introduced in October 1582 in Italy. Easter algorithms using the Gregorian calendar apply to years + // 1583 AD to 4099 (A day adjustment is required in or shortly after 4100 AD). + // After 1752, most western churches have adopted the current algorithm. + if ($year <= 1752) { + $dom = ($year + (int)($year / 4) + 5) % 7; // The 'Dominical number' - finding a Sunday + if ($dom < 0) { + $dom += 7; + } + + $pfm = (3 - (11 * $golden) - 7) % 30; // Uncorrected date of the Paschal full moon + if ($pfm < 0) { + $pfm += 30; + } + } else { + $dom = ($year + (int)($year / 4) - (int)($year / 100) + (int)($year / 400)) % 7; // The 'Dominical number' - finding a Sunday + if ($dom < 0) { + $dom += 7; + } + + $solar = (int)(($year - 1600) / 100) - (int)(($year - 1600) / 400); // The solar correction + $lunar = (int)(((int)(($year - 1400) / 100) * 8) / 25); // The lunar correction + + $pfm = (3 - (11 * $golden) + $solar - $lunar) % 30; // Uncorrected date of the Paschal full moon + if ($pfm < 0) { + $pfm += 30; + } + } + + // Corrected date of the Paschal full moon, - days after 21st March + if (($pfm == 29) || ($pfm == 28 && $golden > 11)) { + --$pfm; + } + + $tmp = (4 - $pfm - $dom) % 7; + if ($tmp < 0) { + $tmp += 7; + } + + $easter_days = (int)($pfm + $tmp + 1); // Easter as the number of days after 21st March + } + $easter = new DateTime("$year-3-21", new DateTimeZone($timezone)); - $easter->add(new DateInterval('P' . \easter_days($year) . 'D')); + $easter->add(new DateInterval('P' . $easter_days . 'D')); return $easter; } diff --git a/tests/Australia/EasterMondayTest.php b/tests/Australia/EasterMondayTest.php index ddd9fde4f..00f92f09b 100644 --- a/tests/Australia/EasterMondayTest.php +++ b/tests/Australia/EasterMondayTest.php @@ -53,8 +53,8 @@ public function HolidayDataProvider() for ($y = 0; $y < 50; $y++) { $year = $this->generateRandomYear(); - $date = new DateTime("$year-3-21", new DateTimeZone($this->timezone)); - $date->add(new DateInterval('P' . (easter_days($year) + 1) . 'D')); + $date = $this->calculateEaster($year, $this->timezone); + $date->add(new DateInterval('P1D')); $data[] = [$year, $date->format('Y-m-d')]; } diff --git a/tests/Australia/GoodFridayTest.php b/tests/Australia/GoodFridayTest.php index d5133c0eb..afd4aaddb 100644 --- a/tests/Australia/GoodFridayTest.php +++ b/tests/Australia/GoodFridayTest.php @@ -53,8 +53,7 @@ public function HolidayDataProvider() for ($y = 0; $y < 50; $y++) { $year = $this->generateRandomYear(); - $date = new DateTime("$year-3-21", new DateTimeZone($this->timezone)); - $date->add(new DateInterval('P' . easter_days($year) . 'D')); + $date = $this->calculateEaster($year, $this->timezone); $date->sub(new DateInterval('P2D')); $data[] = [$year, $date->format('Y-m-d')]; } diff --git a/tests/Ireland/EasterMondayTest.php b/tests/Ireland/EasterMondayTest.php index f9852a2fd..7b5a44c1e 100644 --- a/tests/Ireland/EasterMondayTest.php +++ b/tests/Ireland/EasterMondayTest.php @@ -53,8 +53,8 @@ public function HolidayDataProvider() for ($y = 0; $y < self::TEST_ITERATIONS; $y++) { $year = $this->generateRandomYear(); - $date = new DateTime("$year-3-21", new DateTimeZone(self::TIMEZONE)); - $date->add(new DateInterval('P' . (easter_days($year) + 1) . 'D')); + $date = $this->calculateEaster($year, self::TIMEZONE); + $date->add(new DateInterval('P1D')); $data[] = [$year, $date->format('Y-m-d')]; } diff --git a/tests/Ireland/EasterTest.php b/tests/Ireland/EasterTest.php index e57c3743d..8b0c538e7 100644 --- a/tests/Ireland/EasterTest.php +++ b/tests/Ireland/EasterTest.php @@ -12,7 +12,6 @@ namespace Yasumi\tests\Ireland; -use DateInterval; use DateTime; use DateTimeZone; use Yasumi\Holiday; @@ -53,8 +52,7 @@ public function HolidayDataProvider() for ($y = 0; $y < self::TEST_ITERATIONS; $y++) { $year = $this->generateRandomYear(); - $date = new DateTime("$year-3-21", new DateTimeZone(self::TIMEZONE)); - $date->add(new DateInterval('P' . easter_days($year) . 'D')); + $date = $this->calculateEaster($year, self::TIMEZONE); $data[] = [$year, $date->format('Y-m-d')]; } diff --git a/tests/Ireland/GoodFridayTest.php b/tests/Ireland/GoodFridayTest.php index 91963caa4..f62e388ab 100644 --- a/tests/Ireland/GoodFridayTest.php +++ b/tests/Ireland/GoodFridayTest.php @@ -53,8 +53,7 @@ public function HolidayDataProvider() for ($y = 0; $y < self::TEST_ITERATIONS; $y++) { $year = $this->generateRandomYear(); - $date = new DateTime("$year-3-21", new DateTimeZone(self::TIMEZONE)); - $date->add(new DateInterval('P' . easter_days($year) . 'D')); + $date = $this->calculateEaster($year, self::TIMEZONE); $date->sub(new DateInterval('P2D')); $data[] = [$year, $date->format('Y-m-d')]; } diff --git a/tests/Ireland/PentecostTest.php b/tests/Ireland/PentecostTest.php index 98f9eff31..574deee29 100644 --- a/tests/Ireland/PentecostTest.php +++ b/tests/Ireland/PentecostTest.php @@ -54,8 +54,7 @@ public function HolidayDataProvider() //for ($y = 0; $y < self::TEST_ITERATIONS; $y++) { for ($y = 0; $y < 2; $y++) { $year = $this->generateRandomYear(); - $date = new DateTime("$year-3-21", new DateTimeZone(self::TIMEZONE)); - $date->add(new DateInterval('P' . \easter_days($year) . 'D')); + $date = $this->calculateEaster($year, self::TIMEZONE); $date->add(new DateInterval('P49D')); $data[] = [$year, $date->format('Y-m-d')]; } diff --git a/tests/Ireland/pentecostMondayTest.php b/tests/Ireland/pentecostMondayTest.php index 74076358a..8ab6723e5 100644 --- a/tests/Ireland/pentecostMondayTest.php +++ b/tests/Ireland/pentecostMondayTest.php @@ -58,8 +58,7 @@ public function HolidayDataProvider() for ($y = 0; $y < self::TEST_ITERATIONS; $y++) { $year = $this->generateRandomYear(1000, self::ABOLISHMENT_YEAR); - $date = new DateTime("$year-3-21", new DateTimeZone(self::TIMEZONE)); - $date->add(new DateInterval('P' . \easter_days($year) . 'D')); + $date = $this->calculateEaster($year, self::TIMEZONE); $date->add(new DateInterval('P50D')); $data[] = [$year, $date->format('Y-m-d')]; } diff --git a/tests/NewZealand/EasterMondayTest.php b/tests/NewZealand/EasterMondayTest.php index bff0a2017..c14393e48 100644 --- a/tests/NewZealand/EasterMondayTest.php +++ b/tests/NewZealand/EasterMondayTest.php @@ -53,8 +53,8 @@ public function HolidayDataProvider() for ($y = 0; $y < 50; $y++) { $year = $this->generateRandomYear(); - $date = new DateTime("$year-3-21", new DateTimeZone(self::TIMEZONE)); - $date->add(new DateInterval('P' . (easter_days($year) + 1) . 'D')); + $date = $this->calculateEaster($year, self::TIMEZONE); + $date->add(new DateInterval('P1D')); $data[] = [$year, $date->format('Y-m-d')]; } diff --git a/tests/NewZealand/GoodFridayTest.php b/tests/NewZealand/GoodFridayTest.php index f8c812391..443f59e63 100644 --- a/tests/NewZealand/GoodFridayTest.php +++ b/tests/NewZealand/GoodFridayTest.php @@ -53,8 +53,7 @@ public function HolidayDataProvider() for ($y = 0; $y < 50; $y++) { $year = $this->generateRandomYear(); - $date = new DateTime("$year-3-21", new DateTimeZone(self::TIMEZONE)); - $date->add(new DateInterval('P' . easter_days($year) . 'D')); + $date = $this->calculateEaster($year, self::TIMEZONE); $date->sub(new DateInterval('P2D')); $data[] = [$year, $date->format('Y-m-d')]; } diff --git a/tests/SouthAfrica/FamilyDayTest.php b/tests/SouthAfrica/FamilyDayTest.php index a9c57643e..2623a27b7 100644 --- a/tests/SouthAfrica/FamilyDayTest.php +++ b/tests/SouthAfrica/FamilyDayTest.php @@ -62,8 +62,7 @@ public function HolidayDataProvider() for ($y = 0; $y < 50; $y++) { $year = $this->generateRandomYear(self::ESTABLISHMENT_YEAR); - $date = new DateTime("$year-3-21", new DateTimeZone(self::TIMEZONE)); - $date->add(new DateInterval('P' . easter_days($year) . 'D')); + $date = $this->calculateEaster($year, self::TIMEZONE); $date->add(new DateInterval('P1D')); $data[] = [$year, $date->format('Y-m-d')]; } diff --git a/tests/SouthAfrica/GoodFridayTest.php b/tests/SouthAfrica/GoodFridayTest.php index 003e67c58..316c1ced0 100644 --- a/tests/SouthAfrica/GoodFridayTest.php +++ b/tests/SouthAfrica/GoodFridayTest.php @@ -62,8 +62,7 @@ public function HolidayDataProvider() for ($y = 0; $y < 50; $y++) { $year = $this->generateRandomYear(self::ESTABLISHMENT_YEAR); - $date = new DateTime("$year-3-21", new DateTimeZone(self::TIMEZONE)); - $date->add(new DateInterval('P' . easter_days($year) . 'D')); + $date = $this->calculateEaster($year, self::TIMEZONE); $date->sub(new DateInterval('P2D')); $data[] = [$year, $date->format('Y-m-d')]; } diff --git a/tests/UnitedKingdom/EasterMondayTest.php b/tests/UnitedKingdom/EasterMondayTest.php index 7842aa4a4..802456e6b 100644 --- a/tests/UnitedKingdom/EasterMondayTest.php +++ b/tests/UnitedKingdom/EasterMondayTest.php @@ -53,8 +53,8 @@ public function HolidayDataProvider() for ($y = 0; $y < 50; $y++) { $year = $this->generateRandomYear(); - $date = new DateTime("$year-3-21", new DateTimeZone(self::TIMEZONE)); - $date->add(new DateInterval('P' . (easter_days($year) + 1) . 'D')); + $date = $this->calculateEaster($year, self::TIMEZONE); + $date->add(new DateInterval('P1D')); $data[] = [$year, $date->format('Y-m-d')]; } diff --git a/tests/YasumiBase.php b/tests/YasumiBase.php index 7974d4802..c57b119e2 100644 --- a/tests/YasumiBase.php +++ b/tests/YasumiBase.php @@ -12,6 +12,7 @@ namespace Yasumi\tests; +use DateInterval; use DateTime; use DateTimeZone; use Faker\Factory as Faker; @@ -219,4 +220,82 @@ public function generateRandomYear($lowerLimit = 1000, $upperLimit = 9999) { return (int)Faker::create()->numberBetween($lowerLimit, $upperLimit); } + + /** + * Calculates the date for Easter. + * + * Easter is a festival and holiday celebrating the resurrection of Jesus Christ from the dead. Easter is celebrated + * on a date based on a certain number of days after March 21st. + * + * This function uses the standard PHP 'easter_days' function if the calendar extension is enabled. In case the + * calendar function is not enabled, a fallback calculation has been implemented that is based on the same + * 'easter_days' c function. + * + * Note: In calendrical calculations, frequently operations called integer division are used. + * + * @see easter_days + * + * @link https://github.com/php/php-src/blob/c8aa6f3a9a3d2c114d0c5e0c9fdd0a465dbb54a5/ext/calendar/easter.c + * @link http://www.gmarts.org/index.php?go=415#EasterMallen + * @link http://www.tondering.dk/claus/cal/easter.php + * + * @param int $year the year for which Easter needs to be calculated + * @param string $timezone the timezone in which Easter is celebrated + * + * @return \DateTime date of Easter + */ + protected function calculateEaster($year, $timezone) + { + if (extension_loaded('calendar')) { + $easter_days = \easter_days($year); + } else { + $golden = (int)(($year % 19) + 1); // The Golden Number + + // The Julian calendar applies to the original method from 326AD. The Gregorian calendar was first + // introduced in October 1582 in Italy. Easter algorithms using the Gregorian calendar apply to years + // 1583 AD to 4099 (A day adjustment is required in or shortly after 4100 AD). + // After 1752, most western churches have adopted the current algorithm. + if ($year <= 1752) { + $dom = ($year + (int)($year / 4) + 5) % 7; // The 'Dominical number' - finding a Sunday + if ($dom < 0) { + $dom += 7; + } + + $pfm = (3 - (11 * $golden) - 7) % 30; // Uncorrected date of the Paschal full moon + if ($pfm < 0) { + $pfm += 30; + } + } else { + $dom = ($year + (int)($year / 4) - (int)($year / 100) + (int)($year / 400)) % 7; // The 'Dominical number' - finding a Sunday + if ($dom < 0) { + $dom += 7; + } + + $solar = (int)(($year - 1600) / 100) - (int)(($year - 1600) / 400); // The solar correction + $lunar = (int)(((int)(($year - 1400) / 100) * 8) / 25); // The lunar correction + + $pfm = (3 - (11 * $golden) + $solar - $lunar) % 30; // Uncorrected date of the Paschal full moon + if ($pfm < 0) { + $pfm += 30; + } + } + + // Corrected date of the Paschal full moon, - days after 21st March + if (($pfm == 29) || ($pfm == 28 && $golden > 11)) { + --$pfm; + } + + $tmp = (4 - $pfm - $dom) % 7; + if ($tmp < 0) { + $tmp += 7; + } + + $easter_days = (int)($pfm + $tmp + 1); // Easter as the number of days after 21st March + } + + $easter = new DateTime("$year-3-21", new DateTimeZone($timezone)); + $easter->add(new DateInterval('P' . $easter_days . 'D')); + + return $easter; + } }