1. 8 日周期は、週にも月にも揃わない
工場・病院・コールセンターで見る「4 勤 4 休」は、4 日働いて 4 日休む 8 日周期である。1 月 5 日(月)に始まるサイクルを追うと、次のサイクルの初日は 1 月 13 日(火)、その次は21 日(水)、29 日(木)、2 月 6 日(金)——曜日が毎回ずれる。週(7 日)と 8 日は揃わず、月(28〜31 日)とも揃わない。
cron は「分 時 日 月 曜日」の 5 つの枠しか持たないので、これは書けない。iCalendar の RRULE もFREQ=DAILY;INTERVAL=8 で「8 日ごとの 1 日」は書けるが、「4 日続けて」は書けない。シフト管理のサービスはどうしているかというと、DDDD---- のような独自のパターン文字列で持ち、外に出すときは日付を展開し切った .ics を吐く(「書けないスケジュール」の博物誌、カタログ 26)。展開した日付には、もう「8 日周期」という構造は残っていない。
2. 曜日と同じ仕組みで書く
Kairos で曜日は、日に「Mon, Tue, …, Sun」の 7 個のラベルを繰り返し貼ったものとして定義されている(標準 premise の weekday)。ラベルは日を切り分ける窓ではなく、日と並列に走る目盛りで、周期の長さは 7 でなくてもよい。4 勤 4 休は、8 個のラベルを貼れば書ける:
premise Rota = Gregorian with {
tz: "Asia/Tokyo"
dutyA = day cycle [On, On, On, On, Off, Off, Off, Off] anchor: 2026-01-05
}
premise JPR { calendar-system: Rota; tz: "Asia/Tokyo"; wkst: Mon }
@JPR
everyDay |> filter(d => dutyA(d) == On)
初めて出る記法を順に。
premise Rota = Gregorian with { … }は、標準のグレゴリオ暦を継承してラベルを 1 つ足した 暦法(本社の暦と、支社の暦で書いた「前文の派生」)。day cycle [On, On, On, On, Off, Off, Off, Off]が 8 日周期の反復ラベル。リストは巡回の順を 与えるだけで、先頭に「開始」の意味はない。anchor: 2026-01-05が位相。「anchor の日が先頭ラベル(On)」と決めることで、どの日が どのラベルかが確定する。dutyA(d)は「日 d のラベル」を返す。weekday(d)と同じ読み方である。
1 月 5 日から 4 週間ぶん出してみる:
$ kairos list --from 2026-01-05 --to 2026-02-02 rota.kairos 2026-01-05 2026-01-06 2026-01-07 2026-01-08 2026-01-13 2026-01-14 2026-01-15 2026-01-16 2026-01-21 2026-01-22 2026-01-23 2026-01-24 2026-01-29 2026-01-30 2026-01-31 2026-02-01
4 日出て 4 日休む列が、月をまたいでそのまま続く(1 月 29 日〜2 月 1 日)。年をまたいでも同じ:
$ kairos list --from 2026-12-20 --to 2027-01-20 rota.kairos 2026-12-23 2026-12-24 2026-12-25 2026-12-26 2026-12-31 2027-01-01 2027-01-02 2027-01-03 2027-01-08 2027-01-09 2027-01-10 2027-01-11 2027-01-16 2027-01-17 2027-01-18 2027-01-19
12 月 31 日から 1 月 3 日が勤務のサイクルになっている。式は年のことを何も知らない。2026 年の勤務日数は 181 日(365 日は 8 日周期 45 回と端数 5 日なので、位相によって 180〜184 日になる)。
3. B 班は anchor を 4 日ずらすだけ
A 班が休む 4 日に B 班が出る、という組み方は、同じラベルの列で anchor だけ 4 日後にする:
premise Rota = Gregorian with {
tz: "Asia/Tokyo"
dutyA = day cycle [On, On, On, On, Off, Off, Off, Off] anchor: 2026-01-05
dutyB = day cycle [On, On, On, On, Off, Off, Off, Off] anchor: 2026-01-09
}
$ kairos list --from 2026-01-05 --to 2026-02-02 rotaB.kairos 2026-01-09 2026-01-10 2026-01-11 2026-01-12 2026-01-17 2026-01-18 2026-01-19 2026-01-20 2026-01-25 2026-01-26 2026-01-27 2026-01-28
A 班の休みにぴったり嵌まる。確かめるなら、両班が同時に出る日を積 & で数える:
(everyDay |> filter(d => dutyA(d) == On)) & (everyDay |> filter(d => dutyB(d) == On))
2026 年を通して 0 点。逆に和 | を取れば毎日になる。
ここで一つ、落とし穴を書いておく。B 班を作るとき、リストの方を [Off, Off, Off, Off, On, On, On, On]と書き換えて anchor を同じにしても、結果は同じになる。しかし「位相を持つのは anchor」と決めておく方が事故が少ない。リストを回転させた上に anchor も別の日にすると、ラベルが一斉に回って、どの日が On かが分からなくなる。班を増やすときは、リストは同じまま、anchor だけを実際の初日にする。anchor には照合済みの実日(このサイクルは確かにこの日に始まった)を書く。
4. サイクルの初日と、出勤時刻
「各サイクルの初日に申し送りを出す」なら、勤務日の列を 4 個ごとに間引く(前回の stride):
everyDay |> filter(d => dutyA(d) == On) |> stride(4, from: 2026-01-05)
$ kairos list --from 2026-01-01 --to 2026-03-01 rota-first.kairos 2026-01-05 2026-01-13 2026-01-21 2026-01-29 2026-02-06 2026-02-14 2026-02-22
月・火・水・木・金・土・日と、初日の曜日が 1 つずつ進んでいく。8 日周期を 7 日の週に載せると、こうなる。
出勤時刻を貼るなら at:
everyDay |> filter(d => dutyA(d) == On) |> at(T08:00)
2026-01-05T08:00 2026-01-06T08:00 2026-01-07T08:00 2026-01-08T08:00 2026-01-13T08:00
5. 落とし穴——ラベルと窓、律動とデータ
cycleは律動(無限に繰り返す規則)である。「今年のシフト表は人事が決めた不規則な並び」の ように有限のデータなら、cycleではなくテーブルのlabels:で日付とラベルを 1 対 1 に持つ (調休の回で表を写したのと同じ器)。規則で書けるものと、データで しか持てないものは、Kairos では別の書き方になる。- ラベルは窓ではない。
within(week)のように「サイクルで区切る」ことはできず、filterで ラベルを読む。区切りが要るなら、初日の列(§4)を境界にする。 wkst(週の開始)はcycleと無関係。週が月曜始まりでも日曜始まりでも、8 日周期は動かない。
6. 射程——式が出すのは「誰かが出る日」まで
- 式が出すのは「A 班が出る日」の列であって、誰が A 班かは出さない。人の割当・交代・有給の 差し引きは、シフト管理の実装系の仕事である。言語は集合の定義まで。
- 夜勤の「日をまたぐ勤務」は、ここでは扱っていない。
at(T20:00)で始まりの点は貼れるが、 終わりの時刻や日跨ぎの扱いは営業時間(isOpen)の領分で、別の回にする。 - DuPont 方式(28 日周期・昼夜で時刻が違う)も同じ型で書ける——28 個のラベルの
cycleに、 ラベルごとの時刻をatで貼って|で束ねる。
7. 動かす
月末最終営業日の自動実行まで通すの型 1 がそのまま使える:
0 7 * * * cd /srv/shift && ./run-if-today.sh rota-first.kairos ./send-handover.sh
まとめ
- 4 勤 4 休は 8 日周期。週にも月にも揃わず、初日の曜日が毎回ずれる。cron・RRULE では書けない
- Kairos では曜日と同じ仕組み——
day cycle [On×4, Off×4] anchor: 実日——で書く。月も年もまたぐ - 位相は anchor が持つ。B 班はリストを回さず anchor を 4 日ずらす。両班の交差は 0 日、和は毎日
- サイクル初日は
stride(4, from:)、出勤時刻はat - 律動は
cycle、有限のデータはlabels:。誰が出るかは実装系の仕事
▶ Playground で定義を実行する——2026 年 1〜2 月の A 班の勤務日が出る。コメントにある B 班・両班の交差(空)・サイクル初日・出勤 8 時の行と入れ替えて試せる。anchor: を 1 日動かすと全部がずれる。ブラウザ内で完結し、何も送信しない。
Kairos はスケジュール定義言語です。仕様と参照実装はGitHub で公開しています。

コメント