Date に休日情報を追加した派生クラスを作ってカレンダーを表示する

カレンダーは[随分前に作った](https://ikiuo.wordpress.com/2009/03/20/windows-vista-%e3%82%ac%e3%82%b8%e3%82%a7%e3%83%83%e3%83%88-%e2%80%93-%e3%82%b7%e3%83%b3%e3%83%97%e3%83%ab%e3%82%ab%e3%83%ac%e3%83%b3%e3%83%80%e3%83%bc/)ことがありますが、現在の情報を追加して作り直してみました。

<p class="codepen" data-height="960" data-default-tab="result" data-slug-hash="mdoqNQV" data-user="ikiuo" style="height: 960px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/ikiuo/pen/mdoqNQV">
  カレンダー(月/15週/年)</a> by Ikiuo (<a href="https://codepen.io/ikiuo">@ikiuo</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

# Google Chrome 拡張機能

CodePen のものを少し弄って Google Chrome 拡張機能にしてみました。

https://github.com/ikiuo/google-chrome-extension-calendar

# Date の派生クラス

**意外と面倒臭い休日処理**

- 春分と秋分の日
    - <sub>[大雑把な方法](https://qiita.com/ikiuo/items/565a0f2ee455ec6d653a)で計算しています</sub>
- ハッピーマンデー
- 休日と休日の間が休日になる
- 休日が日曜のときの代替

取得したい日とその前後くらいでは決定できないので、週初めから翌週の日曜までを使って決定しています。

```javascript:
/*
 * 休日データ
 */

class JHoliday {
    static #startDate = (new Date(1970, 1-1, 1)).getTime() * 10;
    static #offsetVE = JHoliday.#startDate +  68619916800;
    static #offsetAE = JHoliday.#startDate + 229674407040;
    static #yEquinox = 315569255616;

    static #monthTable = [...Array(12)].map(() => ({}));
    static #createParameter(name, month, day, start, end) {
        const S = JHoliday;

        month -= 1;
        const holidy = {
            name: name,
            month: month,
            day: day,
            start: Math.max(1948, (start ?? 0)),
            end: end ?? 10000,
        }
        const table = S.#monthTable;
        const mtab = table[month];
        if (!mtab[day])
            mtab[day] = [];
        mtab[day].push(holidy);
        return holidy;
    }

    static #mainTable = [
        ['元日', 1, 1],
        ['成人の日', 1, 15, 0, 1999],
        ['成人の日', 1, 'M2', 2000],

        ['建国記念の日', 2, 11],

        ['春分の日', 3, 'VE'],

        ['憲法記念日', 5, 3],
        ['みどりの日', 5, 4, 2007],
        ['こどもの日', 5, 5],

        ['海の日', 7, 20, 1996, 2002],
        ['海の日', 7, 'M3', 2003],

        ['山の日', 8, 11, 2016],

        ['敬老の日', 9, 15, 1966, 2002],
        ['敬老の日', 9, 'M3', 2003],
        ['秋分の日', 9, 'AE'],

        ['体育の日', 10, 10, 0, 1999],
        ['体育の日', 10, 'M2', 2000, 2019],
        ['スポーツの日', 10, 'M2', 2020],

        ['文化の日', 11, 3],
        ['勤労感謝の日', 11, 23],

        /* 昭和 */
        ['天皇誕生日', 4, 29, 0, 1988],
        ['みどりの日', 4, 29, 1989, 2006],
        ['昭和の日', 4, 29, 2007],

        /* 平成 */
        ['天皇誕生日', 12, 23, 1989, 2018],

        /* 令和 */
        ['天皇の即位の日', 5, 1, 2019, 2019],
        ['即位礼正殿の儀が行われる日', 10, 22, 2019, 2019],
        ['天皇誕生日', 2, 23, 2020],

    ].map((v) => JHoliday.#createParameter(...v));

    static #altname = '休日';

    static #getBasic(date) {
        const S = JHoliday;

        const year = date.getFullYear();
        const month = date.getMonth();
        const mday = date.getDate();
        const wday = date.getDay();

        const mtab = S.#monthTable[month]
        const dtab = mtab[mday];
        if (dtab) {
            const t = dtab.filter((h) =>
                (h.start <= year && year <= h.end));
            if (t.length)
                return t[0].name;
        }
        if (wday == 1) {
            const mweek = Math.trunc((mday - 1) / 7);
            const monday = mtab[`M${mweek+1}`];
            if (monday) {
                const t = monday.filter((h) =>
                    (h.start <= year && year <= h.end));
                if (t.length)
                    return t[0].name;
            }
        }

        const ve = mtab['VE'];
        if (ve) {
            const equniox = S.getVernalEquinox(year);
            if (mday == equniox.getDate())
                return ve[0].name;
        }

        const ae = mtab['AE'];
        if (ae) {
            const equniox = S.getAutumnEquinox(year);
            if (mday == equniox.getDate())
                return ae[0].name;
        }

        return undefined;
    }

    static #weekCache = {}
    static #getWeek(date) {
        const S = JHoliday;

        const year = date.getFullYear();
        const month = date.getMonth();
        const mday = date.getDate();
        const wday = date.getDay();
        const wtop = new Date(year, month, 1).getDay();
        const mweek = Math.trunc((wtop + mday - 1) / 7);
        const index = (year * 12 + month) * 6 + mweek;

        const cache = S.#weekCache[index];
        if (cache)
            return cache;

        const line = [...Array(8)].map((_, i) =>
            S.#getBasic(new Date(year, month, mday - wday + i)));
        if (year >= 1973 && line[0]) {
            for (let i = 1; i < 7; i++) {
                if (!line[i]) {
                    line[i] = S.#altname;
                    break;
                }
            }
        }
        if (year >= 1986)
            for (let i = 1; i < 7; i++)
                if (!line[i] && line[i - 1] && line[i + 1])
                    line[i] = S.#altname;

        S.#weekCache[index] = line;
        return line;
    }

    // 年から春分の日(Date型)を取得.
    static getVernalEquinox(year) {
        const S = JHoliday;
        return new Date(((year - 1970) * S.#yEquinox + S.#offsetVE) / 10);
    }

    // 年から秋分の日(Date型)を取得.
    static getAutumnEquinox(year) {
        const S = JHoliday;
        return new Date(((year - 1970) * S.#yEquinox + S.#offsetAE) / 10);
    }

    // Date 型から休日を取得.
    static getHoliday(date) {
        const S = JHoliday;
        return S.#getWeek(date)[date.getDay()];
    }
}

/*
 * 休日ありの Date 派生クラス
 */

class JHDate extends Date {
    #holiday;

    constructor() {
        super(...arguments);
        this.#holiday = JHoliday.getHoliday(this);
    }

    // 休日を取得.
    getHoliday() {
        return this.#holiday;
    }
}
```