日本語 · English

月末・月末最終営業日に実行する——cron の限界と Kairos の式

「毎月末に実行したい」は cron 最頻出の挫折点のひとつ (Stack Overflow 閲覧 32.5 万)。 Kairos ではどちらも 1 行の式になる:

monthEnd                                  # 月末日
bizDay |> within(month) |> last           # 月末最終営業日

cron で何が起きるか

根にあるのは機能の不足ではなく式が合成できないこと——「月末」を出せても、その結果を 「営業日で前へ丸める」次の規則に流せない。

Kairos で書く

営業日(祝日対応)の月末。@JP の標準前提(土日+日本の祝日データから導出された bizDay)で:

# eval: 2026-01-01..2026-07-01
@JP
bizDay |> within(month) |> last
#=> 2026-01-30 2026-02-27 2026-03-31 2026-04-30 2026-05-29 2026-06-30

1 月末(1/31 土)と 5 月末(5/30 土・5/31 日)が正しく金曜へ退いている。「営業日の列を作り、 月ごとに最後の点を取る」——読んだままの構造で、祝日はカレンダーデータ(covering: つき)から 来るので、データが尽きれば黙らず註釈が出る。

月末から数える変種も同じ語彙の合成:

monthEnd |> roll(Preceding, on: bizDay) |> shift(-3, unit: bizDay)   # 月末の 3 営業日前

米国のカレンダーでも、式は同じ

上の式は日本の祝日データに 1 文字も依存していない。同じ式を米国連邦祝日(2026 年・振替後の observed 日付)に載せ替える:

# eval: 2026-01-01..2026-07-01 tz: America/New_York
premise US {
  calendar-system: Gregorian
  tz: "America/New_York"
  wkst: Sun
}

@US
federal2026 = [2026-01-01, 2026-01-19, 2026-02-16, 2026-05-25, 2026-06-19,
               2026-07-03, 2026-09-07, 2026-10-12, 2026-11-11, 2026-11-26,
               2026-12-25] covering: 2026..2026
satSun = everyDay |> filter(d => weekday(d) == Sat or weekday(d) == Sun)
bizDay = everyDay \ (satSun | federal2026)

bizDay |> within(month) |> last
#=> 2026-01-30 2026-02-27 2026-03-31 2026-04-30 2026-05-29 2026-06-30

変わったのは前提(premise)のデータと tz だけで、本体の式は 1 文字も変わらない。 「営業日の列を作り、月ごとに最後の点を取る」という構造は暦データから独立している——どの国の カレンダーでも、差し替えるのはデータだけである。 米国版を Playground で実行。

動かす——crontab は 1 行だけ残す

Kairos が答えるのは「いつ動くべきか」の集合までで、起動・リトライ・記録は実行系(systemd・ジョブランナー等)の 仕事である(言語仕様 §7.8)。接続の型は 2 つ。どちらも CLI(npm i -g kairos-lang)で そのまま動く。

型 1: cron を時計として残し、判定だけ Kairos に移す。 crontab の行は 1 本だけ残し、毎朝 9 時に「今日は点があるか」 を聞いて、あれば起動する。ラベルと [--from, --to) の窓は機械の tz で読まれる(定義の premise tz と違うなら --tz で揃える):

0 9 * * *  cd /srv/batch && ./run-if-today.sh month-end.kairos ./close-books.sh
#!/bin/sh
# run-if-today.sh <定義.kairos> <ジョブ> —— 今日 [今日, 明日) に点があればジョブを exec する
today=$(date +%F); tomorrow=$(date -d "$today + 1 day" +%F)   # GNU date。macOS は date -v+1d +%F
n=$(kairos list --from "$today" --to "$tomorrow" --json "$1" | jq '.results[0].dates | length')
[ "$n" -gt 0 ] && exec "$2"

人間向けの出力には被覆サマリの # 行が混ざるため、判定は --json で数える。

型 2: 次の 1 点を予約する。 時刻も定義側に置き(… |> last |> at(T23:55))、next --json で次の発火を取る。 壁時計(機械の tz)と epoch ミリ秒の両方で返るので、OS の一回限りのタイマーに渡し、ジョブの最後で次を登録し直す (仕様の言う「定期的に再具現化する」運用):

t=$(kairos next --json month-end.kairos | jq -r '.results[0].dates[0]')   # 例 2026-09-30T23:55
systemd-run --user --on-calendar="$(echo "$t" | tr T ' '):00" ./close-books.sh
# Linux の at(1)、Windows の schtasks /sc once でも同じ形

ブラウザで試す

前提込みの自己完結形(祝日テーブルを式の中に持つ)を Playground で実行 ——from/to を動かしたり、祝日を足したりして挙動を確かめられる。

関連