本社の暦と、支社の暦——premise の派生

Kairos

暦は、会社に一枚ではない

前回、式の前に premise(前提の束)を宣言する話を書いた。tz・週の始まり・暦法——式の意味を決める文脈を、サーバーの設定ではなく定義の一部にする、という話である。

今回はその続き。実際の組織に入ると、すぐ気づくことがある。前提の束は、一つでは足りない。本社と大阪支社では休業日が一日違う。工場は土日ではなく 4 勤 4 休で回っている。経理は月次ではなく 4-4-5 の会計カレンダーで締めている。同じ社内に、暦が何枚もある

汎用のスケジューラでこれを支えようとすると、たいてい「システム全体で一つのカレンダー設定」に突き当たる。Kairos の答えは単純で、premise を複数書き、差分は差分として書く

本社と支社——差分は 1 行

大阪支社は、本社の営業日から創立記念日(9/25)だけが余分に休みだとする。支社の営業日をゼロから書き直す必要はない。本社の営業日列から 1 日引くだけである:

premise JP { calendar-system: Gregorian; tz: "Asia/Tokyo"; wkst: Mon }

@JP
national = [2026-09-21, 2026-09-22, 2026-09-23] covering: 2026..2026
satSun = everyDay |> filter(d => weekday(d) == Sat or weekday(d) == Sun)
hqBiz    = everyDay \ (satSun | national)
osakaBiz = hqBiz \ [2026-09-25] covering: 2026..2026

(祝日は 9 月分の抜粋。\ は差集合——第 6 弾の祝日カスケードで使った、列から列を引く演算子である。)

ここに「給料日は 25 日、休日なら前営業日」という同じ式を、軸だけ替えて評価する:

payday = everyDay |> within(month) |> nth(25)
payday |> roll(Preceding, on: hqBiz)      # 本社
payday |> roll(Preceding, on: osakaBiz)   # 大阪支社

2026 年 9 月の実測:

本社:   2026-09-25
大阪:   2026-09-24

9/25 は金曜日。本社ではそのまま給料日、大阪では創立記念日なので前営業日の木曜に前倒し——式は一字も変わっていないのに、暦が違うから答えが違う。「支社だけ 1 日ずれる」を給与システムのif 文に埋めるのではなく、暦の定義の差分として書けたことになる。

▶ Playground で開く

with——暦法そのものを派生させる

休業日の差分なら列の引き算で足りる。だが「工場は 4 勤 4 休」「経理は 4-4-5」になると、話は週や月の形そのものに及ぶ。そのための構文が with——既存の暦法を土台に、語彙を足したり上書きしたりした新しい暦法を作る:

premise Rota = Gregorian with {
  duty = day cycle [On, On, On, On, Off, Off, Off, Off] anchor: 2026-01-05
}

読み下すと「グレゴリオ暦に、8 日周期のラベル duty を足した暦法」。cycle は巡回ラベルの生成子で、実はこの言語では曜日そのものが 7 日周期の cycle として定義されている(月曜は月曜、を千年先まで言えるのはこの律動のおかげである)。anchor: が位相の錨。勤務日は:

everyDay |> filter(d => duty(d) == On)
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

4 日出て 4 日休む。この列は曜日と噛み合わない(週は 7 日、周期は 8 日)——cron やカレンダーアプリの繰り返し設定が苦手にしてきた形だが、曜日が cycle なら、8 日周期は曜日の隣に並ぶただのもう一つの律動である。特別扱いは何も要らない。

4-4-5——会計の暦を 5 行で

仕上げに、経理の 4-4-5 を書く。四半期を 4 週+4 週+5 週に割る会計カレンダーで、小売業や製造業で広く使われている。「月」が存在しない暦なので、月ベースの繰り返し語彙は全滅し、会計 SaaS 各社が専用機能として個別実装している領域である。

標準同梱の ISO 週暦(月曜始まりの週・その年最初の木曜を含む週が W01)を土台に派生させる:

premise R445 = ISOWeek with {
  periodStart = isoWeekStart |> filter(d =>
    ((isoWeekNo(d) - 1) mod 13 == 0 or (isoWeekNo(d) - 1) mod 13 == 4 or (isoWeekNo(d) - 1) mod 13 == 8)
    and isoWeekNo(d) <= 48)
  period = day |> segmentBy(periodStart, edges: clip, empties: error)
}

「期の頭は、週番号 1, 5, 9, 14, …, 48 の月曜」——4-4-5 の律動を週番号の剰余で言い、segmentBy でその月曜を境に期の窓を張る。各期の初日を実測すると:

2025-12-29  2026-01-26  2026-02-23  2026-03-30  2026-04-27  2026-05-25
2026-06-29  2026-07-27  2026-08-24  2026-09-28  2026-10-26  2026-11-23
2027-01-04  2027-02-01

4 週(12/29→1/26)・4 週(→2/23)・5 週(→3/30)——4-4-5 が刻まれている。そしてここに、この暦のいちばん厄介な問題が黙って解けている。364 日(52 週)の暦は数年に一度 53 週の年に当たり、余った 1 週をどこかの期に足す必要がある(繰り上げ週)。2026 年のISO 年はちょうど 53 週——上の実測で最終期が 11/23 から翌年 1/3 まで 6 週間に伸び、翌年は 1/4(W01 の月曜)から正常に再開している。繰り上げの規則は一行も書いていない。「期の頭は W48 まで」という定義の形から、余り週が最終期に吸収されることが帰結として出るのである。

まとめ

  • 暦は会社に一枚ではない——本社と支社・工場・経理は、それぞれの前提の束で動いている
  • 差分は差分として書く: 支社の営業日は本社の列から 1 日引くだけ・同じ給料日の式が 暦の違いだけで別の日を返す(実測 9/25 と 9/24)
  • with は暦法の派生——8 日周期の勤務ラベル(4 勤 4 休)も、週ベースの会計暦(4-4-5)も、 グレゴリオ暦や ISO 週暦を土台にした差分の宣言で立つ
  • 定義の形が良いと、規則を書かなくても正しさが出る——4-4-5 の 53 週目の繰り上げは、 期の頭の定義から帰結として転がり出た

次回は少し引いた場所から。この連載で「cron では書けない」「RRULE では書けない」と何度も書いてきたが、では世の中で実際に「書けない」と言われてきたスケジュールを集めたら、どれだけ書けるのか——挫折の目録と、その実測を書く予定である。


Kairos は設計中のスケジュール定義言語です。仕様と参照実装はGitHub で公開しています。

コメント

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