java-recipes

ホーム 日付・時刻 › D-08

D-08: 祝日計算(振替休日・春分の日・秋分の日)

春分の日・秋分の日は天文計算不要の近似式で算出できます。 振替休日(日曜祝日の翌月曜)と国民の休日(祝日に挟まれた平日)の計算ロジックも解説します。D-04(祝日判定)で作成した祝日セットと組み合わせることで完全な休日カレンダーを構築できます。

3 つのルール

① 春分の日・秋分の日(近似計算)

国立天文台が毎年2月に官報に掲載する確定値が正式ですが、 2000〜2099年の範囲では近似式でほぼ正確に計算できます。
春分の日 = INT(20.8431 + 0.242194 × (年 - 1980) - INT((年 - 1980) / 4)) → ほぼ 3/20 または 3/21

② 振替休日

祝日が日曜日と重なったとき、翌月曜が「振替休日」になります。 複数の祝日が連続していると、振替日がさらに翌日へ繰り延べされます(例:5月3〜5日が連休のとき)。

③ 国民の休日

「祝日と祝日に挟まれた平日(日曜以外)」は国民の休日になります。 典型例:敬老の日(月)→ 火曜(国民の休日)→ 秋分の日(水)という連休が発生する年があります(2026年など)。

サンプルコード

Sample.java
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;

/**
 * 祝日計算サンプル(Java 8+)。
 * 春分・秋分の近似計算、振替休日、国民の休日を扱います。
 *
 * 注意: 春分・秋分の計算式は 2000〜2099 年に有効な近似式です。
 *       精密な天文計算は不要な場合が多く、この近似式で実用上は十分です。
 */
public class HolidayCalcSample {

    /**
     * 春分の日を計算する(2000〜2099 年の近似式)。
     * ほとんどの年で 3/20 または 3/21 になります。
     */
    public static LocalDate springEquinox(int year) {
        int day = (int)(20.8431 + 0.242194 * (year - 1980)
            - Math.floor((year - 1980) / 4.0));
        return LocalDate.of(year, 3, day);
    }

    /**
     * 秋分の日を計算する(2000〜2099 年の近似式)。
     * ほとんどの年で 9/22 または 9/23 になります。
     */
    public static LocalDate autumnEquinox(int year) {
        int day = (int)(23.2488 + 0.242194 * (year - 1980)
            - Math.floor((year - 1980) / 4.0));
        return LocalDate.of(year, 9, day);
    }

    /**
     * 振替休日を含む休日セットを計算する。
     * 「日曜が祝日のとき → 翌月曜が振替休日」のルールを繰り返し適用。
     * 振替休日が別の祝日と重なる場合、さらに翌日へ繰り延べされます。
     */
    public static Set<LocalDate> addSubstituteHolidays(Set<LocalDate> holidays) {
        Set<LocalDate> result = new TreeSet<>(holidays);
        boolean changed;
        do {
            changed = false;
            Set<LocalDate> toAdd = new TreeSet<>();
            for (LocalDate holiday : result) {
                if (holiday.getDayOfWeek() == DayOfWeek.SUNDAY) {
                    // 翌月曜から始めて、まだ祝日でない最初の平日を探す
                    LocalDate candidate = holiday.plusDays(1);
                    while (result.contains(candidate) || toAdd.contains(candidate)) {
                        candidate = candidate.plusDays(1);
                    }
                    toAdd.add(candidate);
                    changed = true;
                }
            }
            result.addAll(toAdd);
        } while (changed);
        return result;
    }

    /**
     * 国民の休日を追加する。
     * 「祝日と祝日に挟まれた平日は休日になる」ルールを適用します。
     * 例: 敬老の日(月)→ [火曜:国民の休日] → 秋分の日(水)
     */
    public static Set<LocalDate> addCitizensHolidays(Set<LocalDate> holidays) {
        Set<LocalDate> result = new TreeSet<>(holidays);
        LocalDate[] sorted = result.toArray(new LocalDate[0]);
        for (int i = 0; i < sorted.length - 1; i++) {
            LocalDate h1 = sorted[i];
            LocalDate h2 = sorted[i + 1];
            // 2日差 = 間に 1 日ある
            if (h2.equals(h1.plusDays(2))) {
                LocalDate middle = h1.plusDays(1);
                // 日曜でなく、まだ祝日でなければ国民の休日
                if (middle.getDayOfWeek() != DayOfWeek.SUNDAY
                        && !result.contains(middle)) {
                    result.add(middle);
                }
            }
        }
        return result;
    }

