diff --git a/CHANGELOG.md b/CHANGELOG.md index 81bdb306a..dffa5eaea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/) and this - Holiday providers for states of Austria. [\#182](https://github.com/azuyalabs/yasumi/pull/182) ([aprog](https://github.com/aprog)) - Added missing return (correct) and parameter types in various methods. - Day of Liberation (Tag der Befreiung) is an one-time official holiday in 2020 in Berlin (Germany). +- Catholic Christmas Day is a new official holiday since 2017 in the Ukraine. [\#202](https://github.com/azuyalabs/yasumi/pull/202) ### Changed - Holiday names in Danish, Dutch, and Norwegian are no longer capitalized. [\#185](https://github.com/azuyalabs/yasumi/pull/185) ([c960657](https://github.com/c960657)) @@ -21,12 +22,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/) and this - Refactored various conditional structures. - Changed signature of some methods as parameters with defaults should come after required parameters. - Updated third party dependencies. +- Second International Workers Day was an official holiday only until 2018. [\#202](https://github.com/azuyalabs/yasumi/pull/202) ### Fixed - Fixed issue if the next working day happens to be in the next year (i.e. not in the year of the Yasumi instance) [\#192](https://github.com/azuyalabs/yasumi/issues/192) ([tniemann](https://github.com/tniemann)) - Fixed issue if the previous working day happens to be in the previous year (i.e. not in the year of the Yasumi instance) - Fix locale fallback for substitute holidays [\#180](https://github.com/azuyalabs/yasumi/pull/180) ([c960657](https://github.com/c960657)) - Fixed compound conditions that are always true by simplifying the condition steps. +- Fixed Ukraine holidays on weekends. These days need to be substituted. [\#202](https://github.com/azuyalabs/yasumi/pull/202) ### Removed - PHP 7.1 Support, as it has reached its end of life. diff --git a/src/Yasumi/Holiday.php b/src/Yasumi/Holiday.php index 9a185db50..55841b65a 100755 --- a/src/Yasumi/Holiday.php +++ b/src/Yasumi/Holiday.php @@ -1,4 +1,5 @@ timezone = 'Europe/Kiev'; // Add common holidays - $this->addHoliday($this->newYearsDay($this->year, $this->timezone, $this->locale)); + // New Years Day will not be substituted to an monday if it's on a weekend! + $this->addHoliday($this->newYearsDay($this->year, $this->timezone, $this->locale), false); $this->addHoliday($this->internationalWorkersDay($this->year, $this->timezone, $this->locale)); $this->addHoliday($this->internationalWomensDay($this->year, $this->timezone, $this->locale)); @@ -63,6 +66,42 @@ public function initialize(): void $this->calculateConstitutionDay(); $this->calculateIndependenceDay(); $this->calculateDefenderOfUkraineDay(); + $this->calculateCatholicChristmasDay(); + } + + /** + * Adds a holiday to the holidays providers (i.e. country/state) list of holidays. + * + * @param Holiday $holiday Holiday instance (representing a holiday) to be added to the internal list + * of holidays of this country. + * @param bool $substitutable Holidays on a weekend will be substituted to the next monday. + * + * @throws InvalidDateException + * @throws UnknownLocaleException + * @throws \InvalidArgumentException + * @throws \Exception + */ + public function addHoliday(Holiday $holiday, bool $substitutable = true): void + { + parent::addHoliday($holiday); + + if (!$substitutable) { + return; + } + + // Substitute holiday is on the next available weekday + // if a holiday falls on a Saturday or Sunday. + if ($this->isWeekendDay($holiday)) { + $date = clone $holiday; + $date->modify('next monday'); + + parent::addHoliday(new SubstituteHoliday( + $holiday, + [], + $date, + $this->locale + )); + } } /** @@ -85,6 +124,7 @@ private function calculateChristmasDay(): void /** * International Workers' Day. + * National holiday until 2018. * * @link https://en.wikipedia.org/wiki/International_Workers%27_Day#Ukraine * @@ -95,6 +135,10 @@ private function calculateChristmasDay(): void */ private function calculateSecondInternationalWorkersDay(): void { + if ($this->year >= 2018) { + return; + } + $this->addHoliday(new Holiday('secondInternationalWorkersDay', [ 'uk' => 'День міжнародної солідарності трудящих', 'ru' => 'День международной солидарности трудящихся', @@ -222,4 +266,35 @@ public function calculateEaster(int $year, string $timezone): \DateTime { return $this->calculateOrthodoxEaster($year, $timezone); } + + /** + * Catholic Christmas Day. + * (since 2017 instead of International Workers' Day 2. May) + * + * @link https://en.wikipedia.org/wiki/Christmas_in_Ukraine + * + * @throws InvalidDateException + * @throws \InvalidArgumentException + * @throws UnknownLocaleException + * @throws \Exception + */ + private function calculateCatholicChristmasDay(): void + { + if ($this->year < 2017) { + return; + } + + $this->addHoliday( + new Holiday( + 'catholicChristmasDay', + [ + 'uk' => 'Католицький день Різдва', + 'ru' => 'Католическое рождество', + ], + new \DateTime("$this->year-12-25", new \DateTimeZone($this->timezone)), + $this->locale + ), + false // Catholic Christmas Day will not be substituted to an monday if it's on a weekend! + ); + } } diff --git a/tests/Base/WeekendTest.php b/tests/Base/WeekendTest.php index 1185d6df2..ef4c3cfbc 100644 --- a/tests/Base/WeekendTest.php +++ b/tests/Base/WeekendTest.php @@ -1,5 +1,4 @@ - + */ + +namespace Yasumi\tests\Ukraine; + +use DateTime; +use Exception; +use ReflectionException; +use Yasumi\Holiday; +use Yasumi\tests\YasumiTestCaseInterface; +use Yasumi\Yasumi; + +/** + * Class CatholicChristmasDayTest + * @package Yasumi\tests\Ukraine + */ +class CatholicChristmasDayTest extends UkraineBaseTestCase implements YasumiTestCaseInterface +{ + /** + * The name of the holiday + */ + public const HOLIDAY = 'catholicChristmasDay'; + + /** + * Tests Catholic Christmas Day. + * + * @dataProvider CatholicChristmasDayDataProvider + * + * @param int $year the year for which International Workers' Day needs to be tested + * @param DateTime $expected the expected date + * + * @throws ReflectionException + */ + public function testCatholicChristmasDay($year, $expected) + { + $this->assertHoliday(self::REGION, self::HOLIDAY, $year, $expected); + } + + /** + * Tests Catholic Christmas Day before 2017. + * @throws ReflectionException + */ + public function testNoCatholicChristmasDayBefore2017() + { + $year = $this->generateRandomYear(null, 2016); + $holidays = Yasumi::create(self::REGION, $year); + $holiday = $holidays->getHoliday(self::HOLIDAY); + + $this->assertNull($holiday); + + unset($year, $holiday, $holidays); + } + + /** + * Tests translated name of the holiday defined in this test. + * @throws ReflectionException + */ + public function testTranslation(): void + { + $this->assertTranslatedHolidayName( + self::REGION, + self::HOLIDAY, + $this->generateRandomYear(2017), + [self::LOCALE => 'Католицький день Різдва'] + ); + } + + /** + * Tests type of the holiday defined in this test. + * @throws ReflectionException + */ + public function testHolidayType(): void + { + $this->assertHolidayType(self::REGION, self::HOLIDAY, $this->generateRandomYear(2017), Holiday::TYPE_OFFICIAL); + } + + /** + * Returns a list of random test dates used for assertion of Catholic Christmas Day. + * + * @return array list of test dates for Catholic Christmas Day + * @throws Exception + */ + public function CatholicChristmasDayDataProvider(): array + { + $data = []; + + for ($y = 0; $y < 10; $y++) { + $year = $this->generateRandomYear(2017); + $data[] = [$year, new \DateTime("$year-12-25", new \DateTimeZone(self::TIMEZONE))]; + } + + return $data; + } +} diff --git a/tests/Ukraine/SecondInternationalWorkersDayTest.php b/tests/Ukraine/SecondInternationalWorkersDayTest.php index 10c6fb859..9fff35ba3 100644 --- a/tests/Ukraine/SecondInternationalWorkersDayTest.php +++ b/tests/Ukraine/SecondInternationalWorkersDayTest.php @@ -1,4 +1,5 @@ assertHoliday(self::REGION, self::HOLIDAY, $year, $expected); } + /** + * Tests International Workers' Day since 2018. + * @throws ReflectionException + */ + public function testNoSecondInternationalWorkersDaySince2018() + { + $year = $this->generateRandomYear(2018); + $holidays = Yasumi::create(self::REGION, $year); + $holiday = $holidays->getHoliday(self::HOLIDAY); + + $this->assertNull($holiday); + + unset($year, $holiday, $holidays); + } + /** * Tests translated name of the holiday defined in this test. * @throws ReflectionException @@ -53,7 +69,7 @@ public function testTranslation(): void $this->assertTranslatedHolidayName( self::REGION, self::HOLIDAY, - $this->generateRandomYear(), + $this->generateRandomYear(null, 2017), [self::LOCALE => 'День міжнародної солідарності трудящих'] ); } @@ -64,7 +80,12 @@ public function testTranslation(): void */ public function testHolidayType(): void { - $this->assertHolidayType(self::REGION, self::HOLIDAY, $this->generateRandomYear(), Holiday::TYPE_OFFICIAL); + $this->assertHolidayType( + self::REGION, + self::HOLIDAY, + $this->generateRandomYear(null, 2017), + Holiday::TYPE_OFFICIAL + ); } /** @@ -75,6 +96,13 @@ public function testHolidayType(): void */ public function SecondInternationalWorkersDayDataProvider(): array { - return $this->generateRandomDates(5, 2, self::TIMEZONE); + $data = []; + + for ($y = 0; $y < 10; $y++) { + $year = $this->generateRandomYear(null, 2017); + $data[] = [$year, new \DateTime("$year-05-02", new \DateTimeZone(self::TIMEZONE))]; + } + + return $data; } } diff --git a/tests/Ukraine/SubstitutedHolidayTest.php b/tests/Ukraine/SubstitutedHolidayTest.php new file mode 100644 index 000000000..b8e8db952 --- /dev/null +++ b/tests/Ukraine/SubstitutedHolidayTest.php @@ -0,0 +1,184 @@ + + */ + +namespace Yasumi\tests\Ukraine; + +use DateTime; +use DateTimeZone; +use Exception; +use ReflectionException; +use Yasumi\Holiday; +use Yasumi\SubstituteHoliday; +use Yasumi\tests\YasumiTestCaseInterface; +use Yasumi\Yasumi; + +/** + * Class SubstitutedHolidayTest + * @package Yasumi\tests\Ukraine + */ +class SubstitutedHolidayTest extends UkraineBaseTestCase implements YasumiTestCaseInterface +{ + /** + * Tests the substitution of holidays on saturday (weekend). + * @throws Exception + * @throws ReflectionException + */ + public function testSaturdaySubstitution() + { + // 2020-05-09 victoryDay (День перемоги) + $year = 2020; + $holiday = 'victoryDay'; + + $this->assertHolidayWithSubstitution( + self::REGION, + $holiday, + $year, + new DateTime("$year-05-09", new DateTimeZone(self::TIMEZONE)), + new DateTime("$year-05-11", new DateTimeZone(self::TIMEZONE)) + ); + + unset($year, $holiday); + } + + /** + * Tests the substitution of holidays on sunday (weekend). + * @throws Exception + * @throws ReflectionException + */ + public function testSundaySubstitution(): void + { + // 2020-06-28 constitutionDay (День Конституції) + $year = 2020; + $holiday = 'constitutionDay'; + + $this->assertHolidayWithSubstitution( + self::REGION, + $holiday, + $year, + new DateTime("$year-06-28", new DateTimeZone(self::TIMEZONE)), + new DateTime("$year-06-29", new DateTimeZone(self::TIMEZONE)) + ); + + unset($year, $holiday); + } + + /** + * Tests the substitution of new year (1. January) on a weekend. + * Special: no substitution at new year (1. January) on a weekend. + * @throws Exception + * @throws ReflectionException + */ + public function testNewYearNoSubstitution(): void + { + // 2022-01-01 (Saturday) constitutionDay (Новий Рік) + $year = 2022; + $holiday = 'newYearsDay'; + + $this->assertHolidayWithSubstitution( + self::REGION, + $holiday, + $year, + new DateTime("$year-01-01", new DateTimeZone(self::TIMEZONE)) + ); + + unset($year, $holiday); + } + + /** + * Tests the substitution of Catholic Christmas Day (25. December) on a weekend. + * Special: no substitution at Catholic Christmas Day (25. December) on a weekend. + * @throws Exception + * @throws ReflectionException + */ + public function testCatholicChristmasDayNoSubstitution(): void + { + // 2022-12-25 (Sunday) catholicChristmasDay (Католицький день Різдва) + $year = 2022; + $holiday = 'catholicChristmasDay'; + + $this->assertHolidayWithSubstitution( + self::REGION, + $holiday, + $year, + new DateTime("$year-12-25", new DateTimeZone(self::TIMEZONE)) + ); + + unset($year, $holiday); + } + + /** + * Dummy: Tests the translated name of the holiday defined in this test. + * @throws ReflectionException + */ + public function testTranslation(): void + { + $this->assertTrue(true); + } + + /** + * Dummy: Tests type of the holiday defined in this test. + * @throws ReflectionException + */ + public function testHolidayType(): void + { + $this->assertTrue(true); + } + + /** + * Asserts that the expected date is indeed a holiday for that given year and name + * + * @param string $provider the holiday provider (i.e. country/state) for which the holiday need to be tested + * @param string $shortName string the short name of the holiday to be checked against + * @param int $year holiday calendar year + * @param DateTime $expected the official date to be checked against + * @param DateTime $expected the substituted date to be checked against + * + * @throws UnknownLocaleException + * @throws InvalidDateException + * @throws InvalidArgumentException + * @throws RuntimeException + * @throws AssertionFailedError + * @throws ReflectionException + */ + public function assertHolidayWithSubstitution( + string $provider, + string $shortName, + int $year, + DateTime $expectedOfficial, + DateTime $expectedSubstitution = null + ): void { + $holidays = Yasumi::create($provider, $year); + + $holidayOfficial = $holidays->getHoliday($shortName); + $this->assertInstanceOf(Holiday::class, $holidayOfficial); + $this->assertNotNull($holidayOfficial); + $this->assertEquals($expectedOfficial, $holidayOfficial); + $this->assertTrue($holidays->isHoliday($holidayOfficial)); + $this->assertEquals(Holiday::TYPE_OFFICIAL, $holidayOfficial->getType()); + + $holidaySubstitution = $holidays->getHoliday('substituteHoliday:' . $holidayOfficial->shortName); + if ($expectedSubstitution === null) { + // without substitution + $this->assertNull($holidaySubstitution); + } else { + // with substitution + $this->assertNotNull($holidaySubstitution); + $this->assertInstanceOf(SubstituteHoliday::class, $holidaySubstitution); + $this->assertEquals($expectedSubstitution, $holidaySubstitution); + $this->assertTrue($holidays->isHoliday($holidaySubstitution)); + $this->assertEquals(Holiday::TYPE_OFFICIAL, $holidaySubstitution->getType()); + } + + unset($holidayOfficial, $holidaySubstitution, $holidays); + } +} diff --git a/tests/Ukraine/UkraineTest.php b/tests/Ukraine/UkraineTest.php index 6ceb4a95f..6c82c390e 100644 --- a/tests/Ukraine/UkraineTest.php +++ b/tests/Ukraine/UkraineTest.php @@ -1,4 +1,5 @@ assertDefinedHolidays([ + $holidays = [ 'newYearsDay', 'internationalWorkersDay', - 'secondInternationalWorkersDay', 'christmasDay', 'easter', 'pentecost', 'internationalWomensDay', 'victoryDay', - 'constitutionDay', - 'independenceDay', - 'defenderOfUkraineDay', - ], self::REGION, $this->year, Holiday::TYPE_OFFICIAL); + ]; + + if ($this->year >= 1996) { + $holidays[] = 'constitutionDay'; + } + + if ($this->year >= 1991) { + $holidays[] = 'independenceDay'; + } + + if ($this->year >= 2015) { + $holidays[] = 'defenderOfUkraineDay'; + } + + if ($this->year < 2018) { + $holidays[] = 'secondInternationalWorkersDay'; + } + + if ($this->year >= 2017) { + $holidays[] = 'catholicChristmasDay'; + } + + $this->assertDefinedHolidays( + $holidays, + self::REGION, + $this->year, + Holiday::TYPE_OFFICIAL + ); } /** @@ -88,6 +112,6 @@ public function testOtherHolidays(): void */ protected function setUp(): void { - $this->year = $this->generateRandomYear(2015, 2025); + $this->year = $this->generateRandomYear(); } }