休配日マスタ、年末の更新で泣かないために——規則を一度書けば、翌年も再生成できる

Kairos

1. 12 月になると、営業日マスタを手で埋め直す

WMS や受注システムには「営業日マスタ」「休配日マスタ」がある。中身は日付と営業日フラグの列で、年に一度、翌年分を誰かが埋める。元になるのは運送会社の年末年始の休配日告知と、内閣府の祝日の一覧。カレンダーを見ながら 365 行を打つか、前年の表をコピーして曜日をずらし、祝日とお盆と年末年始を直す。

痛いのは、作業そのものより更新漏れである。表が 12 月 31 日で尽きていると、1 月の受注の着日計算が翌営業日を見つけられずに止まるか、もっと悪いと、1 月 1 日を営業日として着日を返す。どちらが起きるかはシステム次第で、気づくのは年明けの現場からの電話だったりする。

この回は、その表を規則から生成する書き方の話である。営業日マスタをなくす話ではない。マスタは残し、埋める作業を「規則を 1 本書き、毎年の表を足し、再生成する」に替える。前回は営業日の列を「数える」道具にした。今回は同じ列を「マスタへ渡す」。

2. 規則を 1 本で書く

公開情報の典型で書く。休配日は日曜・祝日・年末年始(12/30〜1/3)・お盆(8/13〜8/15)。営業日はそれ以外。会社ごとに違うところは §6 で触れる。

# 営業日と休配日(日祝・年末年始・お盆を除く)
premise JP {
  calendar-system: Gregorian
  tz: "Asia/Tokyo"
  wkst: Mon
}

@JP
holidays = [2026-01-01, 2026-01-12, 2026-02-11, 2026-02-23, 2026-03-20,
            2026-04-29, 2026-05-03, 2026-05-04, 2026-05-05, 2026-05-06,
            2026-07-20, 2026-08-11, 2026-09-21, 2026-09-22, 2026-09-23,
            2026-10-12, 2026-11-03, 2026-11-23] covering: 2026..2026
closed = [2026-01-01..2026-01-03, 2026-08-13..2026-08-15, 2026-12-30..2026-12-31] covering: 2026..2026
sunday = everyDay |> filter(d => weekday(d) == Sun)
noDelivery = sunday | holidays | closed
deliveryDay = everyDay \ noDelivery

deliveryDay
noDelivery
  • holidays は 2026 年の祝日 18 日(振替休日 5/6・国民の休日 9/22 を含む)。covering: 2026..2026 は「この表は 2026 年の分」という有効範囲の宣言。
  • closed は会社の休業日。2026-01-01..2026-01-03 は連続した日の略記。年ごとに告知で決まるものなので、規則にせず表に書く。
  • sunday は規則。「日曜なら」は毎年同じなので、表は要らない。
  • | が和、\ が差。休配日は 3 つの和、営業日は毎日から休配日を引いたもの。
  • 最後の 2 行が「出してほしい列」。1 つのファイルに 2 つ並べられる。

「日曜は規則、祝日と休業日は表」という分け方は、前々回の「8 月は規則、年末年始は表」と同じである。ゴールデンウィークで動かしてみる:

$ kairos list --from 2026-04-27 --to 2026-05-11 --tz Asia/Tokyo delivery.kairos
# 式 1(8 件)
2026-04-27
2026-04-28
2026-04-30
2026-05-01
2026-05-02
2026-05-07
2026-05-08
2026-05-09
# 式 2(6 件)
2026-04-29
2026-05-03
2026-05-04
2026-05-05
2026-05-06
2026-05-10
# 被覆サマリ
#   holidays covering 2026-01-01..2026-12-31 残走路 235 日
#   closed covering 2026-01-01..2026-12-31 残走路 235 日

--from 2026-04-27 --to 2026-05-11 は「4/27 から 5/10 まで」。--to の日は含まない。この「右端を含まない」書き方は半開区間の回にある。式 1 が営業日、式 2 が休配日。土曜(5/2・5/9)は営業日、4/29 と 5/3〜5/6 が休配日に落ちている。5/6 は振替休日で、表に書いてあるから落ちる。

3. 表が尽きた先は、黙らない

年末に配る「年末年始の営業日のお知らせ」の帯を出す。12/26 から 1/7 まで:

$ kairos list --from 2026-12-26 --to 2027-01-08 --tz Asia/Tokyo delivery.kairos
# 式 1(9 件)
2026-12-26
2026-12-28
2026-12-29
2027-01-01
2027-01-02
2027-01-04
2027-01-05
2027-01-06
2027-01-07
# ⚠ 範囲外 2027-01-01..2027-01-08(holidays covering 2026-01-01..2026-12-31)
# ⚠ 範囲外 2027-01-01..2027-01-08(closed covering 2026-01-01..2026-12-31)
# 式 2(4 件)
2026-12-27
2026-12-30
2026-12-31
2027-01-03
# ⚠ 範囲外 2027-01-01..2027-01-08(holidays covering 2026-01-01..2026-12-31)
# ⚠ 範囲外 2027-01-01..2027-01-08(closed covering 2026-01-01..2026-12-31)
# 被覆サマリ
#   holidays covering 2026-01-01..2026-12-31 残走路 -7 日
#   closed covering 2026-01-01..2026-12-31 残走路 -7 日

