「第 1・第 3 水曜」と「15 日と月末」——「月 2 回」を、1 本の式で書く

Kairos

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: 2026 年 4 月のカレンダー。左は cron の 0 8 1-7,15-21 * 3 が動く日(毎週水曜と 1〜7 日・15〜21 日の全部=17 日)、右は意図した第 1・第 3 水曜(1 日と 15 日)。
図 1: 2026 年 4 月のカレンダー。左は cron の 0 8 1-7,15-21 * 3 が動く日(毎週水曜と 1〜7 日・15〜21 日の全部=17 日)、右は意図した第 1・第 3 水曜(1 日と 15 日)。

カレンダーアプリの繰り返し設定も、多くは「毎月第 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 で公開しています。

コメント

タイトルとURLをコピーしました