振替休日と国民の休日をJSで計算し内閣府CSVと照合する

「N 営業日後」を出す処理を書くと、最初につまずくのが祝日の扱いだ。元日や文化の日のような日付固定の祝日だけなら表を持てば済むが、日本の休日には祝日から計算で決まる日が 2 種類ある。日曜と重なったときの振替休日と、祝日に挟まれた平日の国民の休日である。

この記事では、その 2 つを祝日法の条文どおりに JavaScript で書き、内閣府が公開している祝日 CSV と突き合わせる。実行環境は Windows 11 上の Node.js v24.11.1。

条文は 3 行しかない

国民の祝日に関する法律(昭和 23 年法律第 178 号)の第 3 条は、e-Gov 法令検索で次のとおりになっている(2026-10-27 確認)。

「国民の祝日」が日曜日に当たるときは、その日後においてその日に最も近い「国民の祝日」でない日を休日とする。(第 2 項)

その前日及び翌日が「国民の祝日」である日(「国民の祝日」でない日に限る。)は、休日とする。(第 3 項)

第 1 項は「国民の祝日は休日とする」なので、休日は次の 3 種類に分かれる。

種類 根拠 2026 年の例
国民の祝日 第 2 条で日付や「第 2 月曜日」などが決まっている 1/1 元日、9/21 敬老の日
振替休日(第 3 条第 2 項) 祝日が日曜のとき、その後で最も近い祝日でない日 5/6(5/3 憲法記念日が日曜)
国民の休日(第 3 条第 3 項) 前日と翌日がどちらも祝日である、祝日でない日 9/22(9/21 敬老の日と 9/23 秋分の日の間)

実装で大事なのは、振替休日も国民の休日も「国民の祝日」だけを見て決まる点だ。振替休日を祝日として扱って国民の休日を判定したり、その逆をしたりすると条文とずれる。

2026 年 5 月がよい例になる。5/3 が日曜なので翌日に振り替えたいが、5/4 みどりの日と 5/5 こどもの日も祝日なので、最も近い「祝日でない日」は 5/6(水)になる。月曜とは限らない。

実際に試す: 祝日・休日を計算する

次を jp-holidays.mjs として保存する。日付は Date.UTC で作り getUTC* で読むので、実行するマシンのタイムゾーンに左右されない。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// jp-holidays.mjs : 日本の祝日・休日を計算する
const z = (n) => String(n).padStart(2, '0');
// 月は 1 始まり。Date.UTC で作り、getUTC* で読む(タイムゾーンの影響を受けない)
const key = (d) => `${d.getUTCFullYear()}-${z(d.getUTCMonth() + 1)}-${z(d.getUTCDate())}`;
const day = (y, m, d) => new Date(Date.UTC(y, m - 1, d));
const addDays = (d, n) => new Date(d.getTime() + n * 86400000);
// m 月の第 nth 月曜日
const nthMonday = (y, m, nth) => {
const first = day(y, m, 1);
return day(y, m, 1 + ((8 - first.getUTCDay()) % 7) + (nth - 1) * 7);
};
// 春分日・秋分日の近似式(1980〜2099 年)
const shunbun = (y) => Math.floor(20.8431 + 0.242194 * (y - 1980) - Math.floor((y - 1980) / 4));
const shubun = (y) => Math.floor(23.2488 + 0.242194 * (y - 1980) - Math.floor((y - 1980) / 4));

export function holidays(y) {
const base = new Map(); // 国民の祝日だけ
const put = (d, name) => base.set(key(d), name);
put(day(y, 1, 1), '元日');
put(nthMonday(y, 1, 2), '成人の日');
put(day(y, 2, 11), '建国記念の日');
if (y >= 2020) put(day(y, 2, 23), '天皇誕生日');
if (y <= 2018) put(day(y, 12, 23), '天皇誕生日');
put(day(y, 3, shunbun(y)), '春分の日');
put(day(y, 4, 29), '昭和の日');
put(day(y, 5, 3), '憲法記念日');
put(day(y, 5, 4), 'みどりの日');
put(day(y, 5, 5), 'こどもの日');
if (y === 2020) {
put(day(y, 7, 23), '海の日'); put(day(y, 7, 24), 'スポーツの日'); put(day(y, 8, 10), '山の日');
} else if (y === 2021) {
put(day(y, 7, 22), '海の日'); put(day(y, 7, 23), 'スポーツの日'); put(day(y, 8, 8), '山の日');
} else {
put(nthMonday(y, 7, 3), '海の日');
put(day(y, 8, 11), '山の日');
put(nthMonday(y, 10, 2), y >= 2020 ? 'スポーツの日' : '体育の日');
}
put(nthMonday(y, 9, 3), '敬老の日');
put(day(y, 9, shubun(y)), '秋分の日');
put(day(y, 11, 3), '文化の日');
put(day(y, 11, 23), '勤労感謝の日');
if (y === 2019) {
put(day(y, 5, 1), '天皇の即位の日');
put(day(y, 10, 22), '即位礼正殿の儀の行われる日');
}

const all = new Map(base);
// 振替休日: 祝日が日曜なら、その後で最も近い「祝日でない日」を休日にする
for (const k of [...base.keys()].sort()) {
const d = new Date(k + 'T00:00:00Z');
if (d.getUTCDay() !== 0) continue;
let n = addDays(d, 1);
while (base.has(key(n))) n = addDays(n, 1);
all.set(key(n), '振替休日');
}
// 国民の休日: 前日と翌日がどちらも祝日である、祝日でない日
for (const k of base.keys()) {
const d = new Date(k + 'T00:00:00Z');
const mid = addDays(d, 1);
if (base.has(key(addDays(d, 2))) && !all.has(key(mid))) all.set(key(mid), '国民の休日');
}
return all;
}

