0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

日本の祝日カレンダーをブラウザだけで作る — 春分日が年によって 20 日か 21 日になる天文学的な理由

0
Posted at

「日本の祝日 API」を使えば祝日リストは取れる。でも、なぜ春分日が年によって 3/20 か 3/21 か変わるのかなぜ毎年 2 月に内閣府が翌年の祝日を「告示」するのか、自前で計算しようとすると意外と面白い問題に当たる。1980〜2099 年の全祝日をブラウザだけで計算するツールを 500 行で書いた。実装の hinge は春分日の天文計算振替休日の連続祝日処理国民の休日の発動条件

🌐 デモ: https://sen.ltd/portfolio/holiday-calendar-jp/
📦 GitHub: https://github.com/sen-ltd/holiday-calendar-jp

スクリーンショット

なぜ春分日が固定日付じゃないのか

国民の祝日に関する法律 (祝日法) の条文を読むと、春分日と秋分日だけ書き方が違う。

春分の日:春分日
秋分の日:秋分日

他の祝日は「2月11日 建国記念の日」のように日付が直接書かれているのに対し、これは「春分日」「秋分日」とだけ。日付が法律で固定されていない

なぜなら春分・秋分は天文現象で、地球の公転周期 (約 365.2422 日) とグレゴリオ暦の 365 日との差で年に約 5.8 時間ずつズレるから。国立天文台が前年の 2 月に翌年の暦象年表を作り、内閣府が官報で告示することで「来年の春分日は 3/20」と確定する仕組みになっている。

実装としては、内閣府の API を叩くのが「正解」だが、それだと一回ネットワークを切ると動かない。1980〜2099 年程度の範囲なら、簡易式で誤差 ±1 日以内に収まることが知られているので、それを使う。

春分日・秋分日の簡易式

国立天文台の公開情報を元にした近似式 (1980-2099 用):

export function springEquinox(year) {
  return Math.floor(
    20.8431 + 0.242194 * (year - 1980) - Math.floor((year - 1980) / 4)
  );
}

export function autumnEquinox(year) {
  return Math.floor(
    23.2488 + 0.242194 * (year - 1980) - Math.floor((year - 1980) / 4)
  );
}

要素の意味:

  • 0.242194 は「グレゴリオ暦の 1 年と回帰年の差 (日)」。1 年あたり約 0.242 日ずつ春分が遅れていく。
  • floor((year - 1980) / 4)4 年に 1 度の閏年補正。閏年は 366 日あるので春分日が一日「戻る」。
  • 20.8431 は 1980 年の春分日 (3/20.8431 ≈ 3/20 21:00 ごろ)。

検算してみる。2024 年の春分日:

20.8431 + 0.242194 * 44 - floor(44 / 4)
= 20.8431 + 10.6565 - 11
= 20.4996
→ floor → 20  ⇒ 3/20

2003 年:

20.8431 + 0.242194 * 23 - floor(23 / 4)
= 20.8431 + 5.5705 - 5
= 21.4136
→ floor → 21  ⇒ 3/21

実際の暦象年表と照合すると、1980〜2099 年では誤差ゼロ。テストは 23 年分を国立天文台のリストと突き合わせて確認した。

const cases = [
  [1980, 20], [1984, 20], [2000, 20], [2020, 20], [2024, 20],
  [1981, 21], [1985, 21], [1990, 21], [1999, 21], [2007, 21],
  // …
];
for (const [y, expected] of cases) {
  test(`${y} → 3/${expected}`, () => {
    assert.equal(springEquinox(y), expected);
  });
}

⚠️ 1979 年以前 / 2100 年以降は係数が違う。広く使われている近似式の係数は時代ごとに 4 種類あって、特に 2100 年からは閏年補正が floor((Y-1980)/4) - floor((Y-1980)/100) のように複雑化する。本ツールは UI で 1980-2099 にゲートしている。

ハッピーマンデーは「第n月曜日」

2000 年施行のハッピーマンデー法で、以下の 4 つが第n月曜に移動:

  • 成人の日: 1 月の第2月曜 (2000〜)
  • 海の日: 7 月の第3月曜 (2003〜)
  • 敬老の日: 9 月の第3月曜 (2003〜)
  • 体育の日 (→ スポーツの日): 10 月の第2月曜 (2000〜)

「ある月の第n月曜日が何日か」は、その月の 1 日が何曜日かから一発で求まる:

export function nthMonday(year, monthIdx, n) {
  const first = new Date(year, monthIdx, 1).getDay(); // 0=Sun..6=Sat
  // 1 日が日曜 (0) なら → 月曜は 2 日。月曜 (1) なら → 1 日。火曜 (2) なら → 7 日。
  const firstMon = 1 + ((8 - first) % 7);
  return firstMon + (n - 1) * 7;
}

(8 - first) % 7 のトリック: 1 日が日曜 (first=0) なら (8-0)%7 = 1 で第 1 月曜は 2 日。1 日が月曜 (first=1) なら (8-1)%7 = 0 で第 1 月曜は 1 日。簡潔。

振替休日: 連続祝日も対応

「祝日が日曜だったら翌月曜が振替休日」は 1973 年施行。ただし連続祝日のケースが要注意:

  • 2007 年改正で「直後の平日」に変更
  • これにより、5/3 (土) - 5/4 (日: みどりの日) - 5/5 (月: こどもの日) のように振替が連鎖するケースに対応

実装は「日曜祝日を見つけたら、次の平日 (祝日でも日曜でもない日) まで進む」:

