Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updated SouthKorea provider to apply the new substitute holidays changed in June 2021. #255

Merged
merged 13 commits into from
Sep 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/) and this
Canada [\#257](https://github.com/azuyalabs/yasumi/pull/257) ([Owen V. Gray](https://github.com/adrx))
- New Juneteenth National Independence Day to
USA [\#253](https://github.com/azuyalabs/yasumi/pull/253) ([Mark Heintz](https://github.com/mheintz))
- The Korea Tourism Organization's holiday guide link was added to the source of SouthKorea Provider. [\#255](https://github.com/azuyalabs/yasumi/pull/253) ([barami](https://github.com/barami))
- All providers now include a method that returns a list of external sources (i.e. references to websites, books,
scientific papers, etc.) that are used for determining the calculation logic of the providers' holidays.

### Changed

- Provider tests must implement the `ProviderTestCase` interface to ensure all required test methods are defined.
- `YasumiTestCaseInterface` was renamed to `HolidayTestCase` to better match the newly added `ProviderTestCase`
interface.
- `YasumiTestCaseInterface` was renamed to `HolidayTestCase` to better match the newly added `ProviderTestCase` interface.
- Revised rules to calculate substitution holidays of SouthKorea to apply the newly enacted law on June 2021. [\#255](https://github.com/azuyalabs/yasumi/pull/253) ([barami](https://github.com/barami))
- Seperate `calculateSubstituteHolidays` method of SouthKorea Provider to `calculateSubstituteHolidays` and `calculateOldSubstituteHolidays`. [\#255](https://github.com/azuyalabs/yasumi/pull/253) ([barami](https://github.com/barami))
- Refactored the tests of SouthKorea provider to testing substitution holidays. [\#255](https://github.com/azuyalabs/yasumi/pull/253) ([barami](https://github.com/barami))

### Fixed

Expand Down
171 changes: 128 additions & 43 deletions src/Yasumi/Provider/SouthKorea.php
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ public function getSources(): array
return [
'https://en.wikipedia.org/wiki/Public_holidays_in_South_Korea',
'https://ko.wikipedia.org/wiki/%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD%EC%9D%98_%EA%B3%B5%ED%9C%B4%EC%9D%BC',
'https://english.visitkorea.or.kr/enu/TRV/TV_ENG_1_1.jsp',
];
}

Expand Down Expand Up @@ -468,68 +469,152 @@ private function calculateHangulDay(): void
}

/**
* Substitute Holidays.
* Substitute Holidays up to 2021.
* Related statutes: Article 3 Alternative Statutory Holidays of the Regulations on Holidays of Government Offices.
*
* Since 2014, it has been applied only on Seollal, Chuseok and Children's Day.
* Due to the lunar calendar, public holidays can overlap even if it's not a Sunday.
* When public holidays fall on each other, the first non-public holiday after the holiday becomes a public holiday.
* As an exception, Children's Day also applies on Saturday.
*
* Since new legislation about public holiday was enacted in June 2021,
* this function is used to calculate the holidays up to 2021.
*
* @throws \Exception
*/
private function calculateOldSubstituteHolidays(): void
{
if ($this->year < 2014) {
return;
}

// Add substitute holidays by fixed entries.
switch ($this->year) {
case 2014:
$this->addSubstituteHoliday($this->getHoliday('dayBeforeChuseok'), "$this->year-9-10");
break;
case 2015:
$this->addSubstituteHoliday($this->getHoliday('chuseok'), "$this->year-9-29");
break;
case 2016:
$this->addSubstituteHoliday($this->getHoliday('dayBeforeSeollal'), "$this->year-2-10");
break;
case 2017:
$this->addSubstituteHoliday($this->getHoliday('dayAfterSeollal'), "$this->year-1-30");
$this->addSubstituteHoliday($this->getHoliday('dayBeforeChuseok'), "$this->year-10-6");
break;
case 2018:
$this->addSubstituteHoliday($this->getHoliday('childrensDay'), "$this->year-5-7");
$this->addSubstituteHoliday($this->getHoliday('dayBeforeChuseok'), "$this->year-9-26");
break;
case 2019:
$this->addSubstituteHoliday($this->getHoliday('childrensDay'), "$this->year-5-6");
break;
case 2020:
$this->addSubstituteHoliday($this->getHoliday('dayAfterSeollal'), "$this->year-1-27");
break;
case 2021:
$this->addSubstituteHoliday($this->getHoliday('liberationDay'), "$this->year-8-16");
$this->addSubstituteHoliday($this->getHoliday('nationalFoundationDay'), "$this->year-10-4");
$this->addSubstituteHoliday($this->getHoliday('hangulDay'), "$this->year-10-11");
break;
}
}

/**
* Substitute Holidays.
*
* Since 2022, it has been applied for all public holidays.
* When public holidays overlap on each other or weekend,
* the first working day after the holiday becomes a substitute holiday.
*
* @throws \Exception
*/
private function calculateSubstituteHolidays(): void
{
if ($this->year <= 2013) {
if ($this->year < 2022) {
$this->calculateOldSubstituteHolidays();

return;
}

// Initialize holidays variable
$holidays = $this->getHolidays();
$acceptedHolidays = [
// Holiday list to allowed to substitute.
$accptedHolidays = [];

// When deciding on alternative holidays, place lunar holidays first for consistent rules.
// These holidays will substitute for the sunday only.
$accptedHolidays += array_fill_keys([
'dayBeforeSeollal', 'seollal', 'dayAfterSeollal',
'dayBeforeChuseok', 'chuseok', 'dayAfterChuseok',
'childrensDay',
];

// Loop through all holidays
foreach ($holidays as $key => $holiday) {
// Get list of holiday dates except this
$holidayDates = array_map(static function ($holiday) use ($key) {
return $holiday->getKey() === $key ? false : $holiday;
}, $holidays);

// Only process accepted holidays and conditions
if (\in_array($key, $acceptedHolidays, true)
&& (
0 === (int) $holiday->format('w')
|| \in_array($holiday, $holidayDates, false)
|| (6 === (int) $holiday->format('w') && 'childrensDay' === $key)
)
) {
$date = clone $holiday;

// Find next week day (not being another holiday)
while (0 === (int) $date->format('w')
|| (6 === (int) $date->format('w') && 'childrensDay' === $key)
|| \in_array($date, $holidayDates, false)) {
$date->add(new DateInterval('P1D'));
}

// Add a new holiday that is substituting the original holiday
$substitute = new SubstituteHoliday(
$holiday,
[],
$date,
$this->locale
);
], [0]);

// These holidays will substitute for any weekend days (Sunday and Saturday).
$accptedHolidays += array_fill_keys([
'childrensDay', 'independenceMovementDay', 'liberationDay',
'nationalFoundationDay', 'hangulDay',
], [0, 6]);

// Step 1. Build a temporary table that aggregates holidays by date.
$dates = [];
foreach ($this->getHolidayDates() as $name => $day) {
$holiday = $this->getHoliday($name);
$dates[$day][] = $name;

if (!isset($accptedHolidays[$name])) {
continue;
}

// Add a new holiday that is substituting the original holiday
$this->addHoliday($substitute);
$dayOfWeek = (int) $holiday->format('w');
if (in_array($dayOfWeek, $accptedHolidays[$name], true)) {
$dates[$day]['weekend:'.$day] = $name;
}
}

// Add substitute holiday to the list
$holidays[] = $substitute;
// Step 2. Add substitute holidays by referring to the temporary table.
$tz = DateTimeZoneFactory::getDateTimeZone($this->timezone);
foreach ($dates as $day => $names) {
$count = count($names);
if ($count < 2) {
continue;
} else {
// In a temporary table, public holidays are keyed by numeric number.
// And weekends are keyed by string start with 'weekend:'.
// For the substitute, we will use first item in queue.
$origin = $this->getHoliday($names[0]);
$workDay = $this->nextWorkingDay(DateTime::createFromFormat('Y-m-d', $day, $tz));
$this->addSubstituteHoliday($origin, $workDay->format('Y-m-d'));
}
}
}

/**
* Helper method to find a first working day after specific date.
*/
private function nextWorkingDay(DateTime $date): DateTime
{
$interval = new DateInterval('P1D');
$next = clone $date;
do {
$next->add($interval);
} while (!$this->isWorkingDay($next));

return $next;
}

/**
* Helper method to add substitute holiday.
*
* Add a substitute holiday from origin holiday to different date.
*
* @throws \Exception
*/
private function addSubstituteHoliday(Holiday $origin, string $date_str): void
{
$this->addHoliday(new SubstituteHoliday(
$origin,
[],
new DateTime($date_str, DateTimeZoneFactory::getDateTimeZone($this->timezone)),
$this->locale
));
}
}
58 changes: 34 additions & 24 deletions tests/SouthKorea/ChildrensDayTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class ChildrensDayTest extends SouthKoreaBaseTestCase implements HolidayTestCase
* @throws Exception
* @throws ReflectionException
*/
public function testMainHoliday(): void
public function testHoliday(): void
{
$year = $this->generateRandomYear(self::ESTABLISHMENT_YEAR);
$this->assertHoliday(
Expand All @@ -62,47 +62,57 @@ public function testMainHoliday(): void
*/
public function testSubstituteHolidayByBuddhasBirthday(): void
{
$tz = new DateTimeZone(self::TIMEZONE);

foreach ([2025, 2044] as $year) {
$this->assertHoliday(
self::REGION,
'substituteHoliday:childrensDay',
'buddhasBirthday',
$year,
new DateTime("$year-5-5", $tz)
);
$this->assertSubstituteHoliday(
self::REGION,
self::HOLIDAY,
$year,
new DateTime("$year-5-6", new DateTimeZone(self::TIMEZONE))
new DateTime("$year-5-6", $tz)
);
}
}

/**
* Tests the substitute holiday defined in this test (conflict with Saturday).
* Tests the substitute holiday defined in this test.
*
* @throws Exception
* @throws ReflectionException
*/
public function testSubstituteHolidayBySaturday(): void
public function testSubstituteHoliday(): void
{
$year = 2029;
$this->assertHoliday(
$tz = new DateTimeZone(self::TIMEZONE);

// Before 2022
$this->assertNotSubstituteHoliday(self::REGION, self::HOLIDAY, 2013);
$this->assertSubstituteHoliday(
self::REGION,
'substituteHoliday:childrensDay',
$year,
new DateTime("$year-5-7", new DateTimeZone(self::TIMEZONE))
self::HOLIDAY,
2019,
new DateTime('2019-5-6', $tz)
);
}

/**
* Tests the substitute holiday defined in this test (conflict with Sunday).
*
* @throws Exception
* @throws ReflectionException
*/
public function testSubstituteHolidayBySunday(): void
{
$year = 2019;
$this->assertHoliday(
// By saturday
$this->assertSubstituteHoliday(
self::REGION,
'substituteHoliday:childrensDay',
$year,
new DateTime("$year-5-6", new DateTimeZone(self::TIMEZONE))
self::HOLIDAY,
2029,
new DateTime('2029-5-7', $tz)
);

// By sunday
$this->assertSubstituteHoliday(
self::REGION,
self::HOLIDAY,
2024,
new DateTime('2024-5-6', $tz)
);
}

Expand Down
Loading