振替休日の while と国民の休日の判定は、どちらも base(国民の祝日だけの表)を見ている。これが条文の「『国民の祝日』でない日」に対応する。

2020 年と 2021 年は東京五輪のため海の日・スポーツの日・山の日が移動し、2019 年は即位に関する祝日が加わったので、個別に書いている。

実際に試す: 内閣府の CSV と突き合わせる

内閣府の「国民の祝日」について のページは、1955 年から 2027 年までの祝日・休日を CSV で配っている。2026-10-27 に取得したファイルは 21,538 バイト、文字コードは Shift_JIS で、1 行目が見出し、2 行目以降が 2026/5/6,休日 の形だった。振替休日も国民の休日も名称は「休日」になっている。

1
curl -o syukujitsu.csv https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv

Node.js の TextDecoder は ICU を含む通常のビルドなら shift_jis を読める。次を check-csv.mjs として同じフォルダに置く。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// check-csv.mjs : holidays() の結果を内閣府の syukujitsu.csv(Shift_JIS)と比べる
import { readFileSync } from 'node:fs';
import { holidays } from './jp-holidays.mjs';

const [file = 'syukujitsu.csv', from = '2016', to = '2027'] = process.argv.slice(2);
const csv = new TextDecoder('shift_jis').decode(readFileSync(file));
const z = (n) => String(n).padStart(2, '0');
const official = new Map();
for (const line of csv.split(/\r?\n/).slice(1)) {
if (!line) continue;
const [date, name] = line.split(',');
const [y, m, d] = date.split('/').map(Number);
if (y >= +from && y <= +to) official.set(`${y}-${z(m)}-${z(d)}`, name);
}

let diff = 0;
for (let y = +from; y <= +to; y++) {
const mine = holidays(y);
for (const [k, v] of mine) if (!official.has(k)) { diff++; console.log('余分', k, v); }
for (const [k, v] of official) if (k.startsWith(y + '-') && !mine.has(k)) { diff++; console.log('不足', k, v); }
}
console.log(`${from}〜${to}年: 内閣府 ${official.size} 日 / 差分 ${diff} 件`);
1
node check-csv.mjs syukujitsu.csv 2016 2027
1
2016〜2027年: 内閣府 219 日 / 差分 0 件

2016 年から 2027 年までの 219 日が、名称の違いを除いて一致した。この期間の CSV で「休日」になっているのは 2019/4/30、2019/5/2、2024/9/23、2026/5/6、2026/9/22、2027/3/22 などで、どれも上のコードが振替休日か国民の休日として出している。

2015 年以前を比べると合わない

範囲を広げると差分が出る。

1
node check-csv.mjs syukujitsu.csv 1998 2009

出力の一部を抜き出す(全体で 41 件)。

1
2
3
4
5
6
7
8
余分 1998-05-06 振替休日
不足 1999-01-15 成人の日
余分 2000-07-17 海の日
不足 2002-09-15 敬老の日
余分 2003-05-04 みどりの日
余分 2009-08-11 山の日
...
1998〜2009年: 内閣府 199 日 / 差分 41 件

コードの誤りではなく、祝日法が何度も改正されているためだ。e-Gov の法令データには改正法と施行日が残っている。

改正法 施行 CSV に出ている変化
平成 10 年法律第 141 号 2000-01-01 成人の日・体育の日が第 2 月曜に(1999 年までは 1/15・10/10)
平成 13 年法律第 59 号 2003-01-01 海の日・敬老の日が第 3 月曜に(2002 年までは 7/20・9/15)
平成 17 年法律第 43 号 2007-01-01 5/4 がみどりの日、4/29 が昭和の日に。振替休日が「最も近い祝日でない日」になる
平成 26 年法律第 43 号 2016-01-01 山の日(8/11)が加わる

振替休日の変化は CSV からも読める。1998 年は 5/3 が日曜だったが、休日は 5/4 だけで 5/6 は休みになっていない。当時の振替休日は日曜の翌日だけが対象で、翌日の 5/4 は国民の休日として既に休みだったためだ。現行の規定で同じ並びになった 2009 年は、5/6 が「休日」として載っている。月曜以外の日が振替休日になった最初は 2008 年 5 月 6 日(火)で、CSV にも 2008/5/6,休日 がある。

過去の日付も扱うなら、改正ごとに規則を切り替える必要がある。現行の規則だけで書くなら、山の日が始まった 2016 年以降に限るのが安全である。

