Delphi で "指定した日が祝日かどうか" を内閣府のデータを使って調べる

#はじめに
以前カレンダーの壁紙を制作しようと思い立ち，祝祭日をどのように決めるかを調べていたら，
ht_dekoさんの記事で以下の記事をがありました。

>**Delphi で "指定した日が祝日かどうか" を調べる @ht_deko**
https://qiita.com/ht_deko/items/a0cbbbd899c1c016ae67

春分の日や秋分の日を計算で出すという便利な関数ですが，記事の最後に**「祝日は祝日テーブルに登録するのがベストだと思われます。」**と書かれておりました。春分の日と秋分の日が国立天文台の発表を待たないと確定せず，それは1年先のものまでしかわからないということに原因があるのだということが分かりました。
また，2021年についてみてみると東京オリンピックの関係で祝日が動いていています(2020/11/27参院可決)。これをいちいちプログラムで変更するのは面倒だなと思いました。

>**大臣官房総務課 国民の祝日について**
https://www8.cao.go.jp/chosei/shukujitsu/gaiyou.html
令和３年（２０２１年）の祝日について
令和３年（2021年）に限り、「海の日」は7月22日に、「スポーツの日」は7月23日に、「山の日」は8月8日(※）になります。
平成三十二年東京オリンピック競技大会・東京パラリンピック競技大会特別措置法等の一部を改正する法律の施行に伴い、改正後の令和三年東京オリンピック競技大会・東京パラリンピック競技大会特別措置法（平成27年法律第33号）第32条第2項の規定に基づき、令和3年（2021年）における海の日、スポーツの日及び山の日は上記の通りとなります。
（※）国民の祝日に関する法律（昭和23年法律第178号）第3条第2項の規定に基づき、8月9日は休日となります。

そこで，公式の休日の情報を取得しておいて，@ht_dekoさんの作られた
**function IsSpecialHoliday(ADate: TDate; var AName: string): Boolean;**
と仕様を合わせた関数を作ろうと考えました。

#データフォーマット
>**DATA●GO.JP データカタログサイト**
https://www.data.go.jp/data/dataset/cao_20190522_0002

によると，昭和30年（1955年）から令和2年（2020年）国民の祝日等（いわゆる振替休日等を含む）（csv形式：19KB）は2021年12月現在で，
https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv
より公開されています。改正後の令和三年東京オリンピック競技大会・東京パラリンピック競技大会特別措置法にも対応しています。
なお，ライセンスはCC-BYとなっております。

>CC BY 4.0
https://creativecommons.org/licenses/by/4.0/deed.ja

営利目的でも利用できますね。クレジット表示は必要です。
ダウンロードして分かったのですが，Shift-JISでエンコードされており，半角カンマで区切られた空白のない文字列です。
日付のフォーマットは yyyy/m/d です。例えば以下のようになっています。

```
2021/11/23,勤労感謝の日
```
休日全てのデータがあり**振替休日**は**休日**とだけ書かれています。

#ロジック
休日祭日のデータを取得しファイルに保存する関数と，ファイルに保存された休日祭日のデータを利用して指定した日が休日祭日かどうかを返す関数を実装します。
これらを一つのユニットにまとめて，

- 起動時に保存されたファイルがあればテーブルに読みだし，
- データが取得された場合にはそれを更新し，
- 休日祭日かどうかを返す関数は読みだされたデータを利用するようにして，
- 終了時にテーブルを開放する

ことにしました。
このデータを[THttpClient](http://docwiki.embarcadero.com/Libraries/ja/System.Net.HttpClient.THTTPClient)(XE8以降で実装された命令)で取得し，指定された場所に保存します。THttpClientはOSの機能でhttpsにも簡単にアクセスできるので便利ですね。シンプルに実装しました。非同期にする場合にはHOSOKAWA @pik様の

>THttpClient の落とし穴（解決編）
https://qiita.com/pik/items/95dfebdb659b30918196

がありますので，こちらを参考にしてください。

#コード

```delphi:p_kato_Holiday.pas
unit p_kato_Holiday;

interface

function SpecialHolidayGetFromHTTP:boolean; // 祝祭日ファイルを更新する ファイルがない場合には必ず実行すること
function IsSpecialHoliday(ADate: TDate; var AName: string): Boolean; // ADateが祝祭日かを返す AName:祝祭日名

implementation

uses
  System.Classes, System.SysUtils, System.Net.HttpClient,
  Generics.Collections;

type
  // 休日祭日のテーブル構造
  TSpecialHoliday=record
    Date:TDate;  // 日付
    Name:string; // 祝日名
  end;

var
  SpecialHolidays:TList<TSpecialHoliday>; // 休日祭日のテーブル 起動時に生成され終了時に破棄される

const
  SpecialHolidayFile='SpecialHoliday.txt'; // 祝日を保存するファイル 特殊フォルダにする場合は変数にしてinitialization時に取得するように変更する

const
  CAOURL='https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv';
  // https://www.data.go.jp/data/dataset/cao_20190522_0002 内閣府データカタログサイトより取得

function GetURLAsString(const AURL: string): string; // URLで指定したファイルの内容を返す
(*
　System.Net.HttpClient.THTTPClient
　http://docwiki.embarcadero.com/Libraries/ja/System.Net.HttpClient.THTTPClient
　山本隆の開発日誌 TNetHTTPRequest/TNetHTTPClientでWebサーバーにアクセスする
　https://www.gesource.jp/weblog/?p=7090
*)
var
  ResponseContent: TMemoryStream;
  HTTPClient:THttpClient;
  st:TStringList;
begin
  Result:='';
  HTTPClient:=THttpClient.Create;
  HTTPClient.AllowCookies := True;
  ResponseContent := TMemoryStream.Create;
  st:=TStringList.Create;
  try
    HTTPClient.Get(AURL, ResponseContent);
    st.LoadFromStream(ResponseContent,TEncoding.GetEncoding('Shift_JIS')); // 2020/12/01現在データはSJIS
    Result:=st.Text;
  finally
    st.Free;
    ResponseContent.Free;
    HTTPClient.Free;
  end;
end;

function StrToDate(s:string;var ADate:TDate):boolean;// YYYY/MM/DD を TDateに変換 成功したらTrueを返す
var
  dt:TStringList;
  y,m,d:word;
begin
  Result:=False;
  dt:=TStringList.Create;
  dt.Delimiter:='/';
  dt.StrictDelimiter:=True;
  dt.DelimitedText:=s;
  if dt.Count=3 then begin
    y:=StrToIntDef(dt[0],0);
    m:=StrToIntDef(dt[1],0);
    d:=StrToIntDef(dt[2],0);
    try
      ADate:=EncodeDate(y,m,d);
      Result:=True;
    finally
    end;
  end;
  dt.Free;
end;

function SpecialHolidayGet(AStringList:TStringList):boolean; // 祭日休日情報の取得
var
  i:integer;
  st:TStringList;
  sd:TSpecialHoliday;
begin
  SpecialHolidays.Clear;
  Result:=True;
  if AStringList.Count>1 then begin
    st:=TStringList.Create;
    st.Delimiter:=',';
    st.StrictDelimiter:=True;
    for i:=1 to AStringList.Count-1 do begin
      st.DelimitedText:=AStringList[i];
      if st.Count=2 then begin
        if StrToDate(st[0],sd.Date) then begin // YYYY/MM/DD を TDateに変換
          sd.Name:=st[1];
          SpecialHolidays.Add(sd);
        end else begin
          Result:=False;
        end;
      end;
    end;
    st.Free;
  end;
end;

function SpecialHolidayGetFromHTTP:boolean; // ファイルを保存できたかを確認する
var
  s:string;
  st:TStringList;
begin
  Result:=False;
  s:=GetURLAsString(CAOURL);
  if s<>'' then begin
    st:=TStringList.Create;
    try
      st.Text:=s;
      st.SaveToFile(SpecialHolidayFile,TEncoding.UTF8);
      if s<>'' then begin
        Result:=SpecialHolidayGet(st); // 祭日休日情報の取得
      end;
    finally
      st.Free;
    end;
  end;
end;

function SpecialHolidayGetFromFile:boolean; // ファイルを保存できたかを確認する
var
  st:TStringList;
begin
  Result:=False;
  if FileExists(SpecialHolidayFile) then begin
    st:=TStringList.Create;
    try
      st.LoadFromFile(SpecialHolidayFile,TEncoding.UTF8);
      if st.Text<>'' then begin
        Result:=SpecialHolidayGet(st); // 祭日休日情報の取得
      end;
    finally
      st.Free;
    end;
  end;
end;

//

function IsSpecialHoliday(ADate: TDate; var AName: string): Boolean;
var
  i:integer;
begin
  for i:=0 to SpecialHolidays.Count-1 do begin
    if SPecialHolidays[i].Date=ADate then begin
      AName :=SPecialHolidays[i].Name;
      Result:=True;
      exit;
    end;
  end;
  AName :='';
  Result:=False;
end;

initialization
  SpecialHolidays:=TList<TSpecialHoliday>.Create; // 休日祭日配列を生成
  SpecialHolidayGetFromFile;                      // 保存されていれば休日祭日配列ファイルを読み込む
finalization
  SpecialHolidays.Free;                           // 休日祭日配列を破棄
end.
```

#使い方
利用するフォームで
**uses
  p_kato_Holidays;**
を記述し，休日祭日を取得してファイルに保存するときに
  **SpecialHolidayGetFromHTTP;**
を実行します。ファイルをまだ持っていないときには必ず実行するようにしてください。
  **IsSpecialHoliday(ADate: TDate; var AName: string): Boolean;**
　で**ADate**が祝日休日かどうかを返します。祝日休日なら**AName**に**祝日祭日名**が入ります。振替休日のときは**休日**が入ります。
　祝日でも休日でもないなら，ANameには**空白**が入ります。

皆様のお役に立てば幸いです。

#おまけ
カレンダーコンポーネントの文字色を設定するには
@igy様の [カレンダーコンポーネントの文字色を設定 https://qiita.com/igy/items/54867fffdb6dac19fa0b](https://qiita.com/igy/items/54867fffdb6dac19fa0b) にその方法が書かれています。ぜひご確認ください。

#参考資料
この記事を書くために参考にした資料で本文でご紹介できなかったものは以下の通りです。
　08_THttpClient または TNetHttpClient を使用する接続処理 Mr.XRAY
　http://mrxray.on.coocan.jp/Delphi/plSamples/772_Indy_HTTPGet.htm#08
　山本隆の開発日誌 TNetHTTPRequest/TNetHTTPClientでWebサーバーにアクセスする
　https://www.gesource.jp/weblog/?p=7090

#謝辞
deco(@ht_deco)様 HOSOKAWA(@pik)様 XRAY様 山本隆様 igy様 皆様のおかげでこの原稿を書くことができました。ありがとうございます。