【PHP】祝日取得クラス

日本の祝日を返すクラスです。
Google Calendar APIを利用する方法をはじめ同様の処理は既にいくつも出回っていますが…

以前より個人的に使っていた処理がクラス化していなかった上、祝日定義の記述自体にも多重に三項演算子を多用していて少々メンテナンスしにくかったので、クラス化のついでにその辺も作り直しました。

春分の日及び秋分の日については簡易的な実装ですので、現在から遠い年では正しい値を返す保証はありません。

~~~php:holiday_class.php
<?php
class Holiday {
    private $holidayDefinitions;
    private $year;
    private $month;
    private $result;
    private $useIndefiniteHoliday;
    private $resultType;
    private $dateTime;

    public function __construct($year = 0) {
        // 祝日定義
        // ('国民の祝日に関する法律'が公布・施行された1948年7月20日以降のもののみ)
        $this->holidayDefinitions = [
            1 => [
                '1949:1'    => '元日',
                '1949:15, 2000:2_1'
                            => '成人の日',
            ],
            2 => [
                '1967:11'   => '建国記念の日',
                '2020:23'   => '天皇誕生日',
                '1989:24, 1990:0'
                            => '大喪の礼',
            ],
            3 => [
                '1949:s'    => '春分の日',
            ],
            4 => [
                '1959:10, 1960:0'
                            => '結婚の儀',
                '1949:29'   => '天皇誕生日, 1989:みどりの日, 2007:昭和の日',
            ],
            5 => [
                '2019:1, 2020:0'
                            => '皇太子殿下即位・改元',
                '1949:3'    => '憲法記念日',
                '2007:4'    => 'みどりの日',
                '1949:5'    => 'こどもの日',
            ],
            6 => [
                '1993:9, 1994:0'
                            => '結婚の儀',
            ],
            7 => [
                '1996:20, 2003:3_1, 2020:23, 2021:22, 2022:3_1'
                            => '海の日',
                '2020:24, 2021:23, 2022:0'
                            => 'スポーツの日',
            ],
            8 => [
                '2016:11, 2020:10, 2021:8, 2022:11'
                            => '山の日',
            ],
            9 => [
                '1966:15, 2003:3_1'
                            => '敬老の日',
                '1948:a'    => '秋分の日',
            ],
            10 => [
                '1966:10, 2000:2_1, 2020:0, 2022:2_1'
                            => '体育の日, 2020:スポーツの日',
                '2019:22, 2020:0'
                            => '即位礼正殿の儀',
            ],
            11 => [
                '1948:3'    => '文化の日',
                '1990:12, 1991:0'
                            => '即位礼正殿の儀',
                '1948:23'   => '勤労感謝の日',
            ],
            12 => [
                '1989:23, 2019:0'
                            => '天皇誕生日',
            ],
        ];

        $this->dateTime = new DateTime();
        $this->dateTime->setTime(0, 0, 0);

        if($year < 1) $year = $this->dateTime->format('Y');
        $this->year = (int) $year;
        $this->month = (int) $this->dateTime->format('m');

        $this->result = [];
        $this->useIndefiniteHoliday = 1;
        $this->resultType = 0;
    }

    /**
     *  1年分のリストを返す
     */
    public function getHolidayOfYear($year = 0) {
        if($year < 1) $year = $this->year;
        $year = (int) $year;

        if(!isset($this->result[$year])) {
            // 該当年の祝日配列に変換
            $holiday = [];
            $equinox = .242194 * ($year - 1980) - floor(($year - 1980) / 4);
            foreach($this->holidayDefinitions as $month => $currentMonthData) {
                $holiday[$month] = [];
                foreach($currentMonthData as $days => $names) {
                    // 対象年・日取得
                    $days = explode(',', $days);
                    $arrTmp = [];
                    foreach($days as $tmp) {
                        $tmp = explode(':', $tmp);
                        $arrTmp[(int) $tmp[0]] = trim($tmp[1]);
                    }
                    $yearTmp = 0;
                    foreach(array_keys($arrTmp) as $tmp)
                        if($year >= $tmp && $tmp >= $yearTmp) $yearTmp = $tmp;

                    if($yearTmp == 0) continue;

                    // 日を記述形式ごとに取得
                    if($arrTmp[$yearTmp] === 's')
                        $day = floor(20.8431 + $equinox);
                    elseif($arrTmp[$yearTmp] === 'a')
                        $day = floor(23.2488 + $equinox);
                    elseif(strpos($arrTmp[$yearTmp], '_') !== false) {
                        [$num, $w] = explode('_', $arrTmp[$yearTmp]);
                        $day = $this->getDayOfNumWeek($year, $month, $num, $w);
                    }
                    else
                        $day = $arrTmp[$yearTmp];

                    if($day < 1) continue;

                    // 名称取得
                    $names = explode(',', $names);
                    $arrTmp = [];
                    foreach($names as $tmp) {
                        $tmp = explode(':', $tmp);
                        if(count($tmp) == 1)
                            $arrTmp[0] = trim($tmp[0]);
                        else
                            $arrTmp[(int) $tmp[0]] = trim($tmp[1]);
                    }

                    $yearTmp = 0;
                    foreach(array_keys($arrTmp) as $tmp)
                        if($year >= $tmp && $tmp >= $yearTmp) $yearTmp = $tmp;

                    $holiday[$month][$day] = !isset($holiday[$month][$day]) ?
                        $arrTmp[$yearTmp] : $holiday[$month][$day]. ', '. $arrTmp[$yearTmp];
                }
            }

            // 国民の休日・振替休日
            if($this->useIndefiniteHoliday)
                $holiday = $this->indefiniteHoliday($holiday, $year);

            foreach($holiday as $k => $v) ksort($holiday[$k]);
            $this->result[$year] = $holiday;
        }

        return $this->resultType ?
            $this->convertLinear($year, $this->result[$year]) :
            $this->result[$year];
    }

