【Java】祝日判定サンプル

# 概要
平日のみbatchを稼働させたくて、土日はcronで簡単に除外できるけど祝日はサクッと除外できないのでJava側で祝日判定した。
ちょっと検索した感じJavaのサンプルがあんまり見当たらなかったので公開します。

## 注意
- 振替休日はちょっと複雑なので考慮しません
- 過去の年は考慮しません
- 2020年と2021年は特殊なので考慮しません
- 春分の日と秋分の日の計算式は1980～2099年のみ有効

# 環境
- Java 1.8
- SpringBoot 2.2.1.RELEASE

# 1. 祝日の定義classを作成
- 固定日付、ハッピーマンデー、春分の日、秋分の日の4パターン

```PublicHoliday.java
package com.tamorieeeen.sample.consts;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * 祝日
 *
 * @author tamorieeeen
 *
 */
@Getter
@AllArgsConstructor
public enum PublicHoliday {

    NEWYEAR("元日", "fixed", 1, 1, 0),
    ADULT("成人の日", "monday", 1, 0, 2),
    FOUNDATION("建国記念の日", "fixed", 2, 11, 0),
    BIRTHDAY("天皇誕生日", "fixed", 2, 23, 0),
    SPRING("春分の日", "spring", 3, 0, 0),
    SHOWA("昭和の日", "fixed", 4, 29, 0),
    CONSTITUTION("憲法記念日", "fixed", 5, 3, 0),
    GREEN("みどりの日", "fixed", 5, 4, 0),
    CHILDREN("こどもの日", "fixed", 5, 5, 0),
    SEA("海の日", "monday", 7, 0, 3),
    MOUNTAIN("山の日", "fixed", 8, 11, 0),
    AGED("敬老の日", "monday", 9, 0, 3),
    AUTUMN("秋分の日", "autumn", 9, 0, 0),
    SPORTS("スポーツの日", "monday", 10, 0, 2),
    CULTURE("文化の日", "fixed", 11, 3, 0),
    THANKSGIVING("勤労感謝の日", "fixed", 11, 23, 0);

    private String name;
    // fixed: 日付固定
    // monday: 第何月曜日
    // spring: 春分の日
    // autumn: 秋分の日
    private String type;
    // 月
    private int month;
    // 日付型: 日
    private int dayOfMonth;
    // 第何月曜日型: 第何か
    private int weekOfMonth;

    /**
     * 固定祝日を取得
     */
    public static List<PublicHoliday> getFixedHoliday() {

        return Stream.of(values())
                .filter(v -> v.type.equals("fixed"))
                .collect(Collectors.toList());
    }

    /**
     * ハッピーマンデーを取得
     */
    public static List<PublicHoliday> getHappyMonday() {

        return Stream.of(values())
                .filter(v -> v.type.equals("monday"))
                .collect(Collectors.toList());
    }

    /**
     * 春分の日を取得
     */
    public static PublicHoliday getSpringDay() {

        return SPRING;
    }

    /**
     * 秋分の日を取得
     */
    public static PublicHoliday getAutumnDay() {

        return AUTUMN;
    }
}

```

# 2. 祝日判定classを作成
- 第何月曜日は`Calendar.DAY_OF_WEEK_IN_MONTH`を使う
- 春分の日: `int(20.8431+0.242194*(年-1980)-int((年-1980)/4))`
- 秋分の日: `int(23.2488+0.242194*(年-1980)-int((年-1980)/4))`