春分の日と秋分の日は近似式でよいか

春分の日と秋分の日だけは法律に日付がなく、「春分日」「秋分日」とだけ書かれている。内閣府のページによると、国立天文台が毎年 2 月に翌年の春分の日と秋分の日を官報で公表する。CSV が 2027 年までしかないのはこのためで、2028 年分は 2027 年 2 月に載る予定と書かれている。

上のコードの近似式が CSV と合うかを、1980 年から 2027 年の 96 日(春分 48・秋分 48)で比べたところ、すべて一致した。ただし 2028 年以降の値は近似式による予測で、正式な日付ではない。業務で使う表は、公表のたびに CSV で上書きする作りにしておくとよい。

予測として、2028 年から 2099 年で国民の休日が出る日をこのコードで数えると、すべて 9 月で、次は 2032-09-21 と 2037-09-22 だった。敬老の日(9/15〜21 の月曜)の 2 日後が秋分の日(9/22〜24)になる年だけ、間の火曜が休日になる。

実際に試す: N 営業日後を出す

祝日表ができれば、営業日の計算は土日と表を飛ばして数えるだけになる。biz.mjs として保存する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// biz.mjs : 土日と祝日・休日を除いて N 営業日後を出す
import { holidays } from './jp-holidays.mjs';

function addBusinessDays(ymd, n) {
const cache = new Map();
const isHoliday = (d) => {
const y = d.getUTCFullYear();
if (!cache.has(y)) cache.set(y, holidays(y));
return cache.get(y).has(d.toISOString().slice(0, 10));
};
let d = new Date(ymd + 'T00:00:00Z');
while (n > 0) {
d = new Date(d.getTime() + 86400000);
const w = d.getUTCDay();
if (w !== 0 && w !== 6 && !isHoliday(d)) n--;
}
return d.toISOString().slice(0, 10);
}

console.log(addBusinessDays('2026-09-18', 3)); // シルバーウィーク前の金曜から
console.log(addBusinessDays('2026-05-01', 1)); // GW 前の金曜から
console.log(addBusinessDays('2026-12-28', 5)); // 年末年始の会社休業は入っていない
1
node biz.mjs
1
2
3
2026-09-28
2026-05-07
2027-01-05

2026-09-18(金)の 3 営業日後は、9/19〜23 の 5 連休(土・日・敬老の日・国民の休日・秋分の日)を飛ばして 9/24・9/25・9/28 と数え、9/28(月)になる。5/1(金)の翌営業日は、振替休日の 5/6 を飛ばした 5/7 だ。年末年始は法律上の休日が 1/1 だけなので、会社の休業日は別に足す必要がある。

ハシトシステムの営業日計算ツールでは

ブラウザで使える 営業日計算ツール(ハシトシステム) も、2026-10-27 時点のソースでは同じ考え方で祝日表を作っている。国民の祝日だけの表を先に作り、そこから振替休日と国民の休日を足す順序で、対応範囲は 2016〜2099 年、春分・秋分は上と同じ近似式である。ツールの祝日表を作る関数をソースから取り出して Node.js で動かし、上の CSV と 2016〜2027 年で比べたところ、差分は 0 件だった。

ツールは年末年始などの「個別の休業日」と、土日祝でも数える「特別出勤日」を入力できるので、biz.mjs で足りない会社ごとの休みはそちらで確かめられる。日付どうしの日数を数えるだけなら 日付の差を計算するツール もある。

法律の条文を日付計算に落とす例としては、4月1日生まれが早生まれになる理由を民法143条で計算する記事 も書いた。日と曜日の組み合わせで迷いやすいものでは cron の日と曜日が OR で照合される話 がある。

まとめ

  • 振替休日と国民の休日は、どちらも「国民の祝日」だけを見て決まる。振替先は月曜とは限らず、2026 年は 5/6(水)になる。
  • 条文どおりに書いたコードは、内閣府の CSV と 2016〜2027 年の 219 日で一致した(Node.js v24.11.1、CSV は 2026-10-27 取得)。
  • 2015 年以前は祝日法の改正(ハッピーマンデー、2007 年の振替休日の変更、山の日)で規則が違うため、現行の規則では合わない。
  • 春分・秋分の近似式は 1980〜2027 年で CSV と一致したが、2028 年以降は予測。国立天文台の公表後に CSV で確かめること。

参考

  • 国民の祝日に関する法律(昭和 23 年法律第 178 号)e-Gov 法令検索(第 3 条、改正履歴)
  • 内閣府「国民の祝日」について(syukujitsu.csv、振替休日と国民の休日の例、春分の日・秋分の日の公表)
  • Wikipedia「振替休日」(2007 年施行の改正と、月曜以外の最初の振替休日 2008 年 5 月 6 日)

振替休日と国民の休日をJSで計算し内閣府CSVと照合する
https://blog.hashito.biz/2026/10/27/japan-furikae-kyujitsu-kokumin-no-kyujitsu-js-cabinet-office-csv/
著者
hashito
作成日
2026年10月27日
著作権