    /**
     *  国民の休日・振替休日
     */
    private function indefiniteHoliday($holiday, $year) {
        for($month = 1; $month <= 12; $month++) {
            // 月末日
            $lastDay = (int) $this->dateTime->setDate($year, $month, 1)->format('t');

            for($day = 1; $day <= $lastDay; $day++) {
                // 前日の月日
                $this->dateTime->setDate($year, $month, $day)->modify('-1 day');
                $prevMonth = (int) $this->dateTime->format('m');
                $prevDay = (int) $this->dateTime->format('d');

                // 祝日に挟まれた平日を国民の休日に変更(1986年以降)
                if( $year >= 1986 &&
                    isset($holiday[$prevMonth][$prevDay])
                ) {
                    // 翌日の月日
                    $this->dateTime->setDate($year, $month, $day)->modify('+1 day');
                    $nextMonth = (int) $this->dateTime->format('m');
                    $nextDay = (int) $this->dateTime->format('d');

                    $this->dateTime->setDate($year, $month, $day);
                    if( isset($holiday[$nextMonth][$nextDay]) &&
                        !isset($holiday[$month][$day]) &&
                        (int) $this->dateTime->format('w') !== 0
                    ) {
                        $holiday[$month][$day] = '国民の休日';
                    }
                }

                $this->dateTime->setDate($year, $month, $day);
                // 振替休日(1973年4月以降)
                if(($year == 1973 && $month >= 4 || $year > 1973) &&
                    // 祝日かつ日曜
                    isset($holiday[$month][$day]) &&
                    (int) $this->dateTime->format('w') === 0
                ) {
                    // その日以降の直近の平日を振替休日に
                    for($i = 1; $i < 7; $i++) {
                        $this->dateTime->setDate($year, $month, $day)->modify("+$i day");
                        $m = (int) $this->dateTime->format('m');
                        $d = (int) $this->dateTime->format('d');
                        if(!isset($holiday[$m][$d])) {
                            $holiday[$m][$d] = '振替休日';
                            break;
                        }
                    }
                }
            }
        }
        return $holiday;
    }

    /**
     *  $year年 $month月 第$num $w曜日に該当する日を返す
     */
    private function getDayOfNumWeek($year, $month, $num, $w) {
        $firstDayWeek = (int) $this->dateTime->setDate($year, $month, 1)->format('w');
        return 1 + ($num - 1) * 7 + (7 + $w - $firstDayWeek) % 7;
    }

    /**
     *  YYYY-MM-DDをキーとした連想配列に変換
     */
    private function convertLinear($year, $array) {
        $arrTmp = [];
        foreach($array as $month => $currentMonthData) {
            foreach($currentMonthData as $days => $names) {
                $arrTmp[sprintf('%04d-%02d-%02d', $year, $month, $days)] = $names;
            }
        }
        return $arrTmp;
    }

    /**
     *  1か月分のリストを返す
     */
    public function getHolidayOfMonth($month = 0) {
        if($month < 1 || $month > 12) $month = $this->month;

        $year = $this->year;

        // 該当年の結果が未取得であれば取得
        if(!isset($this->result[$year]))
            $this->getHolidayOfYear();

        if($this->resultType) {
            $arrTmp = [];
            foreach($this->result[$year][(int) $month] as $days => $names) {
                $arrTmp[sprintf('%04d-%02d-%02d', $year, $month, $days)] = $names;
            }
            return $arrTmp;
        } else {
            return $this->result[$year][(int) $month];
        }
    }

    /**
     *  国民の休日・振替休日 使用フラグ変更
     */
    public function setUseIndefiniteHoliday($flg = 1) {
        $this->useIndefiniteHoliday = $flg == 1 ? 1 : 0;

        // 取得済みの結果をリセット
        $this->result = [];
    }

    /**
     *  戻り値形式変更
     */
    public function setResultType($flg = 0) {
        $this->resultType = $flg == 0 ? 0 : 1;
    }

    /**
     *  年変更
     */
    public function setYear($year = 0) {
        if($year < 1) $year = (new DateTime())->format('Y');
        $this->year = (int) $year;
    }
}
~~~