12/30・12/31 は休業で落ちている。ところが 1/1 と 1/2 が営業日として出ている。祝日表も休業日の表も 2026 年で尽きているので、1 月は「日曜だけ」を引いた結果になっている(1/3 は日曜なので落ちている)。これが §1 の「1 月 1 日を営業日として着日を返す」の正体である。

違いは、黙っていないこと。点の下の ⚠ 範囲外 の 2 行と、被覆サマリの「残走路 −7 日」がそれを言っている。残走路は「表の終わりまであと何日あるか」で、マイナスは聞いた範囲が表を 7 日はみ出したという意味。点は出るが、表を知らずに出ている。この帯を配る前に、表を延ばす(祝日表は黙って腐る)。

図 1: 12 月 26 日から 1 月 7 日までの 13 日の帯を 2 段で比べる。上は表が 2026 年で尽きたままの結果で、1/1 と 1/2 に営業日の点と ⚠ が付き、12/29 出荷の着日は近県 1/1・遠方 1/2・離島 1/4 と出る。下は 2027 年まで延ばした表で、営業日は 12/26・28・29 と 1/4〜1/7 の 7 日、着日は 1/4・1/5・1/6(2026-10-20 図を追加)
図 1: 12 月 26 日から 1 月 7 日までの 13 日の帯を 2 段で比べる。上は表が 2026 年で尽きたままの結果で、1/1 と 1/2 に営業日の点と ⚠ が付き、12/29 出荷の着日は近県 1/1・遠方 1/2・離島 1/4 と出る。下は 2027 年まで延ばした表で、営業日は 12/26・28・29 と 1/4〜1/7 の 7 日、着日は 1/4・1/5・1/6(2026-10-20 図を追加)

図の下段の「12/29 出荷」は、同じ営業日の列で着日(出荷日+N 営業日)を数えたもので、書き方はレシピにある。表が尽きたままだと近県便が 1/1 着と出る。延ばせば 1/4 着になる。

4. 翌年の行を足して、再生成する

表を延ばす=2027 年の祝日 17 日と休業日を足し、covering: を 2026..2027 にする。式は触らない:

holidays = [2026-01-01, 2026-01-12, 2026-02-11, 2026-02-23, 2026-03-20,
            2026-04-29, 2026-05-03, 2026-05-04, 2026-05-05, 2026-05-06,
            2026-07-20, 2026-08-11, 2026-09-21, 2026-09-22, 2026-09-23,
            2026-10-12, 2026-11-03, 2026-11-23,
            2027-01-01, 2027-01-11, 2027-02-11, 2027-02-23, 2027-03-21,
            2027-03-22, 2027-04-29, 2027-05-03, 2027-05-04, 2027-05-05,
            2027-07-19, 2027-08-11, 2027-09-20, 2027-09-23, 2027-10-11,
            2027-11-03, 2027-11-23] covering: 2026..2027
closed = [2026-01-01..2026-01-03, 2026-08-13..2026-08-15, 2026-12-30..2026-12-31,
          2027-01-01..2027-01-03, 2027-08-13..2027-08-15, 2027-12-30..2027-12-31] covering: 2026..2027

同じ帯をもう一度:

$ kairos list --from 2026-12-26 --to 2027-01-08 --tz Asia/Tokyo delivery.kairos
# 式 1(7 件)
2026-12-26
2026-12-28
2026-12-29
2027-01-04
2027-01-05
2027-01-06
2027-01-07
# 式 2(6 件)
2026-12-27
2026-12-30
2026-12-31
2027-01-01
2027-01-02
2027-01-03
# 被覆サマリ
#   holidays covering 2026-01-01..2027-12-31 残走路 358 日
#   closed covering 2026-01-01..2027-12-31 残走路 358 日

⚠ が消え、1/4(月)が年始の初営業日になった。表の名前に年を入れていない(holidays2026 ではなく holidays)ので、延ばしても式を直す場所がない。

マスタへ渡すときは --tsv で、1 点 1 行のタブ区切りに書き出す。註釈や被覆サマリは標準エラーに出るので、ファイルには混ざらない:

$ kairos list --tsv --from 2026-01-01 --to 2027-01-01 --tz Asia/Tokyo delivery.kairos > delivery.tsv
# 式 1(289 件)
# 式 2(76 件)
# 被覆サマリ
#   holidays covering 2026-01-01..2026-12-31 残走路 0 日
#   closed covering 2026-01-01..2026-12-31 残走路 0 日
$ cut -f 1,2,4,7 delivery.tsv | head -4
expr	source	date	note
1	deliveryDay	2026-01-05	
1	deliveryDay	2026-01-06	
1	deliveryDay	2026-01-07	

2026 年は営業日 289 日・休配日 76 日で、足すと 365。expr 列の 1 が営業日、2 が休配日なので、1 つのファイルから 2 つのマスタが切り出せる。note 列は、その点が「表の外」の区間に入るときだけ註釈文が入る。空なら表の内側の計算である。列はほかに uid・time・epoch_ms・label があり、Excel への取り込み方はExcel 対照レシピにある。

