コードは 1 行も変えていないのに、3 月の第 2 日曜からずれる
アメリカ東部のサーバーで毎朝 9 時に走るバッチがある。何ヶ月も正常に動いていて、3 月の 第 2 日曜の朝から、毎朝 10 時に走るようになる。コードは 1 行も変わっていない。
原因はたいていこの形をしている:
next = prev + 24 * 60 * 60 * 1000; // 「1 日後」
アメリカ東部は 3 月の第 2 日曜 02:00 に夏時間へ切り替わる。時計が 1 時間進むので、その市民日は 23 時間しかない。「前回 + 86400 秒」は物理としては正確に 24 時間後だが、壁時計で読むと 1 時間ずれる。そして秋に時計が戻るまで、ずれたままになる。
ここで大事なのは「バグの直し方」ではない。「1 日」という言葉が二つの別の量を指している、という事実のほうだ。
- 市民日の 1 日——カレンダーの 1 マス。壁時計に貼り付き、切替日は 23 時間や 25 時間になる
- 経過時間の 24 時間——物理の 86,400 秒。壁時計がどう動こうと長さは変わらない
どちらも正しい「1 日」で、要件によって使い分ける。「毎朝 9 時の打刻」は前者、「充電開始から 24 時間ごとの点検」は後者。事故は、この二つを同じ型で持つことから始まる。
Kairos では、二つは別のリテラルである
Kairos は幅(どれだけの長さか)を書くリテラルの段階で、この二つを分けている。1d は市民日、 24h は経過時間。同じ「毎日 9 時から」を両方で書いて、夏時間切替(2026-03-08)を跨いで評価してみる:
premise NY {
calendar-system: Gregorian
tz: "America/New_York"
wkst: Mon
}
@NY
everyInstant |> strideBy(1d, from: 2026-03-06T09:00)
everyInstant |> strideBy(24h, from: 2026-03-06T09:00)
# 式 1(4 件) 2026-03-06T09:00 2026-03-07T09:00 2026-03-08T09:00 2026-03-09T09:00 # 式 2(4 件) 2026-03-06T09:00 2026-03-07T09:00 2026-03-08T10:00 2026-03-09T10:00
1d で刻んだ式 1 は毎日 09:00 に貼り付く。24h で刻んだ式 2 は、切替日から 10:00 にずれて、ずれたまま進む——冒頭のバッチの再現である。どちらかが間違っているのではない。二つは違う質問で、言語が違う答えを返しているだけだ。
ちなみに、二つを混ぜた幅を書こうとすると:
字句エラー(8:26): 市民時と経過時間の幅は混合できない: 1d12h(ADR-28)
パースの段階で拒否される。「1 日と 12 時間」は、市民日の伸び縮みを知らないと長さが定まらない量で、書けてしまうと切替日の解釈が実装依存になる。だから最初から書けない。
存在しない時刻を書いたら
もうひとつ、切替日には「存在しない壁時計」がある。02:00〜03:00 は時計が飛ぶので、 02:30 という朝はこの日には来ない。それをデータとして名指しで書くと:
x = [2026-03-08T02:30] covering: 2026..2026 x
存在しない時刻: 2026-03-08T02:30(tz "America/New_York" の DST の隙間に落ちる——実在の壁時計で書く。ADR-33)
黙って 03:30 に前進もしないし、黙って捨てもしない。書き手が特定の壁時計を名指した以上、それが実在しないのはデータの誤りだから、誤りとして返す。一方で、規則から導出される点が隙間に落ちた場合(「毎日 02:30」のような式の切替日ぶん)は「隙間明けの最初の瞬間」という規約で決定的に解決される。名指しは厳格に、導出は規約で——書いた通りにしか動かず、書いていないものは動かない、の時刻版である。
日本には夏時間がないから関係ない?
半分だけ正しい。日本の壁時計は年中 UTC+9 で、市民日はいつも 24 時間。だが「サーバーは UTC、業務は日本の朝 9 時」という構成は普通にあり、そこでは毎日、市民日と経過時間の変換を 誰かがやっている。cron の 0 9 * * * は「サーバーの 9 時」であって、サーバーを引っ越した瞬間に意味が変わる——これも「1 日」の混同と同じ型の、どこの壁時計かの混同だ。
Kairos の前提(premise)が tz: "Asia/Tokyo" の宣言を必須にしているのはこのためで、どのスケジュール定義も「どこの壁時計で読むか」を言わずには書けない。定義がサーバーを引っ越しても、意味は definition 側に固定されている。
——第 3 弾(cron の「13 日の金曜日」)で書いた通り、cron の地雷は表現力の不足ではなく言葉の多義性から来る。「1 日」が二つの量を指す。「9 時」がどこの 9 時か言っていない。言語にできる仕事は、多義な言葉を別の書き方に分けて、混同を書けなくすることだ。データの鮮度(第 5 弾)と同じで、事故は能力の不足ではなく、言い分けの不足から起きる。

コメント