祝日データはどこから来るのか——式は静的、データは実行時

Kairos

祝日リストは、いま、どこにあるか

スケジュール計算には祝日データが要る。ではその祝日リストは、いまどこにあるだろうか。

コードの定数配列。データベースの holidays テーブル。設定ファイル。誰かが毎年更新しているExcel。だいたいこのどれかで、そして本体のロジックとデータ置き場のあいだには、取ってきて、解析して、形式を検査して、変数に詰めるコードが挟まっている。いわゆるグルーコードである。

このグルーは、スケジュールの正しさの一部を確実に担っているのに、たいていレビューされない。「DB から祝日を読むだけの部分」に真剣なレビューをした記憶が、私にはあまりない。事故はよくそこで起きる。年末に翌年分を入れ忘れる(データの問題)、入れたが 1 行ずれて解析される(グルーの問題)、誰も気づかない(検査がない問題)。

第一形態——データを定義に直書きする

Kairos では、いちばん素朴な形はデータを定義に直書きすることだ:

holidays = [2026-01-01, 2026-01-12, 2026-02-11, 2026-02-23, 2026-03-20,
            2026-04-29, 2026-05-03, 2026-05-04, 2026-05-05, 2026-05-06,
            2026-07-20, 2026-08-11, 2026-09-21, 2026-09-22, 2026-09-23,
            2026-10-12, 2026-11-03, 2026-11-23] covering: 2026..2026

式とデータが一枚に収まり、diff も取れるしレビューもできる(営業日の計算に混ぜる形は第 3 弾・第 12 弾で書いたとおり)。covering: 2026..2026 は「2026 年の範囲ではこの列で完全」という主張で、データが尽きた先の評価には警告が併走する(第 5 弾で書いた「祝日テーブルの腐り」の検出装置)。

▶ Playground で開く

ただし直書きには寿命の問題がある。式の寿命とデータの寿命が違うのだ。「月初の営業日」という式は何年も変わらないのに、祝日データは毎年変わる。データの更新のたびに、変わらないはずの定義ファイルを書き換えてコミットすることになる。

external——「ここに、実行時に、データが差し込まれる」

そこで Kairos には、データの取り込み口を宣言だけする形がある:

premise HRDB {
  calendar-system: Gregorian
  tz: "Asia/Tokyo"
  wkst: Mon
  source: "hr-db/holidays"
  holidays = external(kind: dates)
}

external(kind: dates) は「この束縛には、評価のたびに、外から日付の列が差し込まれる」という宣言である。式は静的なままレビューでき、データは実行時に最新が解決される——式は静的、データは実行時。人事 DB から取るのか、API から取るのか、ファイルから読むのかは実装系の自由で、言語は差し込み口の形だけを決める。

CLI なら JSON ファイルで渡せる:

$ kairos list teiki.kairos --supply holidays.json --from 2026-09-01 --to 2027-01-01
2026-09-21
2026-09-22
2026-09-23
2026-10-12
2026-11-03
2026-11-23
# 被覆サマリ
#   HRDB.holidays covering 2026-01-01..2026-12-31 asof 2026-08-14 残走路 0 日

答えの末尾を見てほしい。このデータは 8/14 時点の知識で、2026 年末までで尽きる——鮮度と賞味期限が、計算結果そのものに併走している。「その祝日リスト、いつ時点のやつ?」という朝会の問いに、機械が答える。

契約——検査を書かなくていいのではなく、書いてあるから書かなくていい

external が受け取るデータには契約がある。covering(どの範囲について完全か)と asof (いつ時点の知識か)を必ず運ぶこと。そして中身は、直書きのテーブルと同じ検査を通ること。

たとえば供給側のバグで、covering が 2026 年なのに 2027 年の日付が 1 点混ざったら:

契約違反: 解決値の点が covering の外——external HRDB.holidays: 2027-02-11
(列の全要素は covering に包含——ADR-37 判断 1)

黙って捨てもしないし、黙って covering を広げもしない。データが自分の主張と矛盾していることをその場で止める。タイポで実在しない日付が混ざったら:

契約違反: 実在しない日付——external HRDB.holidays: 2026-02-30
(黙ったロールオーバーの封止=ADR-43 の字句検査の解決値向け再執行。ADR-46)

2/30 が黙って 3/2 に化ける(多くの日付ライブラリの既定動作である)ことはない。そもそも供給が来なかったら:

供給エラー: 解決子がない——external HRDB.holidays(source: "hr-db/holidays")は解決できない

冒頭のグルーコードに本当は書くべきだった検査が、ぜんぶ言語側に最初から入っている。消費側が書くのは差し込みだけ——検査の写経が要らない。

この口は、もう本番で回っている

余談だが、この差し込み口は絵に描いた仕様ではない。external で祝日級のデータを実行時解決し、定期実行の発火を決めているシステムが、すでに本番で動いている。定義はテキストのまま数か月不変で、データ側だけが更新され、被覆サマリの残走路が運用の監視項目に入っている——「式は静的、データは実行時」は、その運用の形をそのまま言語に写したものだ。

まとめ

  • 祝日データとスケジュール式のあいだのグルーコード(取得・解析・検査)は、正しさを担うのに レビューされない常連である
  • Kairos は取り込み口を宣言にした——external(kind: dates)式は静的にレビューでき、 データは実行時に最新が差し込まれる
  • データは covering(完全性の範囲)と asof(知識の時点)を必ず運ぶ——鮮度が答えに併走し、 矛盾は差し込んだ瞬間に契約違反で止まる
  • 検査を「書かなくていい」のではない。言語に書いてあるから、書かなくていい

Kairos は設計中のスケジュール定義言語です。仕様と参照実装はGitHub で公開しています。

コメント

タイトルとURLをコピーしました