<?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;
    }
}