逆向きの検証
この連載では「cron では書けない」「RRULE では書けない」と何度も書いてきた。ただ、それはいつも設計する側からの言い分だった。比較表を作り、能力の差を並べ、こちらの語彙で書いてみせる——出発点はいつもこちら側にあった。
先日、逆向きの検証をやってみた。利用者の側で、実際に「書けない」という挫折が報告されたスケジュール要求を世の中から集めてくるのである。Stack Overflow・Server Fault の頻出質問、POSIX の正文と Debian のバグトラッカ、RFC 5545/7529 の仕様議論と主要実装の issue、Google Calendar や Microsoft Graph の API 仕様、Kubernetes・Airflow・Quartz、給与や会計 SaaS の専用機能ドキュメント。三系統の調査で、32 項目の目録になった。
先に断っておくと、これは道具の否定ではない。cron の 5 フィールドは 50 年間、圧倒的多数の定期実行を支えてきた意図的に小さい道具であり、RRULE は相互運用のための交換形式という別の設計目標を持つ。ここで集めたのは「その設計目標の外にあり続けた要求」——各コミュニティ自身が回避策・方言拡張・専用機構で埋め続けてきた領域である。
標本室から
目録の全体は出典リンクつきで設計文書として公開しているので、ここでは博物誌らしく、収集していて唸った標本をいくつか並べたい。
- 月末に実行したい——cron 最頻出の挫折点。Stack Overflow の該当質問は 閲覧 32.5 万。 Quartz 方言に
Lはあるが処理系間で移植できず、Kubernetes の CronJob では issue が 再提起されるループになっている。 - 「毎月第 3 火曜」のつもりが——POSIX は日付フィールドと曜日フィールドの併記を OR と規定している。
15-21 * * 2と書くと「15〜21 日かつ火曜」ではなく 「15〜21 日、または毎週火曜」になり、 第 3 水曜に全システムを再起動してしまった実害報告がある。 Debian のバグ報告は 15 年 wontfix のまま、Vixie cron のソースコメント自身が 「奇妙だが……これが標準だ」と書いている。 - 隔週——Quartz の公式クックブックが「CronTrigger では無理」と明言している。
- 10 日ごと——
*/10は月替わりでリセットされ、10 日ごとにならない。 Debian の man ページ自身が epoch 秒の剰余ハックを公式に例示している。 - 第 2 火曜と第 4 木曜を一つの系列で——
BYDAY=2TU,4THは RRULE として合法なのに Outlook が拒否し、W3C がカレンダー仕様での提供自体を断念した。 - 短い月の 31 日——RFC 5545 は黙ってスキップする。救済に RFC 7529 が
SKIPを定義したが、 主要実装が 11 年間未対応のままである。 - イースター——RFC 5545/7529 とも表現できず、dateutil の拡張は「RFC 外」と自認、 1900〜2099 年限定の近似 RRULE 集という力技まで存在する。
一つ一つが、誰かの深夜のバッチ障害であり、誰かの手作業である。祝日の週だけ 100 台の cron を手でコメントアウトして回る、という運用報告も標本にある。
目録は三つに割れる
この 32 項目を Kairos で片端から実測していくと、「書けない」の正体は三つに割れた。
- 語彙で書ける——23 項目。挫折の根が既存方式の構造(フィールドの直積・月での リセット・単一系列・閉じたパターン集合)にあり、言語の設計で消える型。今回の実測 17 本は すべてここで、この検証のための言語変更はゼロだった。
- データで書ける——7 項目。祝日・官報・天文・観測。ここはどの道具でも原理的に規則だけ では書けない(春分の日は前年 2 月の官報告示まで正式に決まらない——第 14 弾に書いた)。 差が出るのは「書けるか」ではなく、「データであることを言語が言うか」である。
- 設計上の射程外——2 項目。これは最後に書く。
なお、月末最終営業日やイースターのような検索頻出の形は、実用形に整えてレシピ集にも置いてある。以下では「語彙で書ける」から、この連載でまだ見せていない 3 本を実測でお見せする。
実測 1: 毎月 31 日——「無い月」の二義
毎月 31 日、と指定したとき、31 日の無い月はどうなるべきか。飛ばすのか、月末に丸めるのか。書き手の意図は二通りあり得て、どちらが正しいかは仕様書には書いていない——書いた本人にしか分からない。それなのに RRULE の BYMONTHDAY=31 は黙ってスキップへ倒れる(そして丸めたかった人が、先ほどの SKIP の 11 年を待ち続けている)。
Kairos では、二義が別の式になる。まず飛ばす形。2026 年の上半期で実測する:
premise JP { calendar-system: Gregorian; tz: "Asia/Tokyo"; wkst: Mon }
@JP
everyDay |> within(month) |> nth(31)
2026-01-31 2026-03-31 2026-05-31
|> は左の列を右の演算へ流すパイプ、within(month) は列を月ごとの窓に区切る、nth(31) はその窓の中の 31 番目。2・4・6 月には 31 番目が存在しないから、正当な空として飛ぶ——式がそのまま「31 日が存在する月だけ」と読み下せる。
月末に丸める形は、そのまま月末の式になる:
everyDay |> within(month) |> last
2026-01-31 2026-02-28 2026-03-31 2026-04-30 2026-05-31 2026-06-30
どちらを意図したか、式を読めば分かる。31 以外の一般形——「毎月 30 日、無い月は月末」——も、30 日の列と「30 日が無い月の月末」の列を束ねる 3 行で書き分けられる(目録の実測 (b) にある)。黙る既定に倒れる代わりに、二義を書き分ける言葉がある。集めた挫折のかなりの部分が、この型(多義な要求 + 黙る既定)だった。
実測 2: 月 2 回を、一つの定義で
第 2 火曜(Windows の更新でおなじみの Patch Tuesday の形)と第 4 木曜の定例を、一つのカレンダー系列にしたい。Microsoft Graph の繰り返しパターンは閉じた 6 パターンでこれを表せず、標本のとおり W3C が諦めた形である。2026 年 1〜5 月で実測する:
tue2 = everyDay |> filter(d => weekday(d) == Tue) |> within(month) |> nth(2) thu4 = everyDay |> filter(d => weekday(d) == Thu) |> within(month) |> nth(4) tue2 | thu4
2026-01-13 2026-01-22 2026-02-10 2026-02-26 2026-03-10 2026-03-26 2026-04-14 2026-04-23 2026-05-12 2026-05-28
| は二つの列を一つに束ねる結合子(前提は実測 1 と同じ)。「系列を 2 つ作って運用で管理せよ」という各社の裁定は、結合子が言語に無いことが人間側の作業へ波及した姿で、| が一つあれば定義の中で閉じる。
実測 3: 米大統領選挙日——意図の消えた符号化
「11 月の第 1 月曜の翌日の火曜」。米国の選挙日である。これを RRULE で書くとBYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8 になる——正しく動く。だが、仕様の言葉がどこにも残っていない。「2〜8 日の火曜」がなぜ選挙日と同じものなのか、読み手は逆算で復元するしかない。
Kairos は仕様の言葉の順に書ける。2024〜2028 年で実測する:
premise US { calendar-system: Gregorian; tz: "America/New_York"; wkst: Sun }
@US
firstMonNov = everyDay |> filter(d => weekday(d) == Mon and month(d) == 11) |> within(year) |> first
firstMonNov |> shift(+1, unit: day)
2024-11-05 2025-11-04 2026-11-03 2027-11-02 2028-11-07
「11 月の月曜のうち、その年の最初のもの。その翌日」。定義とは、動けばよい符号ではなく、意図を運ぶ文章でもある——月を跨ぐ年があるせいで RRULE の負値ハックが破綻する「感謝祭の次の日曜」も、目録では同じ型で素直に書けている。
書けないものは、黙らずに断る
最後に、射程外へ落ちた 2 項目。「前回の実行が完了してから 5 時間後」(実行状態へのフィードバック)と、「振替した先がまた祝日ならさらに振替、を無限段」(振替の一般再帰)である。前者はそもそも定義ではなく実行の話なので、注入された時点からの次回計算に分解する。後者は固定回数の展開を受け皿にして、一般再帰そのものは断っている。
大事なのはここで、収集した挫折の多くは「黙って OR になる」「黙ってスキップする」「黙って作成者のタイムゾーンが勝つ」という黙りの既定に由来していた。だから、射程外の側も黙らない——「書けない」を、受け皿の明文化つきで言う。書ける側の 30 項目と同じくらい、この 2 項目の断り方を、この言語の性格だと思っている。
まとめ
- 「書けない」と報告されてきた 32 項目を集めて実測した——23 項目は語彙で書け、7 項目は データで書け、2 項目は受け皿を明文化して断る
- 挫折の根の多くは既存方式の構造(フィールドの直積・月でのリセット・単一系列・閉じた パターン集合)にあり、言語の設計で消える——実測 17 本はすべて現行語彙・言語変更ゼロ
- 「書けるか」と同じくらい「黙るか、言うか」が分水嶺だった——多義な要求に黙る既定が 重なったとき、事故が起きる
- 完全な目録(出典リンクつき)と実測の全コードは 設計文書として公開している
次回は、今日のまとめに顔を出した「黙りの既定」の話。OR になる・スキップする・作成者の時計が勝つ——三つの「黙り」はどうして事故になるのか、黙らない設計は何を言うべきなのかを書く予定である。
Kairos は設計中のスケジュール定義言語です。仕様と参照実装はGitHub で公開しています。

コメント