発端
「月末の 3 営業日前に締め処理の通知を出したい」。よくある要件。cron で書こうとして、手が止まる。
0 9 26 * * # 26 日……ではない。月末から数えて 3 営業日前
書けない。cron が持っているのは「毎月 26 日」であって「月末の 3 営業日前」ではない。タスクスケジューラも同じ。「営業日」という概念が、この種のツールにはそもそも存在しない。
で、どうするか。アプリ側に if 文を書く。祝日テーブルをコードに埋める。動く。数年は動く。
そして 2021 年、海の日が移動する。オリンピック特例。祝日テーブルは誰も更新していない。締め通知は祝日に堂々と発火し、誰も読まない。
このパターンを何度か踏んだ結果、スケジュール定義言語を作ることにした。名前は Kairos。
既存の道具はどこまで行けるか
先に整理。世の中の道具は大きく三段ある。
- cron / タスクスケジューラ — 「毎月 25 日 9 時」まで。営業日・祝日の概念なし。
- RRULE (iCalendar) — 「第 2 月曜」までは書ける。「営業日でなければ前日に倒す」が書けない。
- 営業日カレンダー付きの製品 — 書ける。ただし定義は GUI の中。バージョン管理できず、テストも書けず、別システムに移植もできない。
欲しいのは「営業日を含むスケジュールを、テキストで、宣言的に、検証可能に書ける」層。そこが空いている。
Kairos で書くとこうなる
冒頭の要件。
@JP monthEnd |> roll(Preceding, on: bizDay) |> shift(-3, unit: bizDay)
読み下し: 月末を採り、営業日でなければ前営業日へ寄せ、そこから 3 営業日戻す。@JP は前提の宣言(日本のカレンダー・タイムゾーン・週の始まり)。評価するとこうなる。
2026-08-26 2026-09-25 2026-10-27 2026-11-25
9 月が 25 日になっている点に注目。9 月末はシルバーウィーク明けで、9/21〜23 の三連休を営業日カウントが正しく飛び越えている。if 文でこれを書いた経験があるなら、この 4 行の出力の意味が分かるはず。
給料日も一行。
@JP everyDay |> within(month) |> nth(25) |> roll(Preceding, on: bizDay)
毎月 25 日、休日なら前営業日。出力:
2026-07-24 # 7/25 は土曜 → 前営業日の金曜 2026-08-25 2026-09-25 2026-10-23 # 10/25 は日曜 → 金曜
極めつけは振替休日。「日曜の祝日は翌営業日に振り替え」「祝日に挟まれた平日は休日」——祝日法の条文がほぼそのまま式になる。
@JP nonHoliday = everyDay \ statutory substitutes = statutory |> filter(d => weekday(d) == Sun) |> roll(Following, on: nonHoliday) sandwiched = ((statutory |> shift(+1, unit: day)) & (statutory |> shift(-1, unit: day))) \ statutory substitutes | sandwiched
statutory(法定祝日のリスト)だけを入力に、2026 年で評価した結果。
2026-05-06 # 憲法記念日 5/3 が日曜 → 連休を飛び越えて 5/6 に振替 2026-09-22 # 敬老の日と秋分の日に挟まれた火曜 → 国民の休日
振替先が 5/4 でも 5/5 でもなく 5/6 なのは、連休中の振替が自動で後ろへ送られるから。この導出をハードコードせず規則から出せるのが要点。来年の祝日データを差し替えれば、来年の振替休日が出てくる。
一番作りたかったもの: 「黙って壊れない」
営業日計算の本当の敵は、書きにくさではない。祝日データの鮮度。
祝日は法律と官報で決まる。つまり計算では出せず、データをもらってくるしかない。そしてデータには必ず「ここまでしか知らない」という端がある。問題はその端に達したとき。大半のシステムは黙って壊れる。祝日テーブルが 2026 年で尽きていても、2027 年のスケジュールを平然と計算する。祝日を踏んでいても誰も気づかない。
Kairos では、データに**有効範囲(covering)**を宣言する。範囲の外に踏み出した計算には、結果と一緒に「この区間はデータの範囲外」という注釈が並走する。値は出る。ただし観測できる。監視の仕組みに「祝日データの残りが 75 日を切った」と警告させることもできる。
「壊れるな」ではなく「壊れるときは観測できる形で壊れろ」。運用でスケジュールに裏切られてきた人間の設計だと思ってもらえれば。
自作言語に自分が騙された話
作っている間の失敗談を二つ。
その 1。処理系のテストを書いていて、日付リテラル 2026-02-30 をうっかり書いた。エラーにならない。おかしいと思って調べたら、処理系が黙って 3 月 2 日に繰り上げて受理していた。存在しない日付を、作った本人が書き、作った処理系が忖度する。現在は字句エラーに直してある。
字句エラー: 実在しない日付: 2026-02-30(月は 01..12・日は月と閏年規則の実在日のみ)
その 2。「次の金曜へ進める」という演算子の初期実装が、週の後半に評価すると過去の金曜を返してきた。「同じ週の金曜」を先に探す仕様にしていたせい。次と言ったのに過去が返る。タイムマシンの実装予定はない。現在は前方にしか進まない定義に直した。
どちらも仕様の穴がテストで割れた例。この言語は現在、代表例・祝日一式・二十四節気・旧暦まで含めて 469 本のテストで仕様と実装を突き合わせている。二十四節気は国立天文台の暦要項と照合済み。旧暦の閏月がデータから機械的に出てきたときは少し感動した。
できないこと
意図的に書けなくしてあるものもある。
- 発火そのもの — Kairos が出すのは「時点の列」まで。実際にジョブを起動する層(発報層)は別。言語は計算に徹する。
- 「前回完了から 5 時間ごと」 — 実行結果が次の入力に戻るフィードバックは書けない。ただし「与えられた時点から次の発火を計算する」は普通に書ける。この線引きの理屈はサイトの仕様書 §7.7 に書いた。
- 祝日を当てること — 未来の祝日は法律が決める。言語にできるのは、データの鮮度を観測可能にするところまで。
現状
リリース候補(RC5)。1.0 は 2026 年 9 月を目安に準備中。仕様・リファレンス・実装はすべて公開している。
- サイト: https://kairos-lang.org/
- リポジトリ: https://github.com/azathothx/kairos-lang
- 言語検定の教材とサンプル問題も作り始めた: https://github.com/azathothx/kairos-lang-exam
次回はチュートリアル予定。給料日ルールをゼロから書く。

コメント