diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 6b1ffb198..000f65b29 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -199,5 +199,9 @@ ./tests/USA + + + ./tests/USA/NYSE + diff --git a/src/Yasumi/Provider/Brazil.php b/src/Yasumi/Provider/Brazil.php index 92b835a65..a4e38ec03 100644 --- a/src/Yasumi/Provider/Brazil.php +++ b/src/Yasumi/Provider/Brazil.php @@ -89,6 +89,7 @@ protected function calculateProclamationOfRepublicDay(): void 'proclamationOfRepublicDay', ['pt' => 'Dia da Proclamação da República'], new \DateTime("{$this->year}-11-15", DateTimeZoneFactory::getDateTimeZone($this->timezone)), + $this->locale )); } } diff --git a/src/Yasumi/Provider/USA/NYSE.php b/src/Yasumi/Provider/USA/NYSE.php new file mode 100644 index 000000000..e4808d4bb --- /dev/null +++ b/src/Yasumi/Provider/USA/NYSE.php @@ -0,0 +1,107 @@ + + * @author Art Kurbakov + */ + +// This file in addition to regular holidays observed by +// the New York Stock Exchange (NYSE), +// includes full day special closure events due to: +// weather, national emergencies and presidential proclamations. +// All closure events are included from year 2000. + +namespace Yasumi\Provider\USA; + +use Yasumi\Holiday; +use Yasumi\Provider\USA; + +class NYSE extends USA +{ + /** + * Initialize holidays for the NYSE. + * + * @throws \Exception + */ + public function initialize(): void + { + $this->timezone = 'America/New_York'; + + // Add exchange-specific holidays + $this->addHoliday($this->newYearsDay($this->year, $this->timezone, $this->locale)); + $this->calculateMartinLutherKingday(); + $this->calculateWashingtonsBirthday(); + $this->addHoliday($this->goodFriday($this->year, $this->timezone, $this->locale)); + $this->calculateMemorialDay(); + if (2021 < $this->year) { + $this->calculateJuneteenth(); + } + $this->calculateIndependenceDay(); + $this->calculateLabourDay(); + $this->calculateThanksgivingDay(); + $this->addHoliday($this->christmasDay($this->year, $this->timezone, $this->locale)); + + $this->calculateSubstituteHolidays(); + + // Add other full-day closure events + $this->addWeatherEvents(); + $this->addEmergencies(); + $this->addProclamations(); + } + + public function getSources(): array + { + return [ + 'https://www.nyse.com/trader-update/history#11507', + 'https://s3.amazonaws.com/armstrongeconomics-wp/2013/07/NYSE-Closings.pdf', + 'https://ir.theice.com/press/news-details/2018/New-York-Stock-Exchange-to-Honor-President-George-H-W-Bush/default.aspx', + 'https://ir.theice.com/press/news-details/2024/The-New-York-Stock-Exchange-Will-Close-Markets-on-January-9-to-Honor-the-Passing-of-Former-President-Jimmy-Carter-on-National-Day-of-Mourning/default.aspx', + 'https://www.thecorporatecounsel.net/blog/2021/10/nyse-makes-juneteenth-a-new-market-holiday.html', + ]; + } + + private function addWeatherEvents() + { + if (2012 == $this->year) { + $this->addHoliday(new Holiday('hurricaneSandy1', [], new \DateTime('2012-10-29', new \DateTimeZone($this->timezone)))); + $this->addHoliday(new Holiday('hurricaneSandy2', [], new \DateTime('2012-10-30', new \DateTimeZone($this->timezone)))); + } + } + + private function addEmergencies() + { + if (2001 == $this->year) { + $this->addHoliday(new Holiday('groundZero1', [], new \DateTime('2001-09-11', new \DateTimeZone($this->timezone)))); + $this->addHoliday(new Holiday('groundZero2', [], new \DateTime('2001-09-12', new \DateTimeZone($this->timezone)))); + $this->addHoliday(new Holiday('groundZero3', [], new \DateTime('2001-09-13', new \DateTimeZone($this->timezone)))); + $this->addHoliday(new Holiday('groundZero4', [], new \DateTime('2001-09-14', new \DateTimeZone($this->timezone)))); + } + } + + private function addProclamations() + { + if (2004 == $this->year) { + $this->addHoliday(new Holiday('ReaganMourning', [], new \DateTime('2004-06-11', new \DateTimeZone($this->timezone)))); + } + if (2007 == $this->year) { + $this->addHoliday(new Holiday('GRFordMourning', [], new \DateTime('2007-01-02', new \DateTimeZone($this->timezone)))); + } + if (2018 == $this->year) { + $this->addHoliday(new Holiday('HWBushMourning', [], new \DateTime('2018-12-05', new \DateTimeZone($this->timezone)))); + } + if (2025 == $this->year) { + $this->addHoliday(new Holiday('CarterMourning', [], new \DateTime('2025-01-09', new \DateTimeZone($this->timezone)))); + } + } +} diff --git a/tests/USA/NYSE/CarterMourningTest.php b/tests/USA/NYSE/CarterMourningTest.php new file mode 100644 index 000000000..e59cb0c4c --- /dev/null +++ b/tests/USA/NYSE/CarterMourningTest.php @@ -0,0 +1,80 @@ + + * @author Art Kurbakov + */ + +namespace Yasumi\tests\USA\NYSE; + +use Yasumi\tests\USA\USABaseTestCase; + +/** + * Class to test day of closure of NYSE due to Jimmy Carter mourning proclamation + */ +class CarterMourningTest extends USABaseTestCase +{ + /** + * Name of provider to be tested. + */ + public const REGION = 'USA/NYSE'; + + /** + * The year when the closure was observed. + */ + public const OBSERVED_YEAR = 2025; + + /** + * Tests day of closure on January 9th 2025 + * + * @throws \Exception + */ + public function testCarterMourning(): void + { + $this->assertHoliday( + self::REGION, + 'CarterMourning', + self::OBSERVED_YEAR, + new \DateTime('2025-01-09', new \DateTimeZone(self::TIMEZONE)) + ); + } + + /** + * Tests years before 2025 + * + * @throws \Exception + */ + public function testCarterMourningBefore2025(): void + { + $this->assertNotHoliday( + self::REGION, + 'CarterMourning', + self::OBSERVED_YEAR - 1 + ); + } + + /** + * Tests years after 2025 + * + * @throws \Exception + */ + public function testCarterMourningAfter2025(): void + { + $this->assertNotHoliday( + self::REGION, + 'CarterMourning', + self::OBSERVED_YEAR + 1 + ); + } +} diff --git a/tests/USA/NYSE/GRFordMourningTest.php b/tests/USA/NYSE/GRFordMourningTest.php new file mode 100644 index 000000000..b1a59afc0 --- /dev/null +++ b/tests/USA/NYSE/GRFordMourningTest.php @@ -0,0 +1,80 @@ + + * @author Art Kurbakov + */ + +namespace Yasumi\tests\USA\NYSE; + +use Yasumi\tests\USA\USABaseTestCase; + +/** + * Class to test day of closure of NYSE due to G.R. Ford Mourning proclamation + */ +class GRFordMourningTest extends USABaseTestCase +{ + /** + * Name of provider to be tested. + */ + public const REGION = 'USA/NYSE'; + + /** + * The year when the closure was observed. + */ + public const OBSERVED_YEAR = 2007; + + /** + * Tests day of closure on January 2nd 2007 + * + * @throws \Exception + */ + public function testGRFordMourning(): void + { + $this->assertHoliday( + self::REGION, + 'GRFordMourning', + self::OBSERVED_YEAR, + new \DateTime('2007-01-02', new \DateTimeZone(self::TIMEZONE)) + ); + } + + /** + * Tests years before 2007 + * + * @throws \Exception + */ + public function testGRFordMourningBefore2007(): void + { + $this->assertNotHoliday( + self::REGION, + 'GRFordMourning', + self::OBSERVED_YEAR - 1 + ); + } + + /** + * Tests years after 2007 + * + * @throws \Exception + */ + public function testGRFordMourningAfter2007(): void + { + $this->assertNotHoliday( + self::REGION, + 'GRFordMourning', + self::OBSERVED_YEAR + 1 + ); + } +} diff --git a/tests/USA/NYSE/GroundZeroTest.php b/tests/USA/NYSE/GroundZeroTest.php new file mode 100644 index 000000000..3cc328850 --- /dev/null +++ b/tests/USA/NYSE/GroundZeroTest.php @@ -0,0 +1,137 @@ + + * @author Art Kurbakov + */ + +namespace Yasumi\tests\USA\NYSE; + +use Yasumi\tests\USA\USABaseTestCase; + +/** + * Class to test 4 days of closure of NYSE due to September 11th attacks + */ +class GroundZeroTest extends USABaseTestCase +{ + /** + * Name of provider to be tested. + */ + public const REGION = 'USA/NYSE'; + + /** + * The year when the closure was observed. + */ + public const OBSERVED_YEAR = 2001; + + /** + * Tests 4 days of closure on September 11th 2001 + * + * @throws \Exception + */ + public function testGroundZero(): void + { + $this->assertHoliday( + self::REGION, + 'groundZero1', + self::OBSERVED_YEAR, + new \DateTime('2001-09-11', new \DateTimeZone(self::TIMEZONE)) + ); + + $this->assertHoliday( + self::REGION, + 'groundZero2', + self::OBSERVED_YEAR, + new \DateTime('2001-09-12', new \DateTimeZone(self::TIMEZONE)) + ); + + $this->assertHoliday( + self::REGION, + 'groundZero3', + self::OBSERVED_YEAR, + new \DateTime('2001-09-13', new \DateTimeZone(self::TIMEZONE)) + ); + + $this->assertHoliday( + self::REGION, + 'groundZero4', + self::OBSERVED_YEAR, + new \DateTime('2001-09-14', new \DateTimeZone(self::TIMEZONE)) + ); + } + + /** + * Tests years before 2001 + * + * @throws \Exception + */ + public function testGroundZeroBefore2001(): void + { + $this->assertNotHoliday( + self::REGION, + 'groundZero1', + self::OBSERVED_YEAR - 1 + ); + + $this->assertNotHoliday( + self::REGION, + 'groundZero2', + self::OBSERVED_YEAR - 1 + ); + + $this->assertNotHoliday( + self::REGION, + 'groundZero3', + self::OBSERVED_YEAR - 1 + ); + + $this->assertNotHoliday( + self::REGION, + 'groundZero4', + self::OBSERVED_YEAR - 1 + ); + } + + /** + * Tests years after 2001 + * + * @throws \Exception + */ + public function testGroundZeroAfter2001(): void + { + $this->assertNotHoliday( + self::REGION, + 'groundZero1', + self::OBSERVED_YEAR + 1 + ); + + $this->assertNotHoliday( + self::REGION, + 'groundZero2', + self::OBSERVED_YEAR + 1 + ); + + $this->assertNotHoliday( + self::REGION, + 'groundZero3', + self::OBSERVED_YEAR + 1 + ); + + $this->assertNotHoliday( + self::REGION, + 'groundZero4', + self::OBSERVED_YEAR + 1 + ); + } +} diff --git a/tests/USA/NYSE/HWBushMourningTest.php b/tests/USA/NYSE/HWBushMourningTest.php new file mode 100644 index 000000000..dba469c05 --- /dev/null +++ b/tests/USA/NYSE/HWBushMourningTest.php @@ -0,0 +1,80 @@ + + * @author Art Kurbakov + */ + +namespace Yasumi\tests\USA\NYSE; + +use Yasumi\tests\USA\USABaseTestCase; + +/** + * Class to test day of closure of NYSE due to H.W. Bush Mourning proclamation + */ +class HWBushMourningTest extends USABaseTestCase +{ + /** + * Name of provider to be tested. + */ + public const REGION = 'USA/NYSE'; + + /** + * The year when the closure was observed. + */ + public const OBSERVED_YEAR = 2018; + + /** + * Tests day of closure on December 5th 2018 + * + * @throws \Exception + */ + public function testHWBushMourning(): void + { + $this->assertHoliday( + self::REGION, + 'HWBushMourning', + self::OBSERVED_YEAR, + new \DateTime('2018-12-05', new \DateTimeZone(self::TIMEZONE)) + ); + } + + /** + * Tests years before 2018 + * + * @throws \Exception + */ + public function testHWBushMourningBefore2018(): void + { + $this->assertNotHoliday( + self::REGION, + 'HWBushMourning', + self::OBSERVED_YEAR - 1 + ); + } + + /** + * Tests years after 2018 + * + * @throws \Exception + */ + public function testHWBushMourningAfter2018(): void + { + $this->assertNotHoliday( + self::REGION, + 'HWBushMourning', + self::OBSERVED_YEAR + 1 + ); + } +} diff --git a/tests/USA/NYSE/HurricaneSandyTest.php b/tests/USA/NYSE/HurricaneSandyTest.php new file mode 100644 index 000000000..3365b7eb7 --- /dev/null +++ b/tests/USA/NYSE/HurricaneSandyTest.php @@ -0,0 +1,99 @@ + + * @author Art Kurbakov + */ + +namespace Yasumi\tests\USA\NYSE; + +use Yasumi\tests\USA\USABaseTestCase; + +/** + * Class to test 2 days of closure of NYSE due to Hurricane Sandy + */ +class HurricaneSandyTest extends USABaseTestCase +{ + /** + * Name of provider to be tested. + */ + public const REGION = 'USA/NYSE'; + + /** + * The year when the closure was observed. + */ + public const OBSERVED_YEAR = 2012; + + /** + * Tests both days of closure of October 29-30th, 2012 + * + * @throws \Exception + */ + public function testHurricaneSandyDays(): void + { + $this->assertHoliday( + self::REGION, + 'hurricaneSandy1', + self::OBSERVED_YEAR, + new \DateTime('2012-10-29', new \DateTimeZone(self::TIMEZONE)) + ); + + $this->assertHoliday( + self::REGION, + 'hurricaneSandy2', + self::OBSERVED_YEAR, + new \DateTime('2012-10-30', new \DateTimeZone(self::TIMEZONE)) + ); + } + + /** + * Tests years before 2012 + * + * @throws \Exception + */ + public function testHurricaneSandyBefore2012(): void + { + $this->assertNotHoliday( + self::REGION, + 'hurricaneSandy1', + self::OBSERVED_YEAR - 1 + ); + + $this->assertNotHoliday( + self::REGION, + 'hurricaneSandy2', + self::OBSERVED_YEAR - 1 + ); + } + + /** + * Tests years after 2012 + * + * @throws \Exception + */ + public function testHurricaneSandyAfter2012(): void + { + $this->assertNotHoliday( + self::REGION, + 'hurricaneSandy1', + self::OBSERVED_YEAR + 1 + ); + + $this->assertNotHoliday( + self::REGION, + 'hurricaneSandy2', + self::OBSERVED_YEAR + 1 + ); + } +} diff --git a/tests/USA/NYSE/JuneteenthTest.php b/tests/USA/NYSE/JuneteenthTest.php new file mode 100644 index 000000000..54d14bf30 --- /dev/null +++ b/tests/USA/NYSE/JuneteenthTest.php @@ -0,0 +1,105 @@ + + * @author Art Kurbakov + */ + +namespace Yasumi\tests\USA\NYSE; + +use Yasumi\Holiday; +use Yasumi\tests\USA\USABaseTestCase; + +/** + * Class to test Juneteenth. + */ +class JuneteenthTest extends USABaseTestCase +{ + /** + * Country (name) to be tested. + */ + public const REGION = 'USA/NYSE'; + + /** + * The name of the holiday. + */ + public const HOLIDAY = 'juneteenth'; + + /** + * The year in which the holiday was first established for NYSE. + */ + public const ESTABLISHMENT_YEAR = 2022; + + /** + * Tests Juneteenth on or after 2022. For NYSE Juneteenth is celebrated since 2022 on June 19th. + * + * @throws \Exception + */ + public function testJuneteenthOnAfter2022(): void + { + $year = 2023; + $this->assertHoliday( + self::REGION, + self::HOLIDAY, + $year, + new \DateTime("{$year}-6-19", new \DateTimeZone(self::TIMEZONE)) + ); + } + + /** + * Tests Juneteenth on or after 2022 when substituted on Monday (when Juneteenth falls on Sunday). + * + * @throws \Exception + */ + public function testJuneteenthOnAfter2022SubstitutedMonday(): void + { + $year = 2022; + $this->assertSubstituteHoliday( + self::REGION, + self::HOLIDAY, + $year, + new \DateTime("{$year}-6-20", new \DateTimeZone(self::TIMEZONE)) + ); + } + + /** + * Tests Juneteenth on or after 2022 when substituted on Friday (when Juneteenth falls on Saturday). + * + * @throws \Exception + */ + public function testJuneteenthOnAfter2022SubstitutedFriday(): void + { + $year = 2027; + $this->assertSubstituteHoliday( + self::REGION, + self::HOLIDAY, + $year, + new \DateTime("{$year}-6-18", new \DateTimeZone(self::TIMEZONE)) + ); + } + + /** + * Tests Juneteenth before 2022. For NYSE Juneteenth is celebrated since 2022 on June 19th. + * + * @throws \Exception + */ + public function testJuneteenthBefore2022(): void + { + $this->assertNotHoliday( + self::REGION, + self::HOLIDAY, + self::ESTABLISHMENT_YEAR - 1 + ); + } +} diff --git a/tests/USA/NYSE/NYSETest.php b/tests/USA/NYSE/NYSETest.php new file mode 100644 index 000000000..062520dbf --- /dev/null +++ b/tests/USA/NYSE/NYSETest.php @@ -0,0 +1,111 @@ + + * @author Art Kurbakov + */ + +namespace Yasumi\tests\USA\NYSE; + +use Yasumi\Yasumi; +use Yasumi\Holiday; +use Yasumi\tests\ProviderTestCase; +use Yasumi\tests\USA\USABaseTestCase; + +/** + * Class for testing closure days for NYSE + */ +class NYSETest extends USABaseTestCase implements ProviderTestCase +{ + /** + * Country (name) to be tested. + */ + public const REGION = 'USA/NYSE'; + + /** + * @var int year random year number used for all tests in this Test Case + */ + protected int $year; + + /** + * Initial setup of this Test Case. + * + * @throws \Exception + */ + protected function setUp(): void + { + $this->year = $this->generateRandomYear(2000, 2100); + } + + /** + * Tests if all official holidays in the USA are defined by the provider class. + */ + public function testOfficialHolidays(): void + { + $holidays = [ + 'newYearsDay', + 'martinLutherKingDay', + 'washingtonsBirthday', + 'goodFriday', + 'memorialDay', + 'independenceDay', + 'labourDay', + 'thanksgivingDay', + 'christmasDay', + ]; + + if (2001 == $this->year) { + $holidays[] = 'groundZero1'; + $holidays[] = 'groundZero2'; + $holidays[] = 'groundZero3'; + $holidays[] = 'groundZero4'; + } + + if (2004 == $this->year) { + $holidays[] = 'ReaganMourning'; + } + + if (2007 == $this->year) { + $holidays[] = 'GRFordMourning'; + } + + if (2012 == $this->year) { + $holidays[] = 'hurricaneSandy1'; + $holidays[] = 'hurricaneSandy2'; + } + + if (2018 == $this->year) { + $holidays[] = 'HWBushMourning'; + } + + if (2021 > $this->year) { + $holidays[] = 'juneteenth'; + } + + if (2025 > $this->year) { + $holidays[] = 'CarterMourning'; + } + + $this->assertDefinedHolidays($holidays, self::REGION, $this->year, Holiday::TYPE_OFFICIAL); + } + + /** + * @throws \ReflectionException + * @throws \Exception + */ + public function testSources(): void + { + $this->assertSources(self::REGION, 5); + } +} diff --git a/tests/USA/NYSE/ReaganMourningTest.php b/tests/USA/NYSE/ReaganMourningTest.php new file mode 100644 index 000000000..0abde2a74 --- /dev/null +++ b/tests/USA/NYSE/ReaganMourningTest.php @@ -0,0 +1,80 @@ + + * @author Art Kurbakov + */ + +namespace Yasumi\tests\USA\NYSE; + +use Yasumi\tests\USA\USABaseTestCase; + +/** + * Class to test day of closure of NYSE due to Reagan Mourning proclamation + */ +class ReaganMourningTest extends USABaseTestCase +{ + /** + * Name of provider to be tested. + */ + public const REGION = 'USA/NYSE'; + + /** + * The year when the closure was observed. + */ + public const OBSERVED_YEAR = 2004; + + /** + * Tests day of closure on June 11th 2004 + * + * @throws \Exception + */ + public function testReaganMourning(): void + { + $this->assertHoliday( + self::REGION, + 'ReaganMourning', + self::OBSERVED_YEAR, + new \DateTime('2004-06-11', new \DateTimeZone(self::TIMEZONE)) + ); + } + + /** + * Tests years before 2004 + * + * @throws \Exception + */ + public function testReaganMourningBefore2004(): void + { + $this->assertNotHoliday( + self::REGION, + 'ReaganMourning', + self::OBSERVED_YEAR - 1 + ); + } + + /** + * Tests years after 2004 + * + * @throws \Exception + */ + public function testReaganMourningAfter2004(): void + { + $this->assertNotHoliday( + self::REGION, + 'ReaganMourning', + self::OBSERVED_YEAR + 1 + ); + } +}