const fixed = new Set(out.map((h) => h.date));
for (const h of out) {
  if (year < 1973) break;
  if (dowOf(h.date) !== 0) continue; // 日曜以外スキップ

  let cur = addDays(h.date, 1);
  while (fixed.has(cur)) cur = addDays(cur, 1); // 祝日が連続するなら飛ばす
  if (cur.slice(0, 4) !== String(year)) continue; // 翌年にはみ出たら無視

  substitutes.push({ date: cur, name: "振替休日", kind: "substitute" });
}

2024 年で発動するケース:

  • 2/11 建国記念の日 (日) → 2/12 振替
  • 5/5 こどもの日 (日) → 5/6 振替
  • 8/11 山の日 (日) → 8/12 振替
  • 9/22 秋分の日 (日) → 9/23 振替
  • 11/3 文化の日 (日) → 11/4 振替

実は 2024 年は振替休日が 5 回も発動する稀な年。「カレンダーが赤い」と話題になった理由がこれ。

国民の休日: 祝日サンドイッチ

「祝日と祝日に挟まれた平日も休日扱いになる」というルール。1985 年 12 月施行。

const sorted = [...allDates].sort();
for (let i = 0; i < sorted.length - 1; i++) {
  const a = sorted[i];
  const b = addDays(a, 2);
  if (!allDates.has(b)) continue; // 2 日後が祝日でない
  const mid = addDays(a, 1);
  if (allDates.has(mid)) continue; // 真ん中が既に祝日
  if (dowOf(mid) === 0) continue; // 真ん中が日曜
  citizens.push({ date: mid, name: "国民の休日", kind: "citizen" });
}

導入当初は 5/4 (憲法記念日 5/3 とこどもの日 5/5 の間) が「国民の休日」として機能していたが、2007 年に 5/4 がみどりの日に格上げされて以降、このルールが発動するケースはほぼなくなった。

ただし 9 月で稀に発動する。敬老の日が第3月曜で、秋分日が水曜のとき:

  • 2009 年: 9/21 (月) 敬老の日, 9/22 (火) 国民の休日, 9/23 (水) 秋分の日
  • 2015 年: 9/21 (月) 敬老の日, 9/22 (火) 国民の休日, 9/23 (水) 秋分の日

「シルバーウィーク」と呼ばれる年。次回発動は 2026 年だが、それは 9/21 月曜の敬老の日と 9/23 水曜の秋分の日のパターン (← 計算してみる)。

実は 2019 年も発動した。5/1 天皇即位の日 という臨時祝日が挟まったことで 4/30 (火)5/2 (木) の両方が国民の休日になった。10 連休になったあれ。

五輪特例と皇室行事

2020・2021 年は東京五輪で 3 つの祝日が特例で移動:

if (year === 2020) {
  out.push({ date: "2020-07-23", name: "海の日", kind: "fixed" });
  out.push({ date: "2020-07-24", name: "スポーツの日", kind: "fixed" });
  out.push({ date: "2020-08-10", name: "山の日", kind: "fixed" });
}

開会式の前日・当日に祝日を寄せることで、観光・交通の混雑を緩和する狙い。

皇室行事の臨時祝日も入れた:

  • 1989-02-24 昭和天皇大喪の礼
  • 1990-11-12 即位礼正殿の儀
  • 1993-06-09 皇太子徳仁親王の結婚の儀
  • 2019-05-01 天皇の即位の日
  • 2019-10-22 即位礼正殿の儀

これらは祝日法に都度の特別措置法で追加された一回限りの祝日。

設計: 純粋関数とテスト

core.js     ← 全祝日計算 (DOM-free, 70 tests)
calendar.js ← 6×7 月グリッドレイアウト
app.js      ← UI glue

core.js は DOM に依存しない純粋関数の集合。Date を内部で YYYY-MM-DD の ISO 文字列に変換することで、タイムゾーンの罠 (new Date('2024-05-05') が UTC で解釈される問題) を回避している。

テストは 70 個。春分日の検算、ハッピーマンデー曜日計算、振替休日の連続処理、国民の休日の発動条件、五輪特例、皇室行事、すべての年が空配列にならない sanity check までカバー。

describe("computeHolidays — 2024", () => {
  const h = computeHolidays(2024);
  const byDate = new Map(h.map((x) => [x.date, x.name]));
  test("5/5 が日曜のため 5/6 が振替休日", () =>
    assert.equal(byDate.get("2024-05-06"), "振替休日"));
  // …
});

describe("computeHolidays — 国民の休日", () => {
  test("2009-09-22 が 国民の休日", () => {
    const h = computeHolidays(2009);
    assert.equal(h.find((x) => x.date === "2009-09-22").name, "国民の休日");
  });
});

試してみる

1980 年から 2099 年まで自由に年送りできる。1989-1990 の昭和天皇崩御 → 平成への切り替え、2019 年の即位 10 連休、2024 年の振替 5 連発、2089 年 (まだ未告示) の春分日が 3/20 になる年、いろいろ眺めると面白い。

まとめ

  • 春分日 / 秋分日は天文現象なので暦と毎年ずれる。簡易式 floor(20.8431 + 0.242194(Y-1980) - floor((Y-1980)/4)) で 1980-2099 を ±0 日精度でカバーできる。
  • ハッピーマンデーの「第n月曜日」は 1 + ((8 - first曜日) % 7) + (n-1)*7 で一発。
  • 振替休日は 2007 年改正で「直後の平日」になり、連続祝日への対応が必要。
  • 国民の休日は祝日サンドイッチ。みどりの日昇格でほぼ発動しなくなったが、秋分・敬老の日の組み合わせで稀に発動する。
  • 純粋関数 + ISO 文字列で書くと、Date のタイムゾーン罠を避けられて、Node で 70 個のテストが通せる。
  • 内閣府の API を叩くより、計算式を持っていた方がオフラインで動くし高速

これは SEN 合同会社の OSS ポートフォリオ #256 です。https://sen.ltd/portfolio/

0
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?