両方の市場が開いている日だけ、決済を流したい
東京とニューヨーク、両方の市場が営業日のときだけ動かせる処理がある。クロス市場の決済、国際送金のカットオフ、グローバル拠点の合同バッチ。要件を言葉にするのは簡単だ——「両方が営業日の日」。
素朴には、二つの祝日リストを突き合わせればよさそうに見える。日本の 2/11 は建国記念の日、アメリカの 2/16 はワシントン誕生日、どちらかにあれば除外。Excel で年に一度作る「共通営業日カレンダー」は、たいていこの作り方をしている。
ところがこの突き合わせには、リストの中身より前に落とし穴がある。「同じ日付」とは何か、だ。
「同じ日」は、物理の時間軸のどこにもない
東京の 2 月 12 日は、JST の 0 時から 24 時までの区間である。ニューヨークの 2 月 12 日は、東部時間の 0 時から 24 時までの区間である。二つを一本の絶対時間軸に置くと、14 時間ずれた別々の区間になる。部分的には重なるが、一致はしない。
つまり「東京の 2/12 と NY の 2/12」を対応させる操作は、物理的な「同じ時刻」の操作ではない。日付というラベルの一致であって、瞬間の一致ではない。前回(第 7 弾)は「1 日後」という言葉が二つの量(市民日と 86,400 秒)を指している話を書いたが、今回も同じ型だ——「同じ日」という言葉が、同じ瞬間と同じ日付という二つの操作を指している。
素朴に積を取ると、止まる
Kairos で東京の営業日列 Tok.biz と NY の営業日列 NYk.biz を作り、素朴に積 & を取ってみる:
Tok.biz & NYk.biz
結合子 &: 両辺の整列が同一でない——点の等値所属が黙って空振りする形(ADR-36)。 入力=市民日 1d 位相 0 tz "Asia/Tokyo"・軸/右辺=市民日 1d 位相 0 tz "America/New_York"。 同じ点が意図なら snapTo で明示的に整合する(同じ所属〈日〉が意図なら coincides。ADR-38)
静的エラーで止まった。積 & は「同じ点」の集合演算で、東京の 2/12 の開始瞬間と NY の 2/12 の開始瞬間は等しくない——仮にこの式を通したら、答えは黙って空になる。空の結果は最悪の不具合である。何も落ちてこないだけで、エラーも警告も出ず、決済は静かに全部止まる。だからこの言語は「黙って空振りする形」を型で止めて、代わりの道具を 2 つ挙げてくる。
「同じ瞬間」で写すと、祝日が化けて出る
エラーが挙げた道具の一つ、snapTo(点をその瞬間が属する窓へ写す)を試すとどうなるか。東京の営業日列を NY の日窓に写して、NY 時間で表示する:
Tok.biz |> snapTo(day)
2026-02-09 2026-02-10 2026-02-11 2026-02-12
(このほか被覆サマリと、実体化の端に関する警告が 1 行併走するが、本題ではないので省いた。)
出力に 2 月 11 日——建国記念の日——が現れた。東京が休んでいる日が、東京の営業日の写像に入っている。バグではない。東京の 2/12(木・営業日)の朝 0 時は、ニューヨークではまだ 2/11 の午前 10 時だからだ。「同じ瞬間」で写すというのはそういう操作で、地球が丸いという事実がそのまま出力に出る。系統的に 1 日ずれ、しかもずれた結果が「相手の祝日」というそれらしい顔で並ぶ——目視のレビューでは、まず捕まらない。
「同じ日付」で写す——rebase
求めていた操作は「日付ラベルを保存して、相手の tz の同じ日付へ写す」。Kairos では rebase という 1 語がその宣言になる:
premise Tok = Gregorian with {
source: "cal-tokyo"; asof: 2026-01-15
tz: "Asia/Tokyo"
hol = [2026-02-11, 2026-02-23] covering: 2026..2026 # 建国記念の日・天皇誕生日
ss = everyDay |> filter(d => weekday(d) == Sat or weekday(d) == Sun)
biz = everyDay \ (ss | hol)
}
premise NYk = Gregorian with {
source: "cal-ny"; asof: 2026-01-15
tz: "America/New_York"
hol = [2026-02-16] covering: 2026..2026 # ワシントン誕生日
ss = everyDay |> filter(d => weekday(d) == Sat or weekday(d) == Sun)
biz = everyDay \ (ss | hol)
}
premise UNY { calendar-system: Gregorian; tz: "America/New_York"; wkst: Mon }
@UNY
(Tok.biz |> rebase(to: "America/New_York")) & NYk.biz
2026-02-09 2026-02-10 2026-02-12 2026-02-13 2026-02-17 2026-02-18 2026-02-19 2026-02-20 # 被覆サマリ # Tok.hol covering 2026-01-01..2026-12-31 asof 2026-01-15 残走路 313 日 # NYk.hol covering 2026-01-01..2026-12-31 asof 2026-01-15 残走路 314 日
2 週間ぶんの共通営業日、8 日。2/11 は東京の祝日で落ち、2/16 は NY の祝日で落ち、土日は両方で落ちる。rebase が「ラベル対応」の再整列を宣言したので、後段の積 & は元の意味のまま正しく重なる。定義はこの 20 行で全部で、祝日データの鮮度(asof)と賞味期限(残走路)まで答えに併走してくる——「Excel の共通営業日カレンダー、去年のままでは?」という朝会の不安ごと、機械可読になる。
& を snapTo 形に書き換えて再評価してみてほしい。建国記念の日が「共通営業日」に化けて出る瞬間が、目視レビューの気持ちである。
「同じ日付」の相手が、存在しないことがある
日付ラベルの対応という約束には、もう一つ端がある。サモアは 2011 年末に日付変更線の西側へ移った——Pacific/Apia の暦に 2011 年 12 月 30 日は存在しない(12/29 の翌日が 12/31)。その日付を含む列をサモアへ rebase すると:
rebase: 存在しない日付 2011-12-30(tz "Pacific/Apia" に無い——日付変更線の移動で消えた日。 ADR-33 判断 9 のデータ相対エラー・ADR-40 判断 3)
黙って 12/31 に置いたりはしない。「同じ日付」という約束に相手がいないなら、それは書き手が知るべき事実であって、処理系が丸める事実ではない——寄せたいなら、寄せ方(前へ? 後ろへ?)を書き手が明示する。暦は、思っているよりずっと凸凹である。凸凹を隠す道具は、凸凹に足を取られた日に沈黙する。
おまけ——この記事の例が、実装のバグを 1 件見つけた
白状すると、この記事の例を検証している途中で、参照実装の欠陥を 1 件見つけた。文書には「snapTo は系統的に 1 日ずれる」と書いてあるのに、特定の条件(実行タイムゾーンが対象より東にあるときの、紀元近くの端の点)で実装がエラーを出す帯があった。仕様・実装・文書の三点を突き合わせる検証は、新しい例を一つ書くたびに全部走る——だから見つかり、その日のうちに修正して公開した(F107——綻びログ・テストは 509 本になった)。スケジュールの答えが検証可能であるというのは、言語自身の開発にもそのまま跳ね返る——答えが疑えるものは、疑う手が機械になる。
まとめ
- 「同じ日」という言葉は二つの操作を指す——同じ瞬間(snapTo)と同じ日付(rebase)
- 取り違えは静的エラーが止める。仮に通れば「黙って空」か「相手の祝日が化けて出る」で、どちらも目視では捕まらない
- 存在しない日付への対応は明示エラー——暦の凸凹は、隠すものではなく見せるもの
- グローバルなシステムで「今日」と書くとき、それは誰の今日か。premise が tz を明示させるのは、この問いを設計の入口に置くためである
次回は、営業「時間」の話——「営業時間内の毎正時」を 1 行で書く道具と、半日休のデータが尽きた日に何が起きるかを書く予定。
Kairos は設計中のスケジュール定義言語です。仕様と参照実装は GitHub で公開しています。

コメント