cron の「9-14」という数字は、どこから来たか
営業時間内だけ、毎正時に監視ジョブを回したい。cron で書くと、たいていこうなる:
0 9-14 * * 1-5 monitoring.sh
これで「平日の 9 時から 14 時まで毎正時」——最初は合っている。だが祝日に誤発火する。そこで monitoring.sh の先頭に祝日判定の if 文が入る。次に大納会(11:30 引け)の日に 12 時と 13 時の監視が「閉場後の市場」を叩いて警報を鳴らす。if 文がもう一つ増える。
数年後、誰かが訊く——「この 9-14 という数字、どこから来たんですか」。答えられる人はもういない。営業時間は取引所の事実なのに、それが cron の数字と、シェルスクリプトの if 文と、運用手順書の注記に分散して埋まっている。
営業時間は「実体の文化」として宣言する
Kairos では、営業時間はジョブの側ではなくカレンダー実体——取引所そのもの——の側に書く:
premise TSE {
calendar-system: Gregorian
tz: "Asia/Tokyo"
source: "example"
satSunC = everyDay |> filter(d => weekday(d) == Sat or weekday(d) == Sun)
holidays = [2026-01-01] covering: 2026..2026
nonWorking = satSunC | holidays
nine = chronos grid 1d anchor: 2026-01-01T09:00
three = chronos grid 1d anchor: 2026-01-01T15:00
halfDayCloses = [2026-01-06T11:30] covering: 2026..2026
sessionOpens = nine |> first
sessionCloses = (three |> first |> filter(t => not coincides(halfDayCloses, day, t))) | halfDayCloses
}
読み下すと: 開場は毎日 9 時(sessionOpens)。閉場は通常 15 時、ただし半日休テーブル halfDayCloses にある日はその時刻(この例では 1/6 が 11:30 引け)。休業日は土日と祝日(nonWorking)。半日休テーブル 1 本が「どの日が半日か」と「何時に引けるか」を兼ねる——日付のリストと時刻のリストを別々に保守して食い違う、という事故の形が最初から存在しない。
そして肝心のジョブ側は、1 行になる:
hourly |> filter(t => isOpen(t))
isOpen(t) は言語が実体の宣言から一律に導出する述語で、「t は営業セッション [開場, 閉場) のどれかに入っているか」を答える。実測:
2026-01-05T09:00 2026-01-05T10:00 2026-01-05T11:00 2026-01-05T12:00 2026-01-05T13:00 2026-01-05T14:00 2026-01-06T09:00 2026-01-06T10:00 2026-01-06T11:00 2026-01-07T09:00 2026-01-07T10:00 2026-01-07T11:00 2026-01-07T12:00 2026-01-07T13:00 2026-01-07T14:00
月曜(1/5)は 9 時から 14 時まで 6 点。半日休の火曜(1/6)は 9・10・11 時の 3 点だけ—— 11:30 に引けるので、12 時の正時はもう営業外。水曜(1/7)はまた 6 点に戻る。cron の 9-14 と 2 本の if 文でやっていたことの全部が、実体の宣言と filter 一段に畳まれた。
境界にも規約がある。開場の 9:00 ちょうどは営業中(含む)、閉場の 15:00 ちょうどは営業外(含まない)——半開区間。「15 時の正時は発火するのか」という、cron の 9-14 ではそもそも問えなかった問いに、言語が決定的に答える。
評価範囲を 2027 年に書き換えて再評価してみてほしい。次の節の話が、そのまま起きる。
半日休のデータが尽きた日に、何が起きるか
halfDayCloses の covering は 2026 年まで。では 2027 年 1 月の「営業時間内の毎正時」を評価すると何が出るか。「2027 年の半日休はまだ知らないのだから、ぜんぶ通常の 15 時引けとみなして動く」——多くのシステムはそう作られている。そして大納会の午後に、閉まった市場を監視し続ける。
Kairos の実測:
# ⚠ 範囲外 2027-01-04..2027-01-06(TSE.halfDayCloses covering 2026-01-01..2026-12-31) # ⚠ 範囲外 2027-01-04..2027-01-06(TSE.holidays covering 2026-01-01..2026-12-31) # 被覆サマリ # TSE.halfDayCloses covering 2026-01-01..2026-12-31 残走路 -5 日 # TSE.holidays covering 2026-01-01..2026-12-31 残走路 -5 日
発火点はゼロ。代わりに範囲外の註釈が併走する。halfDayCloses の covering が主張しているのは「2026 年の範囲では、半日休はこの列で完全」であって、2027 年については何も言っていない。すると 2027 年の各営業日が 15 時引けなのか 11:30 引けなのかは判定材料が欠けている——だから営業帯そのものが「未知」であり、黙って通常営業に延長しない。
「データが無いから通常営業」と「データが無いから未知」。前者は勝手な補完で、後者は正直な答えである。そして残走路 −5 日——「半日休データの賞味期限は 5 日前に切れている。2027 年分を入れよ」という運用信号が、評価結果そのものに機械可読で載っている。祝日テーブルの腐り(第 5 弾)・残走路の監視(第 8 弾)と同じ器が、営業「時間」の粒度でもそのまま働く。
片方だけ書いたら
ちなみに、開場列 sessionOpens だけ宣言して閉場列を書き忘れると:
sessionOpens/sessionCloses は対で宣言する——実体 TSE は sessionOpens だけ (片方だけは静的エラー・with 派生は継承込みで判定。ADR-41)
「閉場が書いてないなら 24 時間営業」とはみなさない。セッションは [開場, 閉場) の対で初めて定義できる——宣言していない営業時間を言語が勝手に補うことは、ここでもしない。
まとめ
- 営業時間は cron の数字でもジョブの if 文でもなく、実体の文化として宣言する——開閉の対・休業日・半日休テーブルが一箇所に集まり、
isOpen一語で使える - 半日休テーブル 1 本が「どの日か」と「何時までか」を兼ねる——リストの食い違いという事故の形が消える
- データが尽きた先の営業帯は「未知」——黙って通常営業に延長せず、註釈と残走路が「データを足せ」と機械可読に言ってくる
- 境界は言語の規約(開場は含む・閉場は含まない)——「15 時は発火するのか」に決定的に答えられる
Kairos は設計中のスケジュール定義言語です。仕様と参照実装は GitHub で公開しています。


コメント