##使用例##
####インスタンス生成####
`new Holiday([$year])`
**year**
`年プロパティ`の初期値。省略、または0以下の値を渡した場合は現在の年で設定。

~~~php:example
$holiday = new Holiday(2020);
$holiday = new Holiday();
$holiday = new Holiday;
~~~

####メソッド####

**年プロパティ変更**
`setYear([$year])`
**year**
`年`。省略、または0以下の値を渡した場合は現在の年で設定。

~~~php:example
$holiday = new Holiday;
$holiday->setYear(2000);
$holiday->setYear();
~~~

**1年分の祝日リストを取得**
`getHolidayOfYear([$year])`
**year**
`年`。省略、または0以下の値を渡した場合は年プロパティの値を使用。
インスタンス内の年プロパティ変更はしません。

~~~php:example
$holiday = new Holiday;
print_r($holiday->getHolidayOfYear());
~~~

~~~plain:result
Array
(
    [1] => Array
        (
            [1] => 元日
            [13] => 成人の日
        )

    [2] => Array
        (
            [11] => 建国記念の日
            [23] => 天皇誕生日
            [24] => 振替休日
        )

    [3] => Array
        (
            [20] => 春分の日
        )

    [4] => Array
        (
            [29] => 昭和の日
        )

    [5] => Array
        (
            [3] => 憲法記念日
            [4] => みどりの日
            [5] => こどもの日
            [6] => 振替休日
        )

    [6] => Array
        (
        )

    [7] => Array
        (
            [23] => 海の日
            [24] => スポーツの日
        )

    [8] => Array
        (
            [10] => 山の日
        )

    [9] => Array
        (
            [21] => 敬老の日
            [22] => 秋分の日
        )

    [10] => Array
        (
        )

    [11] => Array
        (
            [3] => 文化の日
            [23] => 勤労感謝の日
        )

    [12] => Array
        (
        )

)
~~~

**1か月分の祝日リストを取得**
`getHolidayOfMonth([$month])`
**month**
`月`。省略時または範囲外の値を指定した場合は現在の月が使用されます。
年はインスタンス内の年プロパティを使用します。

~~~php:example
$holiday = new Holiday;
print_r($holiday->getHolidayOfMonth());
~~~

~~~plain:result
Array
(
    [23] => 海の日
    [24] => スポーツの日
)
~~~

**戻り値のリスト形式変更**
`setResultType([$resultType])`
**resultType**
戻り値の形式
`0`: 月、日をキーとした連想配列(デフォルト)
`1`: YYYY-MM-DD をキーとした連想配列

~~~php:example
$holiday = new Holiday;
$holiday->setResultType(1);
print_r($holiday->getHolidayOfYear());
print_r($holiday->getHolidayOfMonth(5));
~~~

~~~plain:result
Array
(
    [2020-01-01] => 元日
    [2020-01-13] => 成人の日
    [2020-02-11] => 建国記念の日
    [2020-02-23] => 天皇誕生日
    [2020-02-24] => 振替休日
    [2020-03-20] => 春分の日
    [2020-04-29] => 昭和の日
    [2020-05-03] => 憲法記念日
    [2020-05-04] => みどりの日
    [2020-05-05] => こどもの日
    [2020-05-06] => 振替休日
    [2020-07-23] => 海の日
    [2020-07-24] => スポーツの日
    [2020-08-10] => 山の日
    [2020-09-21] => 敬老の日
    [2020-09-22] => 秋分の日
    [2020-11-03] => 文化の日
    [2020-11-23] => 勤労感謝の日
)
Array
(
    [2020-05-03] => 憲法記念日
    [2020-05-04] => みどりの日
    [2020-05-05] => こどもの日
    [2020-05-06] => 振替休日
)
~~~

**国民の休日及び振替休日の使用指定**
`setUseIndefiniteHoliday([$useIndefiniteHoliday])`
**useIndefiniteHoliday**
`0`: 使用しない
`1`: 使用する(デフォルト)

~~~php:example
$holiday = new Holiday(2019);
$holiday->setResultType(1);
print_r($holiday->getHolidayOfMonth(5));
$holiday->setUseIndefiniteHoliday(0);
print_r($holiday->getHolidayOfMonth(5));
~~~

~~~plain:result
Array
(
    [2019-05-01] => 皇太子殿下即位・改元
    [2019-05-02] => 国民の休日
    [2019-05-03] => 憲法記念日
    [2019-05-04] => みどりの日
    [2019-05-05] => こどもの日
    [2019-05-06] => 振替休日
)
Array
(
    [2019-05-01] => 皇太子殿下即位・改元
    [2019-05-03] => 憲法記念日
    [2019-05-04] => みどりの日
    [2019-05-05] => こどもの日
)
~~~

##参考##
[「国民の祝日」について | 内閣府](https://www8.cao.go.jp/chosei/shukujitsu/gaiyou.html)
[昭和30年（1955年）から令和3年（2021年）国民の祝日 csv | 内閣府](https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv)