0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

日本の祝日を自前で計算する(振替休日と国民の休日でつまずいた)

0
Posted at

背景

営業日を数えるツールが自分の運営サイトに無くて、たまに困っていました。

「3営業日後っていつだっけ」を毎回カレンダーで指折り数えていて、効率は悪いです…。

作るにあたって最初に考えたのは、祝日をどこから取ってくるかでした。

祝日APIはいくつかありますが、外部への通信を1本増やすとオフラインで動かなくなります。
私のツールは全部ブラウザ内で完結させる方針なので、通信を足したくありませんでした。

というわけで自前で計算することにしたのですが、ここが思ったより厄介でした。
備忘録として書いておきます。

結論から

できたものは下記に置いてあります。

入力欄に起算日と営業日数を入れると、土日と祝日を飛ばした日付が出ます。
マイナスを入れると逆算します。

祝日は3種類ある

まず用語の整理からします。

日本の「休みの日」は、法律上3つに分かれています。

  • 国民の祝日 … 元日、成人の日、憲法記念日など。「国民の祝日に関する法律」に列挙されているもの。
  • 振替休日 … 祝日が日曜と重なったとき、その後の「祝日でない日」が休みになる。
  • 国民の休日 … 祝日と祝日に平日1日だけが挟まれたとき、その平日が休みになる。

この3つ目を私は知りませんでした。

2026年の9月がまさにこれで、21日が敬老の日、23日が秋分の日、間の22日(火)が国民の休日になります。

つまり法律に書いてある祝日だけを実装すると、9月22日を営業日として数えてしまうわけです。

実装:まず「法定の祝日」だけの表を作る

コードは1つの関数にまとめました。jpHolidays(year) は年を受け取って、{"2026-09-23": "秋分の日", ...} という形のオブジェクトを返します。キーは YYYY-MM-DD の文字列です。

まず法定の祝日だけを base に入れます。

function jpHolidays(y){
  var base = {};                              // 法定の祝日だけ。振替の判定に使う
  function put(d, name){ base[ymd(d)] = name; }   // ymd() は Date を "YYYY-MM-DD" にする自作関数

  put(new Date(y,0,1), '元日');
  put(nthDow(y,1,2,1), '成人の日');            // 1月の第2月曜
  put(new Date(y,1,11), '建国記念の日');
  put(new Date(y,2,shunbunDay(y)), '春分の日');
  // ...(以下、他の祝日も同じ形で)

nthDow(y, month, nth, dow) は「その月の第 nth ○曜日」を返す自作関数で、月の1日の曜日から必要な日数を足しているだけです。ハッピーマンデー制度で移動する祝日(成人の日・海の日・敬老の日・スポーツの日)がこれで書けます。

function nthDow(y, month1, nth, dow){
  var first = new Date(y, month1-1, 1);
  var shift = (dow - first.getDay() + 7) % 7;
  return new Date(y, month1-1, 1 + shift + (nth-1)*7);
}

春分の日と秋分の日は日付が年ごとに動きます。天文計算をするわけにはいかないので、よく使われている近似式を使いました。

function shunbunDay(y){ return Math.floor(20.8431 + 0.242194*(y-1980) - Math.floor((y-1980)/4)); }

2026年を入れると Math.floor(20.8431 + 11.1409 - 11) で 20 が返り、3月20日になります。実際の2026年の春分の日と一致します。
この式は1980年から2099年で有効とされているので、ツール側の対応範囲もそこに合わせました。

実装:振替休日は「次の祝日でない日」まで進める

ここが1つ目のつまずきです。

振替休日を「日曜の祝日の翌日」と書くと間違えます。翌日も祝日だったら、さらにその次に押し出されるからです。

2026年の5月がその例で、5月3日(憲法記念日)が日曜です。翌4日はみどりの日、5日はこどもの日で、どちらも祝日です。
なので振替休日は**5月6日(水)**になります。

コードでは、祝日でない日に当たるまで進めます。

  // base(法定の祝日だけ)を回して、日曜のものを探す
  Object.keys(base).sort().forEach(function(k){
    var d = mkDate(k);                 // "YYYY-MM-DD" → Date
    if(d.getDay() !== 0) return;       // 日曜でなければ振替なし
    var n = addDays(d, 1);
    while(base[ymd(n)]) n = addDays(n, 1);   // 祝日が続く限り先へ進める
    all[ymd(n)] = '振替休日';
  });

判定に使っているのが base(法定の祝日だけ)で、書き込む先が all(結果)である点が大事です。
同じオブジェクトを見ながら書き込むと、いま足した振替休日を「祝日」と見て、さらに次の日へ押し出してしまいます。

実装:国民の休日は「祝日・平日・祝日」の並びを探す

2つ目のつまずきがこれです。

条件は「前後が祝日で、真ん中が祝日でない平日」です。土日は最初から休みなので対象外にします。

  Object.keys(base).forEach(function(k){
    var d = mkDate(k);
    var mid = addDays(d, 1), next = addDays(d, 2);
    if(base[ymd(next)] && !base[ymd(mid)] && mid.getDay() !== 0 && mid.getDay() !== 6){
      if(!all[ymd(mid)]) all[ymd(mid)] = '国民の休日';
    }
  });

これも base を見て all に書きます。振替休日を含めた all を見てしまうと、振替休日と祝日に挟まれた平日まで国民の休日にしてしまい、実在しない休みが生まれます。

2026年で動かすと、9月22日が 国民の休日 として出ます。年間の休みは18日になりました。

例外は素直に埋め込んだ

2020年と2021年は、東京五輪に合わせて海の日・スポーツの日・山の日が移動しています。
2019年には即位の日と即位礼正殿の儀の日が休みになりました。天皇誕生日も2018年までは12月23日、2020年からは2月23日です。

こういうものは規則で書けないので、年で分岐して素直に上書きしました。

  if(y === 2020){
    put(new Date(y,6,23), '海の日');
    put(new Date(y,6,24), 'スポーツの日');
    put(new Date(y,7,10), '山の日');
  } else if(y === 2021){
    // ...
  } else {
    put(nthDow(y,7,3,1), '海の日');
    put(new Date(y,7,11), '山の日');
  }

無理に一般化しようとすると、かえって読めなくなる気がします。
実際の運用でも過去の日付を数えることはあるので、消さずに残しました。

テストは「知っている年」で書く

祝日の計算は、目で見て正しいかどうかが分かりません。
なので手元で確認できる年をテストに固定しました。

test("jpHolidays: 敬老の日と秋分に挟まれた平日は国民の休日(2026-09-22)", () => {
  const h = t.jpHolidays(2026);
  assert.equal(h["2026-09-21"], "敬老の日");
  assert.equal(h["2026-09-22"], "国民の休日");
  assert.equal(h["2026-09-23"], "秋分の日");
});

2026年の9月と5月、2020年と2021年の移動分、2018年と2020年の天皇誕生日。
この5つを押さえておけば、上に書いた「つまずき」は全部拾えます。

営業日の加減算のほうも、起算日を数えないことと、休みの日が起算日でも翌日から数え始めることを固定しました。

まとめの代わりに

祝日APIを使えば5分で終わる話ではあります。

それでも自前で書いてよかったのは、振替休日と国民の休日を「あとから足す」順序を間違えると、実在しない休みが増えるという構造に気づけたことでした。
外部APIを使っていたら、この2つが別の概念だということすら知らないままだったと思います。

日付まわりを自分で書くときは、法律に列挙されているものと、そこから派生して決まるものを、別々の表に分けて持ってください。同じ表に混ぜた瞬間に壊れます。


本記事はAI補助で執筆した、個人開発の紹介記事です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?