```HolidayService.java
package com.tamorieeeen.sample.service;

import java.time.LocalDate;
import java.util.Calendar;
import org.springframework.stereotype.Service;

import com.tamorieeeen.sample.consts.PublicHoliday;

/**
 *
 * @author tamorieeeen
 *
 */
@Service
public class HolidayService {

    // 1年は365.242194日なので、1年あたり0.242194日進む
    private static final double DIFF_OF_YEAR = 0.242194;
    // うるう年であった1980年から計算
    private static final int LEAP_YEAR = 1980;
    // 1980年の春分の時刻「20.8431日」
    private static final double SPRING_DAY_BASE = 20.8431;
    // 1980年の秋分の時刻「23.2488日」
    private static final double AUTMN_DAY_BASE = 23.2488;

    /**
     * 祝日かどうか
     */
    public boolean isHoliday() {

        LocalDate today = LocalDate.now();

        if (this.isFixedHoliday(today) || this.isHappyMonday(today) ||
                this.isSpringDay(today) || this.isAutmnDay(today)) {

            return true;
        }

        return false;
    }

    /**
     * 固定祝日かどうか
     */
    private boolean isFixedHoliday(LocalDate today) {

        return PublicHoliday.getFixedHoliday()
                .stream()
                .map(h -> LocalDate.of(
                        today.getYear(), h.getMonth(), h.getDayOfMonth()))
                .anyMatch(h -> h.isEqual(today));
    }

    /**
     * ハッピーマンデーかどうか
     */
    private boolean isHappyMonday(LocalDate today) {

        return PublicHoliday.getHappyMonday()
                .stream()
                .map(h -> LocalDate.of(
                        today.getYear(), h.getMonth(),
                        this.getDayOfMonth(h.getMonth(), h.getWeekOfMonth())))
                .anyMatch(h -> h.isEqual(today));
    }

    /**
     * ハッピーマンデーの日付を取得
     */
    private int getDayOfMonth(int month, int weekOfMonth) {

        Calendar cal = Calendar.getInstance();
        cal.set(Calendar.MONTH, month - 1);
        cal.set(Calendar.DAY_OF_WEEK, 2);
        cal.set(Calendar.DAY_OF_WEEK_IN_MONTH, weekOfMonth);

        return cal.get(Calendar.DAY_OF_MONTH);
    }

    /**
     * 春分の日かどうか
     */
    private boolean isSpringDay(LocalDate today) {

        int dayOfMonth =
                this.getMovedDays(today.getYear(), SPRING_DAY_BASE)
                - this.getLeapYearDays(today.getYear());
        LocalDate springDay = LocalDate.of(
                today.getYear(),
                PublicHoliday.getSpringDay().getMonth(),
                dayOfMonth);

        return today.isEqual(springDay);
    }

    /**
     * 秋分の日かどうか
     */
    private boolean isAutmnDay(LocalDate today) {

        int dayOfMonth =
                this.getMovedDays(today.getYear(), AUTMN_DAY_BASE)
                - this.getLeapYearDays(today.getYear());
        LocalDate autmnDay = LocalDate.of(
                today.getYear(),
                PublicHoliday.getAutumnDay().getMonth(),
                dayOfMonth);

        return today.isEqual(autmnDay);
    }

    /**
     * 進んでる分を取得
     */
    private int getMovedDays(int year, double baseDay) {

        return (int) Math.floor(baseDay + this.getDiffDays(year));
    }

    /**
     * 1980年からの差分を取得
     */
    private double getDiffDays(int year) {

        return DIFF_OF_YEAR * (year - LEAP_YEAR);
    }

    /**
     * うるう年の分を取得
     */
    private int getLeapYearDays(int year) {

        return (int) Math.floor((year - LEAP_YEAR) / 4.0);
    }
}
```

# 3. 呼び出しclassを作成
- `holidayService.isHoliday()`がtrueなら祝日、falseなら平日

```SampleController.java
package com.tamorieeeen.sample.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import com.tamorieeeen.sample.service.HolidayService;

import lombok.extern.slf4j.Slf4j;

/**
 *
 * @author tamorieeeen
 *
 */
@Slf4j
@Controller
public class SampleController {

    @Autowired
    private HolidayService holidayService;

    @GetMapping("/sample")
    public String top() {

        if (holidayService.isHoliday()) {
            log.info("today is holiday.");
        } else {
            log.info("today is weekday.");
        }

        return "sample";
    }
}
```

# 感想
春分の日と秋分の日の計算がめんどくさい！

# 参考
[内閣府「国民の祝日」について](https://www8.cao.go.jp/chosei/shukujitsu/gaiyou.html)
[「今月の第3月曜日」の日付を求める](http://orangeclover.hatenablog.com/entry/20091230/1262158033)
[PHPで春分の日、秋分の日を計算](http://php.o0o0.jp/article/php-equinox_day)