Rで日本の祝日、休日カレンダーを作る

## 動機

業務上、祝日や休日の表を作り、Rで言うところの****_join()をするという作業が必要になることが多いわたくしめ。今まではいちいち休日を入力していました。それだったら休日カレンダーをRで作っちゃおう！と思ったのですが…。以下まとめに続く。

## 「国民の祝日」について
[内閣府のホームページ](https://www8.cao.go.jp/chosei/shukujitsu/gaiyou.html)を参照し、祝日、休日を以下のとおり分類しました。
- 日付が固定された祝日
- 春分の日と秋分の日
- ハッピーマンデー制度による祝日
- 振替休日
- その前日及び翌日が「国民の祝日」である日（「国民の祝日」でない日に限る。）

## Rでのコーディング
日本の祝日を考える上で説明しやすいので、2026年を例にRコードを説明します。
```mazuha.R
library(conflicted)
library(tidyverse)
conflict_prefer('filter', 'dplyr')
vec_year <- c(2026)
```

### 日付が固定された祝日
これは10日あります。これは容易にコーディングできました。あくまで一例としてご覧ください。
```fixed_national_holiday.R
# 2024年現在、日が固定された祝日10日
fixed_date <- c("0101", "0211", "0223", "0429", "0503", "0504", "0505",
                "0811", "1103", "1123")
vec_fixed_national_holiday <- dplyr::tibble(date = fixed_date) |>
  mutate(date = ymd(str_c(vec_year, date)))
vec_fixed_national_holiday <- vec_fixed_national_holiday$date
```

### 春分の日と秋分の日
[【Excel Tips】春分の日や秋分の日を求めるには？](https://enterprisezine.jp/article/detail/865)を参考にしました。
春分の日の○日の計算式
```math
\left\lfloor 20.8431 + 0.242194 \times (x - 1980) \right\rfloor - \left\lfloor \frac{x - 1980}{4} \right\rfloor
```
秋分の日の○日の計算式
```math
\left\lfloor 23.2488+0.242194 \times (x - 1980) \right\rfloor - \left\lfloor \frac{x - 1980}{4} \right\rfloor
```
このように計算式が明確な場合は、プログラミングは楽ですね。
```equinox.R
#春分の日 INT(20.8431+0.242194*(自分が調べたい年-1980))-INT((自分が調べたい年-1980)/4)
vec_spring_equinox <- floor(20.8431 + 0.242194 * (vec_year - 1980) -
                      floor((vec_year - 1980) / 4))
vec_spring_equinox <- ymd(str_c(vec_year, "03", vec_spring_equinox))
#秋分の日 INT(23.2488+0.242194*(年-1980))-INT((年-1980)/4)
vec_autumn_equinox <- floor(23.2488 + 0.242194 * (vec_year - 1980) -
                      floor((vec_year - 1980) / 4))
vec_autumn_equinox <- ymd(str_c(vec_year, "09", vec_autumn_equinox))
```

### ハッピーマンデー制度による祝日
1月第2月曜日、7月第3月曜日、9月第3月曜日、10月第2月曜日が対象日です。
かなり悩みましたが、1ヶ月分のカレンダーを作って、月曜日のみfilter()し、後は第2（第3）月曜日をslice()すれば良いと思いつきました。結構きれいなコードになったと自画自賛しています。そして、tidyverse様に感謝です。
```happy_monday.R
happy_month <- c("01", "07", "09", "10") #1月、7月、9月、10月
happy_week <- c(2, 3, 3, 2) #第2週と第3週
vec_happy_monday <- map(1:length(happy_month), function(monday){
  dplyr::tibble(date = seq(ym(str_c(vec_year, happy_month[monday])),
                           ym(str_c(vec_year, happy_month[monday])) + months(1) - days(1),
                           by = "days")) |> 
    mutate(weekday = wday(date)) |> 
    filter(weekday == 2) |> 
    slice(happy_week[monday])
}) |> list_rbind()
vec_happy_monday <- vec_happy_monday$date
```

### 振替休日
まさかのここで躓きました。「祝日=日曜日だったら翌月曜日を休みにするだけ！」と安易に考えていたのですが、実際は「『国民の祝日』が日曜日に当たるとき、その日の後の最も近い平日を休日とする」という定義です。
「祝日=日曜日の翌日も祝日」なんてことは5月のGWしか起こらないことなので、「5月3日か5月4日が日曜日だったら5月6日も休み」というプログラムを作れば現行制度上は問題ありません。が、やはりそこを厳密にプログラミングしたくなっちゃうんですよねえ…。
かなり苦労して作ったのが以下のコード。while({ })を使って1日ずつ祝日かどうかを判定しました。2026年は5月3日が日曜日なので、ちゃんとこのコードが動くか確認できました。
```substitute_holiday.R
#振替休日出力のため、日曜日の祝日を抽出
df_holiday_sunday <- df_national_holiday |> 
  filter(weekday == 1)
#翌日が祝日でなくなるまで +days(1)処理
vec_substitute_holiday <- map(df_holiday_sunday$holiday_date, function(subs){
  next_day <- subs + days(1)
  while (next_day %in% vec_national_holiday) {
  next_day <- next_day + days(1)
  }
  return(next_day)
}) |> list_c()
```

### その前日及び翌日が「国民の祝日」である日（「国民の祝日」でない日に限る。）
「祝日と祝日の間の平日は休日にする」という意味です。これも9月の敬老の日と秋分の日がたまにそうなるというケースしかあり得ないのですが、これも真面目にプログラムを考えてみました。まず祝日の翌日が平日の場合のみfilter()、その上で「祝日」と「次の祝日」の日数差が2日のものだけfilter()しました。2026年9月がそのケースだったので、コードをテストできました。
```mid_weekday.R
#祝日と祝日の間に挟まれた平日
vec_mid_weekday <- df_national_holiday |> 
  mutate(next_holiday = lead(holiday_date),
         next_day = holiday_date + days(1)) |> 
  mutate(lag_days = time_length(difftime(next_holiday, holiday_date), unit = "days")) |> 
  mutate(next_day_wday = wday(next_day)) |>        
  filter(next_day_wday > 1) |> 
  filter(lag_days == 2)
vec_mid_weekday <- vec_mid_weekday$next_day
```

## 統合コード（2022年から2026年までの休日を出力）
map()を使って5年間の休日を出力してみます。
```japan_holidays.R
library(conflicted)
library(tidyverse)
conflict_prefer('filter', 'dplyr')

vec_year <- seq(2022, 2026)
fixed_date <- c("0101", "0211", "0223", "0429", "0503", "0504", "0505",
                "0811", "1103", "1123")
# ハッピーマンデー制度による祝日。1月第2月、7月第3月、9月第3月、10月第2月
happy_month <- c("01", "07", "09", "10") #1月、7月、9月、10月
happy_week <- c(2, 3, 3, 2) #第2週と第3週

vec_national_holiday <-  map(vec_year, function(f_year) {
  # 2024年現在、日が固定された祝日10日
  vec_fixed_national_holiday <- dplyr::tibble(date = fixed_date) |>
    mutate(date = ymd(str_c(f_year, date)))
  vec_fixed_national_holiday <- vec_fixed_national_holiday$date

  #春分の日 INT(20.8431+0.242194*(自分が調べたい年-1980))-INT((自分が調べたい年-1980)/4)
  vec_spring_equinox <- floor(20.8431 + 0.242194 * (f_year - 1980) -
                        floor((f_year - 1980) / 4))
  vec_spring_equinox <- ymd(str_c(f_year, "03", vec_spring_equinox))
  #秋分の日 INT(23.2488+0.242194*(年-1980))-INT((年-1980)/4)
  vec_autumn_equinox <- floor(23.2488 + 0.242194 * (f_year - 1980) -
                        floor((f_year - 1980) / 4))
  vec_autumn_equinox <- ymd(str_c(f_year, "09", vec_autumn_equinox))

  # ハッピーマンデー制度による祝日。1月第2月、7月第3月、9月第3月、10月第2月
  vec_happy_monday <- map(1:length(happy_month), function(monday){
    dplyr::tibble(date = seq(ym(str_c(f_year, happy_month[monday])),
                            ym(str_c(f_year, happy_month[monday])) + months(1) - days(1),
                            by = "days")) |> 
      mutate(weekday = wday(date)) |> 
      filter(weekday == 2) |> 
      slice(happy_week[monday])
  }) |> list_rbind()
  vec_happy_monday <- vec_happy_monday$date

  c(vec_fixed_national_holiday, vec_spring_equinox, vec_autumn_equinox, vec_happy_monday)
}) |> list_c()

df_national_holiday <- dplyr::tibble(holiday_date = vec_national_holiday) |> 
  arrange(holiday_date) |> 
  mutate(weekday = wday(holiday_date))

#振替休日出力のため、日曜日の祝日を抽出
df_holiday_sunday <- df_national_holiday |> 
  filter(weekday == 1)
#翌日が祝日でなくなるまで +days(1)処理
vec_substitute_holiday <- map(df_holiday_sunday$holiday_date, function(subs){
  next_day <- subs + days(1)
  while (next_day %in% vec_national_holiday) {
  next_day <- next_day + days(1)
  }
  return(next_day)
}) |> list_c()

#祝日と祝日の間に挟まれた平日
vec_mid_weekday <- df_national_holiday |> 
  mutate(next_holiday = lead(holiday_date),
         next_day = holiday_date + days(1)) |> 
  mutate(lag_days = time_length(difftime(next_holiday, holiday_date), unit = "days")) |> 
  mutate(next_day_wday = wday(next_day)) |>        
  filter(next_day_wday > 1) |> 
  filter(lag_days == 2)
vec_mid_weekday <- vec_mid_weekday$next_day

df_holiday_all <- dplyr::tibble(date = c(vec_national_holiday,
                                         vec_substitute_holiday,
                                         vec_mid_weekday)) |> 
  distinct(date) |> 
  arrange(date)
print(df_holiday_all$date)
```
```
 [1] "2022-01-01" "2022-01-10" "2022-02-11" "2022-02-23" "2022-03-21" "2022-04-29" "2022-05-03"
 [8] "2022-05-04" "2022-05-05" "2022-07-18" "2022-08-11" "2022-09-19" "2022-09-23" "2022-10-10"
[15] "2022-11-03" "2022-11-23" "2023-01-01" "2023-01-02" "2023-01-09" "2023-02-11" "2023-02-23"
[22] "2023-03-21" "2023-04-29" "2023-05-03" "2023-05-04" "2023-05-05" "2023-07-17" "2023-08-11"
[29] "2023-09-18" "2023-09-23" "2023-10-09" "2023-11-03" "2023-11-23" "2024-01-01" "2024-01-08"
[36] "2024-02-11" "2024-02-12" "2024-02-23" "2024-03-20" "2024-04-29" "2024-05-03" "2024-05-04"
[43] "2024-05-05" "2024-05-06" "2024-07-15" "2024-08-11" "2024-08-12" "2024-09-16" "2024-09-22"
[50] "2024-09-23" "2024-10-14" "2024-11-03" "2024-11-04" "2024-11-23" "2025-01-01" "2025-01-13"
[57] "2025-02-11" "2025-02-23" "2025-02-24" "2025-03-20" "2025-04-29" "2025-05-03" "2025-05-04"
[64] "2025-05-05" "2025-05-06" "2025-07-21" "2025-08-11" "2025-09-15" "2025-09-23" "2025-10-13"
[71] "2025-11-03" "2025-11-23" "2025-11-24" "2026-01-01" "2026-01-12" "2026-02-11" "2026-02-23"
[78] "2026-03-20" "2026-04-29" "2026-05-03" "2026-05-04" "2026-05-05" "2026-05-06" "2026-07-20"
[85] "2026-08-11" "2026-09-21" "2026-09-22" "2026-09-23" "2026-10-12" "2026-11-03" "2026-11-23"
```
## まとめ
最初は簡単にできると思って始めたのですが、法律の定義に沿ってプログラミングすると結構頭を悩ませました。しかもこれは未来永劫使えるわけではない（2020年や2021年のように祝日が変わることもある）ので、どこまで意味があるのか…。途中で挫折しかけました。
ただ、私のような初級者が取り組むには良い題材だと思います。結果的に自分なりには納得できるコードができました。問題として取り組んでみると年末年始の時間つぶしには楽しめると思いますよ。