年次更新は、延ばした表で同じコマンドを 2 年ぶん走らせ、旧マスタと差分を取る:

$ kairos list --tsv --from 2026-01-01 --to 2028-01-01 --tz Asia/Tokyo delivery.kairos > delivery-2027.tsv
# 式 1(581 件)
# 式 2(149 件)
# 被覆サマリ
#   holidays covering 2026-01-01..2027-12-31 残走路 0 日
#   closed covering 2026-01-01..2027-12-31 残走路 0 日
$ diff delivery.tsv delivery-2027.tsv | cut -f 1,2,4 | head -3
290a291,582
> 1	deliveryDay	2027-01-04
> 1	deliveryDay	2027-01-05

差分は追加 365 行・削除 0 行で、追加はすべて 2027 年の行(営業日 292・休配日 73)。2026 年の 365 行は 1 行も変わっていない。これが検算である。表を足して式を触っていないのだから当然に見えるが、「当然」を機械に言わせるのが年次更新の肝で、前年の表をコピーして直す運用には、この検算がない。

旧マスタが Excel や DB にあるなら、日付と営業日フラグの 2 列に書き出し、生成した TSV の date 列と突き合わせる。Excel なら COUNTIF、PowerShell なら Compare-Object。差分が「足した年の行だけ」なら、更新は済んでいる。

5. 置き換えではない——上流の権威は上流に

  • Kairos は WMS・TMS・受注システムを置き換えない。「どの日が営業日か」を定義から出し、マスタへ渡すところまで。受注・在庫・配車は、これまでのシステムに。
  • 休配日の正は運送会社の公式告知にある。年末年始・お盆の休配は毎年告知され、年によって日付も長さも変わる。ここでの 12/30〜1/3・8/13〜8/15 は典型例で、告知が出たら closed の行を告知どおりに直す。式が休配日を決めることはない。
  • 祝日の正は内閣府「国民の祝日について」。毎年 2 月に翌年分が公示される。式は表を読むだけで、祝日を決めない。
  • 着日の正は運送会社のサービス(地域別の所要日数・離島・繁忙期や天候の遅れ)。規則から出るのは「規則どおりなら」の予定日で、遅延は式の外。

6. 落とし穴

表と出力の落とし穴:

  • 会社差。日祝も配達する会社がある。土曜も休配なら sunday の行を satSun = everyDay |> filter(d => weekday(d) == Sat or weekday(d) == Sun) に替え、noDelivery の sunday を satSun にする。
  • 年次更新の時期。祝日は 2 月に公示されるので、そこで翌年分を足す。忘れても、秋以降に翌年へかかる範囲(着日・年末年始の帯)を出すたびに ⚠ と残走路のマイナスが出る。黙って 1/1 を営業日にはしない。
  • --tz を書かない。機械のタイムゾーンで印字され、日付に時刻が付くことがある。定義の tz と揃える。

着日まで書くときの落とし穴:

  • 受注日が休配日のとき。着日は shift(+N, unit: deliveryDay) で数えるが、入力が営業日でないとエラーで止まる。roll(Following, on: deliveryDay) で翌営業日へ寄せてから数える(roll と shift の回)。
  • 評価範囲の端。shift で範囲の外へ動いた点は出ない(window-clip の警告)。着日を出すときは、範囲を N 営業日ぶん広げて聞く。

受注の締め時刻(15 時以降は翌営業日出荷)・定期便の集荷(火・金、休配日は翌営業日)・月末の棚卸(月末最終営業日)も、同じ営業日の列から書ける。書き方と実走結果は物流レシピ: 休配日と着日にまとめた。

7. 動かす

再生成はコマンド 1 本なので、毎朝「今日は営業日か」を聞く型もそのまま使える。月末最終営業日の自動実行まで通すの型 1、Windows ならWindows Update の回の §6 のガイドが同じ定義ファイルで動く。マスタの再生成は年 1 回、日々の判定は同じ定義——書く場所が 1 つになる。

まとめ

  • 営業日マスタは残す。埋める作業を「規則を 1 本書き、毎年の表を足し、再生成する」に替える
  • 規則は deliveryDay = everyDay \ (sunday | holidays | closed)。日曜は規則、祝日と休業日は表+covering:
  • 表が尽きた先も点は出るが、⚠ 範囲外 と残走路のマイナスで黙らない。1/1 を営業日にして年を越す前に表を延ばす
  • 再生成は --tsv。旧マスタとの差分が「追加 365 行・削除 0」なら検算は済み
  • 休配日の正は運送会社の告知、祝日の正は内閣府。式は表を読むだけで、決めない

▶ Playground で定義を実行する——2027 年まで延ばした表で、12/26 から 1/7 までの営業日 7 点が出る。コメントのとおり 2027 年の行を消して covering: を 2026..2026 に戻すと、§3 の 9 点と ⚠ に戻る。ブラウザ内で完結し、何も送信しない。


Kairos はスケジュール定義言語です——決めたら、いつでも。仕様と参照実装はGitHub で公開しています。

コメント

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