自分が欲しいから作った

https://holidays.plasticheart.info/

今から5年ほど前に業務上こういうのがあると嬉しいというもののひとつに日本の休日がわかるツールというものがあった。当時はプロジェクトの立ち上げ時期で、土日のどちらかも働いているような状況だったので、構想を練ってコードも途中まで書いたはいいものの、結局頓挫してしまった。

当時は仕事で使い始めたこともありC#で実装をしていたのだが、個人的にはあまり好きになれない言語であったし、かといって経験の長いPythonで実装するのも長期的な保守の面からすると適してないように思えた。

それから数年経って昨年にはRustも書けるようになり、その堅牢さ1に惹かれたのもあり、改めての挑戦として取り組んでみた。なにより自分が欲しかったからであり、他人の問題意識にフリーライドしないので、モチベーションが保てるし、自分が使えるツールが増えることにも繋がるので非常によろしい。

コードはほぼ100%Rustだが補助ツールとしてPythonも使っている。Pythonはちょっとしたところで取り回しがいいし2、ネットのみんなが叩く割には素直な挙動をしているので、データの整形や簡単な計算では大いに活用できる。

日本の休日の課題

日本の休日APIを作り始めて直面した難しいところのひとつは日本の休日だ。ちょっと思いついたところを書き出すだけでもこれだけある。

  • 内閣府からは確定分として来年末までの祝日が公表される
    • 2年以上先は未確定である
  • 毎年の不確定要素がある
    • 春分の日 (3月20~3月21日)
    • 秋分の日 (9月22~9月24日)
  • 法律によって祝日が発生する
    • 皇太子の結婚の儀の日は祝日になる
    • 天皇の即位の日と大喪の礼の日は祝日になる
    • オリンピック特別法によって祝日が移動することがある

法律による祝日は突発的で対応に追われがちだが、政府の発表やそのニュースを注視していればあまり問題にはならない。むしろ一番の課題となりうるのは、天体の動きに依存する春分の日と秋分の日だ。これは国立天文台が毎年観測に基づいて春分点と秋分点を決めており、それが祝日として採用される。

単純に考えれば毎年春分の日と秋分の日は来るのだから、平均的な経過時間を加算すればいいと思うかもしれないが、天体の1年と我々が普段使っている太陽暦の1年は長さが異なることから始まって、閏年による補正や、地球の移動速度がだんだん遅くなってきていることなど、算出にあたってはいくつもの要因があるので、まあうまくいかない。この部分の解決に時間の半分以上を費やした。もしもっと簡単に、精度が日単位でいいのであれば巷にはExcelでも使える計算式が転がっているのでそれを採用するといいだろう。

しかし、そもそも論としてこの問題を回避するためにGoogleカレンダー等の信頼できる情報源を頼りにすることもできる。ただスクレイピングの類ではあるので、Googleカレンダーの仕様変更によって七夕やクリスマスといったイベントが祝日として抽出されることがあるし、外部サービスに依存しているサービスというものはコアロジックが外にあるので共倒れのリスクが大きいところが難点でもある。

なぜ既存の休日APIではダメなのか?

すでにいくつか日本の休日APIは存在するが、それぞれに採用をためらう理由がある。少なくとも私が望む水準にはない。

Googleカレンダーから取得しているもの

これは前項で取り上げた通りで、コアロジックが外部にあるので共倒れのリスクを常に孕んでいる。Googleカレンダーが終了するとは思えないが、そうでなくともプラットフォームの影響を強く受ける。ただ、Googleカレンダーを整形されたフォーマットとして取得できると考えれば悪くない選択と言える。

今年と来年にしか対応していないもの

内閣府の発表がない限りは断定できないのだが、現行法に基づいて2年以上先の暦を取得したい要求が生まれうることは容易に想像できる。国立天文台のQ&Aコーナーでも何年後かの春分の日・秋分の日はわかるの?という質問があることからも需要はありそうだ。2050年1月1日は元日であるにも関わらず「わかりません」はあまりにもナンセンスだ。

今回作った国民の休日APIは2年以上先の日付に対しても現行法に準拠して取得できるようになっている。もちろん保証はされていないので、確らしさというプロパティを設けて、"yes""maybe" で返却するようにしている。

無償と無限は違う

日本の休日APIということで、間違っていたら大事に繋がりかねない話でもある。なので基本的に無償で無保証での提供とした。そもそも自分が欲しいから作ったのであって、よければほかの人も使ってみてくださいという考えだ。しかし、仕組みを作って動かしているからにはそれなりのコストがかかっているわけで、無限にやりたい放題に使われても困る。

そこでまずはメールアドレスを担保にユーザー登録させようとしたが、メールアドレスには2つの問題がある:

  • メールアドレスは個人情報として扱うのが妥当である。すなわち個人情報保護法の適用対象となり、流出した場合にはしかるべき機関に報告の義務がある。
  • いわゆる捨てアドサービスで取得したメールアドレスを登録されることによって、担保が意味をなさなくなる。検証機能が必要になる上にGmailやYahoo!メールを使われたところで同じ話である。

結果的に個人情報を扱うのはリスクが大きいと判断して、GmailアカウントのOAuth2認証を使うことにした。その上でOpenIDを取得すればメールアドレスを扱うことなく一意なIDを取得できるのでユーザー管理も簡単であるし、捨てアドサービスのように機械的な取得が困難ではあるので、再現なく使い倒される心配はなさそうであると考えた。

また、「クレジット」と呼ばれる自動回復する単位でレートリミットを設けた。現在はAPIで取得した日数だけこのクレジットを消費するようになっている。コストがかからないような設計にはなっているものの、それでも想定外の利用をされると困る状況は発生する。わずか数人の悪質なユーザーのために全員が割を食うことは避けたかったので、性善説を採用することはできなかった。

開発中には「リクエストが多い時間帯へのリクエストは多くのクレジットを消費する」という少し変わったアルゴリズムを思いついたのだが、リクエスト側がクレジット数の消費量を予測できないことと、そもそもその統計情報を保存するための時間と金銭的なコストがかかることが予想されたので今回は見送った3

ホビー利用目的にした

このAPIは原則としてホビー利用目的しか認めていない。業務ロジックに組み込まれて事故った挙句に怒りのメッセージを頂戴しても困るからだ。昔2chの投稿で「ネットでフリーで公開していたツールを予告なしに停止したら、そのツールが業務に組み込まれている会社から怒りのメールが送られてきた」という主旨の投稿を見たことがあり、往々にして起こり得る事態ではあるので、あらかじめそういったリスクは排除している。

Footnotes

  1. 主にはOption<T>とResult<T, E>でもたらされる恩恵によるもの。

  2. でもそろそろflaskとrequestsは標準パッケージに採用してほしい。

  3. 趣味の範囲では100円すら高いのだ。