1. 「第 1・第 3 水曜」を cron に書くと、毎週水曜に動く
資源ごみは第 1・第 3 水曜、町内会の回覧は第 2・第 4 木曜、給料の締めは 15 日と月末。「月 2 回」は暮らしにも仕事にもいくらでもあるのに、道具の側がこれを苦手にしている。
crontab に「第 1・第 3 水曜の 8 時」を書こうとすると、0 8 1-7,15-21 * 3 になる。1〜7 日のどれかが第 1 水曜、15〜21 日のどれかが第 3 水曜だから、日と曜日の両方で絞ったつもりである。ところが cron は、日と曜日を両方書くと「どちらかが合えば動く」と読む。結果、毎週水曜日と、毎月 1〜7 日・15〜21 日の全部で動く。「第 3 火曜に再起動」のつもりで 15-21 * * 2 と書き、第 3 水曜に全システムが再起動した実害報告がある。

カレンダーアプリの繰り返し設定も、多くは「毎月第 1 水曜」までで、「第 1 と第 3」は予定を 2 つ作ることになる。iCalendar の規格(RRULE)には BYDAY=2TU,4TH と書けるのだが、Outlook が受け付けず、W3C のカレンダー仕様の議論ではこの形の提供を断念した。Microsoft Graph の繰り返しは閉じた 6 パターンで、やはり「系列を 2 つ作れ」になる(「書けないスケジュール」の博物誌のカタログ 4・15)。
「系列を 2 つ作れ」は、束ねる記号が言語に無いことが人間側に波及した姿である。束ねる記号があれば、定義の中で閉じる。
2. 第 1 水曜と第 3 水曜を、それぞれ作って束ねる
premise JP { calendar-system: Gregorian; tz: "Asia/Tokyo"; wkst: Mon }
@JP
wed = everyDay |> filter(d => weekday(d) == Wed)
(wed |> within(month) |> nth(1)) | (wed |> within(month) |> nth(3))
wedは毎週水曜の列。within(month) |> nth(1)で「月ごとに区切って 1 番目」=第 1 水曜(「毎月 30 日払い」の回でnth(30)に使ったのと同じ器)。nth(3)が第 3 水曜。|が和。2 本の列を 1 本に束ねる。
$ kairos list --from 2026-01-01 --to 2026-07-01 wed13.kairos 2026-01-07 2026-01-21 2026-02-04 2026-02-18 2026-03-04 2026-03-18 2026-04-01 2026-04-15 2026-05-06 2026-05-20 2026-06-03 2026-06-17
毎月 2 本ずつ、2026 年で 24 回。4 月は 1 日が水曜なので 1 日と 15 日、5 月は 6 日と 20 日。「1〜7 日のどれか」と手で数えていた部分が nth(1) に入っている。
3. 「15 日と月末」——休みなら前営業日
日付で決まる月 2 回は nth(15) と last:
d15 = everyDay |> within(month) |> nth(15) eom = everyDay |> within(month) |> last d15 | eom
$ kairos list --from 2026-01-01 --to 2026-07-01 pay.kairos 2026-01-15 2026-01-31 2026-02-15 2026-02-28 2026-03-15 2026-03-31 2026-04-15 2026-04-30 2026-05-15 2026-05-31 2026-06-15 2026-06-30
「1 日と 15 日」なら cron でも 0 0 1,15 * * と日付だけで書ける(曜日が絡まなければ OR 罠は出ない)。しかし「月末」は書けない——0 0 31 * * は年 7 回しか動かない(「毎月 30 日払い」の回)。そして cron で書けないもう一段が、「土日祝なら前営業日」である。祝日の表と営業日を前文に置き、束ねた列を roll で丸める:
premise JP {
calendar-system: Gregorian; tz: "Asia/Tokyo"; wkst: Mon
national = [2026-01-01, 2026-01-12, 2026-02-11, 2026-02-23, 2026-03-20, 2026-04-29, 2026-05-03..2026-05-06,
2026-07-20, 2026-08-11, 2026-09-21..2026-09-23, 2026-10-12, 2026-11-03, 2026-11-23] covering: 2026..2026
satSun = everyDay |> filter(d => weekday(d) == Sat or weekday(d) == Sun)
bizDay = everyDay \ (satSun | national)
}
@JP
d15 = everyDay |> within(month) |> nth(15)
eom = everyDay |> within(month) |> last
(d15 | eom) |> roll(Preceding, on: bizDay)
$ kairos list --from 2026-01-01 --to 2026-07-01 pay.kairos 2026-01-15 2026-01-30 2026-02-13 2026-02-27 2026-03-13 2026-03-31 2026-04-15 2026-04-30 2026-05-15 2026-05-29 2026-06-15 2026-06-30 # ⚠ 範囲外 2026-01-01..2026-01-02(JP.national covering 2026-01-01..2026-12-31)
1 月 31 日(土)は 30 日(金)へ、2 月 15 日(日)は 13 日(金)へ、2 月 28 日(土)は 27 日へ、3 月 15 日(日)は 13 日へ、5 月 31 日(日)は 29 日へ。roll(Preceding, on: bizDay) は「bizDay に無ければ手前の bizDay へ寄せる」(roll と shift 入門)。束ねてから丸めるので、roll は 1 回書けばよい。
末尾の注記は、1 月 1〜2 日に点が落ちれば「前営業日」が 2025 年 12 月——表の外——になる、という言語からの一言である。この定義で 1 月 1〜2 日に点が落ちることはないので実害はないが、式は表の外に依存しうる区間を黙らない(調休の回の §6 と同じ機序)。
4. 「第 2 火曜と第 4 木曜」——Outlook が拒否した形
§1 で W3C が断念した形も、同じ型で書ける:
tue2 = everyDay |> filter(d => weekday(d) == Tue) |> within(month) |> nth(2) thu4 = everyDay |> filter(d => weekday(d) == Thu) |> within(month) |> nth(4) tue2 | thu4
$ kairos list --from 2026-01-01 --to 2026-07-01 tue2thu4.kairos 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 2026-06-09 2026-06-25
第 2 火曜は Windows Update の配信日(Patch Tuesday)でもある。曜日が違っても、序数が違っても、列を作って束ねるだけで、「系列を 2 つ」持ち歩く必要はない。
5. 落とし穴——「隔週」と「第 1・第 3」は別物
- 「隔週水曜」は 14 日周期で、月に 3 回ある月が出る(「隔週」と「10 日ごと」)。 2026 年なら隔週水曜は 26 回、第 1・第 3 水曜は 24 回。資源ごみの日は「隔週」ではなく「第 1・第 3」で 決まっていることが多い。どちらなのかは自治体の案内が正で、式はそれを写す。
- 「15 日が土日のとき」をどうするかは書き手が決める。前営業日なら
roll(Preceding …)、翌営業日ならroll(Following …)。cron も RRULE も、ここを決めずに書けてしまう(「毎月 30 日払い」の回)。 - 第 5 水曜がある月でも、「第 1・第 3」の式は影響を受けない。「第 5 水曜があれば」が要るなら
nth(5)を 束ねればよく、無い月は空になるだけ。
6. 動かす
月末最終営業日の自動実行まで通すの型 1 がそのまま使える。crontab は「毎日 8 時」、動くかどうかの判定は定義に委ねる:
0 8 * * * cd /srv/notice && ./run-if-today.sh wed13.kairos ./post-reminder.sh
まとめ
- 「第 1・第 3 水曜」を cron に書くと、日と曜日が OR になって毎週水曜に動く
- カレンダーの規格・API は「系列を 2 つ作れ」になる——束ねる記号が無いから
- Kairos では列をそれぞれ作って
|で束ねる:(wed |> within(month) |> nth(1)) | (… |> nth(3)) - 「15 日と月末」は
nth(15) | last。休日の繰り上げは束ねた後にroll(Preceding, on: bizDay)を 1 回 - 「隔週」と「第 1・第 3」は別物(年 26 回と 24 回)。どちらかは案内が正で、式はそれを写す
▶ Playground で定義を実行する——2026 年 1〜6 月の第 1・第 3 水曜 12 点が出る。コメントにある「15 日と月末・前営業日」「第 2 火曜と第 4 木曜」の行と入れ替えて試せる。ブラウザ内で完結し、何も送信しない。
Kairos はスケジュール定義言語です。仕様と参照実装はGitHub で公開しています。

コメント