09 — DST と幅の規約——「1 日後」と「24 時間後」の運用検証
発端(2026-08-06): 運用事故の定番——「毎朝 9 時のバッチが、春の DST 切替から毎朝 10 時に
走る。コードは 1 行も変わっていない」(実装は prev + 86400s の類)。原因は 「1 日」という
言葉が二つの別の量を指すこと。この事故の型を、仕様に散在する DST 関連の裁定(ADR-11/12・
ADR-28・ADR-31 改訂 2・ADR-33)へ一枚で対応づけ、実行検証で固定する。営業時間の文脈での
二意味論(hour 序数 vs 壁時計)は 06 §6.1 が既検証——本ページは
その純粋な時間モデル側の総集編で、外向けの説明にそのまま使える最小例を集める。
9.1 事故の型 → 規約の対応
| 事故・混同の型 | 言語の答え | 正本 |
|---|---|---|
「1 日後」を +86400s で実装して春に 1 時間ずれる |
1d(市民日・壁時計保存)と 24h(経過時間)は別のリテラル |
ADR-11/12(幅の規約) |
| 「1 日と 12 時間」のような曖昧な幅 | 1d12h は字句エラー(市民時と経過時間の混合禁止) |
ADR-28 |
| 存在しない壁時計(隙間)をデータに書いた | 時刻リテラルは明示エラー(黙って前進も破棄もしない) | ADR-33 判断 4 |
| 規則の導出点が隙間・重複に落ちた | 隙間=隙間明けの最初の瞬間・重複=最初の出現(決定的な規約) | ADR-31 改訂 2 |
cron の 0 9 * * * が「どこの 9 時」か言っていない |
premise の tz: は宣言必須——定義がサーバーを移っても意味は不変 |
ADR-33/35(射影パラメータモデル) |
9.2 実行検証(doctest)
舞台は America/New_York。2026 年の春切替は 3/8(02:00→03:00・23 時間の市民日)、秋切替は 11/1(02:00→01:00・25 時間の市民日)。
1d=市民日で刻む——切替日を跨いでも壁時計 09:00 に貼り付く:
# eval: 2026-03-06..2026-03-10 tz: America/New_York
premise NY {
calendar-system: Gregorian
tz: "America/New_York"
wkst: Mon
}
@NY
everyInstant |> strideBy(1d, from: 2026-03-06T09:00)
#=> 2026-03-06T09:00 2026-03-07T09:00 2026-03-08T09:00 2026-03-09T09:00
24h=経過時間で刻む——切替日から壁時計が 1 時間ずれ、ずれたままになる(冒頭の
事故の再現。どちらかが誤りなのではなく、二つは違う質問への違う答え):
# eval: 2026-03-06..2026-03-10 tz: America/New_York
premise NY {
calendar-system: Gregorian
tz: "America/New_York"
wkst: Mon
}
@NY
everyInstant |> strideBy(24h, from: 2026-03-06T09:00)
#=> 2026-03-06T09:00 2026-03-07T09:00 2026-03-08T10:00 2026-03-09T10:00
導出点が隙間に落ちる——anchor の壁時計 02:30 は 3/8 に存在しない。導出点は「隙間明けの 最初の瞬間」(03:00)へ寄り、翌日は 02:30 に戻る(ずれが伝播しない=壁時計ラベル読み。 ADR-31 改訂 2):
# eval: 2026-03-07..2026-03-10 tz: America/New_York
premise NY {
calendar-system: Gregorian
tz: "America/New_York"
wkst: Mon
}
@NY
everyInstant |> strideBy(1d, from: 2026-03-07T02:30)
#=> 2026-03-07T02:30 2026-03-08T03:00 2026-03-09T02:30
導出点が重複に落ちる——秋切替の 01:30 は二度ある。表示は同じ 01:30 だが、着地は 最初の出現(EDT 側=05:30Z。機械可読の points で確認済み・11/1 から翌日までの実経過は 25 時間):
# eval: 2026-10-31..2026-11-03 tz: America/New_York
premise NY {
calendar-system: Gregorian
tz: "America/New_York"
wkst: Mon
}
@NY
everyInstant |> strideBy(1d, from: 2026-10-31T01:30)
#=> 2026-10-31T01:30 2026-11-01T01:30 2026-11-02T01:30
9.3 名指しは厳格・導出は規約
上の 9.2 で導出点は規約で解決された。対照的に、書き手が名指しした壁時計が実在しない
場合は明示エラーになる(以下は提示用の実測——エラーの機械検査は impl/test/timezone.test.ts
〈隙間/重複リテラル〉・lexical-limits.test.ts〈字句〉が担う):
premise NY {
calendar-system: Gregorian
tz: "America/New_York"
wkst: Mon
}
@NY
x = [2026-03-08T02:30] covering: 2026..2026
x
存在しない時刻: 2026-03-08T02:30(tz "America/New_York" の DST の隙間に落ちる——実在の壁時計で書く。ADR-33)
混合幅はさらに手前——字句の段階で止まる(8 行目 26 桁=1d12h の位置):
premise NY {
calendar-system: Gregorian
tz: "America/New_York"
wkst: Mon
}
@NY
everyInstant |> strideBy(1d12h, from: 2026-03-06T09:00)
字句エラー(8:26): 市民時と経過時間の幅は混合できない: 1d12h(ADR-28)
名指しは特定の壁時計の主張だから、実在しなければデータの誤りとして返す。導出は主張して いないから、規約で決定的に埋める。「書いた通りにしか動かず、書いていないものは黙って動かない」 の時刻版で、この非対称が運用の型になる。
9.4 運用の型
- どちらの幅かは要件が決める: 「毎朝 9 時の打刻」=
1d(壁時計)・「稼働 24 時間ごとの 点検」=24h(経過)。混同した式は書けない(字句で止まる)。 - DST の無い国でも他人事ではない: 「サーバーは UTC・業務は東京の朝 9 時」の構成では、
市民日 ↔ 経過時間の変換が毎日起きている。
tz:の宣言必須は「どこの壁時計か」を定義側に 固定する——サーバーを移しても意味が変わらない。 - 秋の重複は静かに通る: 表示だけ見ると何も起きていない(01:30 は 01:30)。監査・突合が
要るなら CLI の
--json(points=epoch ms)で実時刻を見る。
判定: 書ける——二つの「1 日」はどちらも確定語彙で書け、しかも混同が書けないように 分離されている。補完機構の要求なし。
(関連: 営業時間文脈の二意味論と切替日の実測は 06 §6.1・幅規約の 正本は ADR-11/12・時刻付き anchor/from: の壁時計ラベル読みは ADR-31 改訂 2・隙間/重複 リテラルの明示エラーは ADR-33)