休日表示付きカレンダー

# 何をする？

[MixJuice](http://mixjuice.shizentai.jp/)経由で[IchigoJam](https://ichigojam.net/)から祝日データを取得し、
日曜と祝日をハイライトしたカレンダーを出力します。

※IchigoJamはjig.jpの登録商標です。

# 日本の祝日を取得するAPI

日本の祝日を取得するWeb APIを探したところ、以下のものが見つかりました。

## 国民の祝日：日本

[このサイトについて | 国民の祝日：日本](https://national-holidays.jp/about.html)

指定した年・月・日の祝日のリストが得られます。

例えば、以下のURLにアクセスすると

```
https://api.national-holidays.jp/202108
```

以下の結果が得られました。

```
[{"date": "2021-08-08", "name": "山の日", "type": "国民の祝日"}, {"date": "2021-08-09", "name": "振替休日", "type": "振替休日"}]
```

見やすく整形すると、以下のような感じです。

```json
[
    {
        "date": "2021-08-08",
        "name": "山の日",
        "type": "国民の祝日"
    },
    {
        "date": "2021-08-09",
        "name": "振替休日",
        "type": "振替休日"
    }
]
```

今回見つかった中で唯一指定の月の休日の取得に対応しており、今回の目的との相性が良さそうです。
さらに、HTTPSに対応しています。
Firefoxで直接APIのURLにアクセスした時は`Access-Control-Allow-Origin`ヘッダは確認できませんでしたが、
IchigoJam webのバーチャルMixJuiceからのアクセスを試してみると
`Access-Control-Allow-Origin: *`ヘッダが入り、データの取得が可能でした。

記事執筆時点で、1948年～2022年のデータの取得に対応しているようです。

## Holidays JP API

[Holidays JP API (日本の祝日API)](https://holidays-jp.github.io/)

指定の月の祝日のリストが取得できます。

例えば、以下のURLにアクセスすると

```
https://holidays-jp.github.io/api/v1/2021/date.json
```

以下の結果が得られました。

```
{
    "2021-01-01": "元日",
    "2021-01-11": "成人の日",
    "2021-02-11": "建国記念の日",
    "2021-02-23": "天皇誕生日",
    "2021-03-20": "春分の日",
    "2021-04-29": "昭和の日",
    "2021-05-03": "憲法記念日",
    "2021-05-04": "みどりの日",
    "2021-05-05": "こどもの日",
    "2021-07-22": "海の日",
    "2021-07-23": "スポーツの日",
    "2021-08-08": "山の日",
    "2021-08-09": "休日 山の日",
    "2021-09-20": "敬老の日",
    "2021-09-23": "秋分の日",
    "2021-11-03": "文化の日",
    "2021-11-23": "勤労感謝の日"
}
```

今回見つかった他のAPIとは違い、結果に最初から改行やインデントが入っていました。

HTTPSに対応し、`Access-Control-Allow-Origin: *`ヘッダも入っているため、
IchigoJam webのバーチャルMixJuiceでの利用がしやすそうです。

記事執筆時点では2015年～2022年のデータの取得に対応しているようで、
今回見つかった中では一番対応範囲が狭いです。

## 祝日判定web API

[国民の祝日チェック](http://s-proj.com/utils/holiday.html)

祝日のリストを得る機能は見当たらず、指定した日1日が祝日かどうかを判定できます。

例えば、

```
http://s-proj.com/utils/checkHoliday.php?kind=ph&date=20210809
```

というURLにアクセスすると

```
holiday
```

という結果が返り、

```
http://s-proj.com/utils/checkHoliday.php?kind=ph&date=20210810
```

というURLにアクセスすると

```
else
```

という結果が返ってきました。
今回見つかった他のAPIと違い、結果はJSONではなくシンプルなプレーンテキストで返るようです。

ヘッダに`Access-Control-Allow-Origin`は含まれていませんでした。
また、HTTPSへの対応も不完全です。(接続はできるが、証明書が期限切れの自己署名証明書)
IchigoJam webのバーチャルMixJuiceでの利用は難しいかもしれません。

データの対応範囲は2000年～となっていますが、試してみると1955年までは結果が返ってきました。
記事執筆時において、上限は2022年のようです。

## Nager.Date

[Worldwide Public Holidays - Nager.Date](https://date.nager.at/)

日本だけでなく、世界各国の祝日のリストの取得ができます。

例えば、

```
https://date.nager.at/api/v3/PublicHolidays/2021/JP
```

というURLにアクセスすると、

```
{"date":"2021-01-01","localName":"元日","name":"New Year's Day","countryCode":"JP","fixed":false,"global":true,"counties":null,"launchYear":null,"types":["Public"]}
```

という形式のデータの配列が得られました。
見やすく整形すると、以下のような感じです。

```json
{
    "date": "2021-01-01",
    "localName": "元日",
    "name": "New Year's Day",
    "countryCode": "JP",
    "fixed": false,
    "global": true,
    "counties": null,
    "launchYear": null,
    "types": [
        "Public"
    ]
}
```

日付と名前の他に様々な属性がついており、結果が長くなっています。
また、HTTPSに対応しています。
Firefoxで直接APIのURLにアクセスした時は`Access-Control-Allow-Origin`ヘッダは確認できませんでしたが、
IchigoJam webのバーチャルMixJuiceからのアクセスを試してみると
`access-control-allow-origin: *`ヘッダが入り、データの取得が可能でした。

対応範囲については、1年～9999年について結果が返ってきました。
今回見つけた中で一番対応範囲が広いようです。

祝日と日曜日が重なる場合は、振替休日の日付が祝日の日付として返るようです。
また、2019年5月1日「天皇の即位の日」が載っていない、
2021年8月8日(振替休日9日)ではなく8月11日が「山の日」となっているなど、正確性に難があるようです。

## まとめ

|API|HTTPS対応|CORS対応|指定年取得|指定月取得|指定日取得|シンプルな結果|正確性|対応年範囲|
|---|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|[国民の祝日：日本](https://national-holidays.jp/about.html)|○|○|○|○|○|○|○|1948～2022|
|[Holidays JP API](https://holidays-jp.github.io/)|○|◎|○|×|×|○|○|2015～2022|
|[祝日判定web API](http://s-proj.com/utils/holiday.html)|×|×|×|×|○|◎|○|1955(2000?)～2022|
|[Nager.Date](https://date.nager.at/)|○|○|○|×|×|×|×|1～9999|

(CORS対応：ヘッダに`Access-Control-Allow-Origin: *`が含まれる)
(正確性：2019年5月1日「天皇の即位の日」および2021年8月8日「山の日」をチェックしました)

# カレンダーの出力

今回は、年と月を入力させ、その年のその月のカレンダーを出力することにします。
カレンダーを出力するには、「その月の1日が何曜日か」と「その月が何日までか」の情報を使います。
「その月が何日までか」は、[昨日の記事](https://qiita.com/mikecat_mixc/items/3dd1e1e03b44edf99a13)の方法で求められます。
「その月の1日が何曜日か」は、[ツェラーの公式](https://ja.wikipedia.org/wiki/%E3%83%84%E3%82%A7%E3%83%A9%E3%83%BC%E3%81%AE%E5%85%AC%E5%BC%8F)を用いて求められます。
これらの情報をもとに、1日の位置を合わせ、月末まで日付を表す数を出力します。

```
10 ' ｶﾚﾝﾀﾞｰ
20 INPUT "ﾅﾝ  ﾈﾝ?",Y
30 INPUT "ﾅﾝ ｶﾞﾂ?",M
40 IF M<=2 Z=Y-1:N=M+12 ELSE Z=Y:N=M
50 C=Z/100:D=Z%100
60 E=26*(N+1)/10:F=5*C+C/4
70 H=(1+E+D+D/4+F+6)%7
80 G=(30+M%2)^(M>7):IF M=2 U=Y%400=0OR(Y%4=0ANDY%100):G=28+U
90 ?" Su Mo Tu We Th Fr Sa"
100 IF H>0 FOR I=1 TO H:?"   ";:NEXT
110 FOR I=1 TO G
120 ?" ";:IF I<10 ?" ";
130 ?I;
140 H=H+1:IF H>6 H=0:?CHR$(10);
150 NEXT
160 IF H>0 ?CHR$(10);
```

# 祝日リストの取得と利用

## 使用するAPI

今回は、情報が正確で対応範囲が広い、[国民の祝日：日本](https://national-holidays.jp/about.html)を使用することにしました。
月単位で取得できるAPIもありますが、データをまとめて取得できる年単位のAPIを使用することにしました。

## 祝日の日付の取得

今回は祝日の日付のみ取得できればいいので、以下の方法で取得することにしました。

* 読み込んだ文字が数字なら、数値の読み取りを進める。
* 読み込んだ文字が数字でなく、直前の文字が数字のとき、2個前に読み込んだ数値が取得中の年であれば、1個前に読み込んだ数値を月、今読み込んだ数値を日として、祝日のリストに加える。

さらに、今回読み込む数値は日付を表す正の数のみです。
そこで、読み取り中の数値が正であれば、直前の文字が数字だと判定します。

祝日のリストは、ビットマップで表現します。
1個の月を配列の2個の要素(16ビット×2＝32ビット)で表し、各ビットを祝日なら1、祝日でなければ0とします。
ビットは0番目～31番目が使えるので、最大31の日を表すのにピッタリです。

以下が具体的なコードです。

ビットマップと、読み込んだ数値を格納する変数`N`(年)、`T`(月)、`H`(日)を初期化します。

```
60 FORI=2TO25:[P+I]=0:NEXT:N=0:T=0:H=0
```

クエリを送信し、進捗状況表示用の変数`L`を初期化します。

```
70 ?CHR$(10);"MJ GETS api.national-holidays.jp/";Y:L=0
```

1文字読み込みます。
数値を読み込んだ後文字を読み込めず0.5秒経つか、`]`を読み込んだら、次の処理に行きます。

```
80 C=INKEY():IF(TANDTICK()>29)ORC=#5D?CHR$(10);:GOTO140ELSEIFC=0GOTO80
```

読み込んだ文字が数字であれば、読み込んだ数値`H`を更新します。

```
90 CLT:IF#30<=CANDC<=#39H=H*10+C-#30:GOTO80
```

読み込んだ文字が数字でないとき、ここ以降の処理に入ります。
数値を読み込んでいないとき、そのまま次の文字を読み込みに行きます。

```
100 IFH=0GOTO80
```

年が読み込み中の年で、月が有効なものである時、ビットマップに祝日情報を追加します。

```
110 IFN=YAND1<=TANDT<=12X=P+T*2+(H>15):[X]=[X]|1<<(H&15)ELSEGOTO130
```

進捗状況を出力します。
新しい月に移った時、この月の数になるように`*`を出力します。

```
120 IFL<T?"*";:L=L+1:GOTO120
```

読み込んだ数値の更新と初期化を行い、次の文字を読み込みに行きます。

```
130 N=T:T=H:H=0:GOTO80
```

## 休日を考慮したカレンダーの出力

今回は、休日(日曜日と祝日)を白黒反転した数字で表示することにしました。
これを実現するため、`#E0`からの部分に白黒反転した数字のフォントを用意します。

```
20 FORI=0TO79:POKE#700+I,~PEEK(#180+I):NEXT
```

どの年のどの月のカレンダーを出力するのかを読み込みます。

```
30 INPUT"ﾅﾝ  ﾈﾝ?",Y:INPUT"ﾅﾝ ｶﾞﾂ?",M
```

ツェラーの公式でこの月の1日の曜日を求めます。
`H`を祝日フラグに使うため、曜日を表す変数を`W`としています。

```
140 IFM<3Z=Y-1:N=M+12ELSEZ=Y:N=M
150 C=Z/100:D=Z%100:E=26*(N+1)/10:F=5*C+C/4:W=(1+E+D+D/4+F+6)%7:G=(30+M%2)^(M>7):IFM=2U=Y%400=0OR(Y%4=0ANDY%100):G=28+U
```

曜日と、1日の前の空白を出力します。

```
160 ?" Su Mo Tu We Th Fr Sa":IFW>0FORI=1TOW:?"   ";:NEXT
```

休日かを判定し、それに応じて日付を出力します。

* 1日～9日 (1桁) のとき
  * 休日のときは、白1色(`#8F`)を出力します。
  * 平日のときは、空白(`#20`)を出力します。
* 10日～ (2桁) のとき
  * 休日の時は、`#E0`～`#E9`の白黒反転した数字を出力します。
  * 平日の時は、`#30`～`#39`の通常の数字を出力します。

```
170 FORI=1TOG:?" ";:X=P+M*2+(I>15):H=W=0OR[X]>>(I&15)&1:IFI<10?CHR$(#20+H*#6F);ELSE?CHR$(#30+#B0*H+I/10);
```

日付の1桁目を出力します。
休日の時は`#E0`～`#E9`の白黒反転した数字を出力し、平日の時は`#30`～`#39`の通常の数字を出力します。

```
180 ?CHR$(#30+#B0*H+I%10);:W=W+1:IFW>6W=0:?CHR$(10);
```

月末まで日付を出力後、改行がない場合は改行を出力します。

```
190 NEXT:IFW?CHR$(10);
```

## 祝日リストのキャッシュ

今回扱う祝日リストは比較的長いデータであり、MixJuiceでの取得には無視できない時間がかかります。
そこで、取得した祝日リストをキャッシュし、カレンダー出力の時に毎回取得しなくていいようにしました。

IchigoJamの配列は102要素あり、1年のデータの格納には24要素を使うので、4年分のデータを格納できます。
そこで、さらに1年あたり1要素を使用し、
「何年のデータか」と「そのデータをどこに格納したか」を管理することにしました。
今回は、下2ビットを「どこに格納したか」、それ以外のビットを「何年のデータか」に割り当てました。

4年分のデータを取得・格納した後、また別の年のデータを格納するには、
格納した4年分のデータのいずれかを消去して上書きしないといけません。
ここでどのデータを消去するかによって、キャッシュの利便性が変わってくる可能性があります。
できれば、この後使うデータを残し、使わないデータを消去したいです。

今回は、「格納されたデータの中で、最後に使ったのが一番昔のもの」を消去することにしました。
これはLRU (Least Recently Used) と呼ばれ、一般的に使われる方法の一つです。

以下が具体的なコードです。

キャッシュの管理用データを初期化します。
「何年のデータか」を全て0年とし、「どこに格納したか」の情報を準備します。
今回は、最近使ったデータほど大きい添字に格納することにしました。

```
20 LET[0],0,1,2,3
```

カレンダーを出力する年`Y`のデータがキャッシュにあるかを探します。
見つかった場合は、祝日データを格納する位置`P`を取得し、
管理用データをシフトして見つかった管理用データを`[3]`に移動します。
ちなみに、使いたいデータがキャッシュから見つかることを「ヒット」といいます。

```
40 P=0:FORI=0TO3:IF[I]/4=Y:P=[I]%4*24+2:T=[I]:FORJ=ITO3:[J]=[J+1]:NEXT:[3]=T:I=9
50 NEXT:IFPGOTO140
```

データが見つからなかった場合は、
最後に使ったのが一番昔のデータ`[0]`のかわりに新しい年のデータを格納することにします。
管理用データをシフトし、新しいデータの年と格納位置を`[3]`にセットします。
ちなみに、使いたいデータがキャッシュから見つからないことを「ミス」といいます。

```
60 T=[0]:[0]=[1]:[1]=[2]:[2]=[3]:[3]=Y*4+T%4:P=T%4*24+2
```

# 作成したプログラム

```
10 ' ｷｭｳｼﾞﾂ ｶﾚﾝﾀﾞｰ
20 LET[0],0,1,2,3:FORI=0TO79:POKE#700+I,~PEEK(#180+I):NEXT
30 INPUT"ﾅﾝ  ﾈﾝ?",Y:INPUT"ﾅﾝ ｶﾞﾂ?",M
40 P=0:FORI=0TO3:IF[I]/4=Y:P=[I]%4*24+2:T=[I]:FORJ=ITO3:[J]=[J+1]:NEXT:[3]=T:I=9
50 NEXT:IFPGOTO140
60 T=[0]:[0]=[1]:[1]=[2]:[2]=[3]:[3]=Y*4+T%4:P=T%4*24+2:FORI=2TO25:[P+I]=0:NEXT:N=0:T=0:H=0
70 ?CHR$(10);"MJ GETS api.national-holidays.jp/";Y:L=0
80 C=INKEY():IF(TANDTICK()>29)ORC=#5D?CHR$(10);:GOTO140ELSEIFC=0GOTO80
90 CLT:IF#30<=CANDC<=#39H=H*10+C-#30:GOTO80
100 IFH=0GOTO80
110 IFN=YAND1<=TANDT<=12X=P+T*2+(H>15):[X]=[X]|1<<(H&15)ELSEGOTO130
120 IFL<T?"*";:L=L+1:GOTO120
130 N=T:T=H:H=0:GOTO80
140 IFM<3Z=Y-1:N=M+12ELSEZ=Y:N=M
150 C=Z/100:D=Z%100:E=26*(N+1)/10:F=5*C+C/4:W=(1+E+D+D/4+F+6)%7:G=(30+M%2)^(M>7):IFM=2U=Y%400=0OR(Y%4=0ANDY%100):G=28+U
160 ?" Su Mo Tu We Th Fr Sa":IFW>0FORI=1TOW:?"   ";:NEXT
170 FORI=1TOG:?" ";:X=P+M*2+(I>15):H=W=0OR[X]>>(I&15)&1:IFI<10?CHR$(#20+H*#6F);ELSE?CHR$(#30+#B0*H+I/10);
180 ?CHR$(#30+#B0*H+I%10);:W=W+1:IFW>6W=0:?CHR$(10);
190 NEXT:IFW?CHR$(10);
200 GOTO30
```

# 実行結果

祝日のデータを取得し、休日をハイライトしたカレンダーを出力することができました。

![実行結果](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/2438c439-11a1-bb60-628f-f7716a19dccf.png)

IchigoJam・MixJuice実機においては、
IchigoJam BASIC 1.0.0、MixJuiceからの送信間隔30ms (`?"MJ SPW 30"`)において、
データを欠けずに取得することができました。

IchigoJam web・バーチャルMixJuiceにおいては、データが欠けてしまい、正しく取得できませんでした。