    /**
     * 春分・秋分を加えた祝日セットを構築し、振替休日・国民の休日も追加する。
     *
     * @param year  対象年
     * @param base  D-04 等で用意したベースの祝日セット(春分・秋分を除く)
     */
    public static Set<LocalDate> buildFullHolidays(int year, Set<LocalDate> base) {
        Set<LocalDate> holidays = new HashSet<>(base);
        holidays.add(springEquinox(year));
        holidays.add(autumnEquinox(year));
        Set<LocalDate> withSub = addSubstituteHolidays(holidays);
        return addCitizensHolidays(withSub);
    }

    public static void main(String[] args) {
        // 春分・秋分の計算
        for (int y = 2024; y <= 2027; y++) {
            System.out.printf("%d年: 春分=%s  秋分=%s%n",
                y, springEquinox(y), autumnEquinox(y));
        }

        // 2026年9月の国民の休日が発生するケースを確認
        // 敬老の日(9/21 月)と秋分の日(9/23 水)に挟まれた 9/22(火)
        Set<LocalDate> base2026 = new HashSet<>(Arrays.asList(
            LocalDate.of(2026, 9, 21),  // 敬老の日(第3月曜)
            LocalDate.of(2026, 11, 3),  // 文化の日(他の月は省略)
            LocalDate.of(2026, 11, 23)  // 勤労感謝の日
            // ... 実際は D-04 で用意した全祝日リストを渡す
        ));

        Set<LocalDate> full = buildFullHolidays(2026, base2026);

        System.out.println("\n--- 2026年9月の祝日 ---");
        for (LocalDate d : new TreeSet<>(full)) {
            if (d.getMonthValue() == 9) {
                System.out.println(d + " (" + d.getDayOfWeek() + ")");
            }
        }
        // 期待出力:
        //   2026-09-21 (MONDAY)    ← 敬老の日
        //   2026-09-22 (TUESDAY)   ← 国民の休日
        //   2026-09-23 (WEDNESDAY) ← 秋分の日(近似式より算出)
    }
}

よくあるミス・注意点

⚠️ 近似式の適用範囲は 2000〜2099 年

この近似式は 2000〜2099 年に有効です。2100 年以降や 1999 年以前に使う場合は別の係数が必要です。 業務システムで使用する際は、適用範囲をコメントに明記しておきましょう。

⚠️ 振替休日の繰り延べは繰り返し処理が必要

例:5/3(土)・5/4(日)・5/5(月)のとき、5/4(日)の振替は 5/6(火)になります。 1回のスキャンだけでは正しく計算できないため、変更がなくなるまでループを繰り返す必要があります。

⚠️ 国民の休日の判定は振替休日追加後に行う

振替休日を追加する前に国民の休日を計算すると、振替休日同士で囲まれたケースを見落とすことがあります。 必ず「振替休日追加 → 国民の休日追加」の順で処理してください。

テストする観点

  • ✅ 春分・秋分:2024〜2030 年の春分の日・秋分の日が正しいか(国立天文台の確定値と照合)
  • ✅ 振替休日(単純):日曜が祝日のとき、翌月曜が追加されるか
  • ✅ 振替休日(連続):5/3〜5/5 連休の場合、振替が 5/6 になるか
  • ✅ 国民の休日:2026年 9/22 が国民の休日として追加されるか
  • ✅ 国民の休日(不発):挟まれた日が日曜の場合、国民の休日にならないか
  • ✅ D-04 との組み合わせ:buildFullHolidays() に D-04 の祝日セットを渡して正しく動作するか

GitHub でソースコードを見る →