空腹おやじのログと備忘録

VBA(主にExcel)でいろいろな実験的な事とか、Linuxのコマンドとか設定とかについて忘れないように、あれこれと・・・

VBAによる祝日判定および祝日取得

2018/9/10 追記
この記事は、祝日(振替休日等を含む)を判定の対象にしていますが、日曜日や年末年始の休み等は判定対象外です。
これらの休日も判定対象に含めたい場合には、以下の記事をあわせて参照してみて下さい。
z1000s.hatenablog.com


2018/6/14 追記
この記事には、2018/6/13の改正東京五輪パラリンピック特別措置法が参院本会議での可決に伴って、更新された記事があります。
z1000s.hatenablog.com

2019/5/23 追記
Googleで検索して来る方が多いようですが、中にはこのページが本来の目的ではない方もいるかもしれないので、とりあえず、「こんなページもあるよ」ということで載せておきます。
z1000s.hatenablog.com
z1000s.hatenablog.com


目次みたいなもの


Excelで祝日判定をしたい」というと、「祝日データを用意して、それを・・・」とお決まりの文言が出てくるのが一般的です。
では、その祝日データはどうすれば用意できるのでしょうか?

ここのように、期間を指定して生成してくれるようなところもあるようです。

が、必要なデータが変わるたびにサイトにアクセスしてというのも面倒です。
となれば、「自分で作ってしまえ作れるようにしてしまえ」となりました。

以上



???普通ならそうはならない???
まぁ、いいじゃないですか。私がそう思ったんだから。
それに、ここで終わったら、ただの落書きで終わってしましますよ。


そろそろ本題に。

まず、処理するに当たって祝日を分類してみる。カッコ内は例。

  1. 月、日が固定のもの(元日:1月1日)
  2. 月、第N ○曜日が固定のもの(成人の日:1月第2月曜日)
  3. 月は固定だが、日が可変のもの(春分の日:3月19〜22日)
  4. 月、日が定まっておらず、他の祝日などに依存するもの(振替休日、国民の休日

1、2項は簡単に求められる。
3項は、計算式があるのでこれも問題なし。
4項は、他の祝日に依存するため、単独では求められない。予め1〜3項の祝日がわかっている必要がある。(或いは、その都度依存する日の状態を確認するか。)

では具体的にどうすれば求められるか、簡単(?)かもしれない解説。
前提条件として、データはDate型で保持するものとします。

1.月、日が固定のもの

VBA では、DateSerial 関数を使用すると、年、月、日を指定してDate 型の値を取得できます。

DateSerial( year, month, day )

DateSerial 関数については、こちら

2.月、第N ○曜日が固定のもの

指定された月の1日の曜日がわかれば、1日以降の最初の指定された曜日(第1○曜日)へのオフセット値(何日補正すればよいか)がわかります。
それがわかれば、7の倍数を加算することで求められます。

まず、指定月の1日を求めるのは、1項で登場したDateSerial 関数

DateSerial( year, month, 1 )

と日に「1」を指定すればOK。

次にその曜日
曜日は、Weekday 関数を使用します。

Weekday( date , [ firstdayofweek ] )

dateに、上で求めた1日の日付データを渡します。
firstdayofweekは省略可能です。今回は使用しません。
そうすると、指定した日付によって1〜7の値が返ってきます。

定数 説明
vbSunday 1 日曜日
vbMonday 2 月曜日
vbTuesday 3 火曜日
vbWednesday 4 水曜日
vbThursday 5 木曜日
vbFriday 6 金曜日
vbSaturday 7 土曜日

これを書いている2018年5月の場合を考えてみます。
第2週までのカレンダーは、以下の通りです。

1 2 3 4 5
6 7 8 9 10 11 12

最初の曜日までの日数は、以下のようになります。

曜日 指定曜日の日付 1日から指定曜日までの日数
日曜日 6 5
月曜日 7 6
火曜日 1 0
水曜日 2 1
木曜日 3 2
金曜日 4 3
土曜日 5 4

で、曜日を使って「指定曜日までの日数」をどうやって求めるかというと
前述の曜日の定数を使って「指定曜日」 から 「1日の曜日」を引きます。
ただし、「指定曜日」が「1日の曜日」より前の場合、結果が負の値となるのでその場合には7を加えます。

5月1日は火曜日なので、vbTuesday ===> 3
例えば、第1金曜日なら、vbFriday ===> 6
なので、6 - 3 = 3 となります。

第1日曜日なら、vbSunday ===> 1
なので、1 - 3 = -2 となりますが、結果が負なので、+7 して
-2 + 7 = 5 となります。

この値が求められれば、次は1日の日付に加算を行います。
日付の加減算は、DateAdd 関数を使用します。

DateAdd(interval, number, date)

intervalには、日数の計算を行うので、"d" を指定します。
numberには、上記の計算で求めた値を指定します。
dateには、1日の日付データを指定します。

最初の曜日であれば、これで良いのですが2回め以降の曜日の場合、更に7の倍数を加算する必要があります。
第2曜日であれば、+7
第3曜日であれば、+14
第4曜日であれば、+21
第5曜日であれば、+28
となるので、第N曜日の場合
(N ー 1)*7
を加算すれば良いことになります。

最終的には、以下のようなプロシージャにより求められます。

Public Function getNthWeeksDayOfWeek(ByVal lYear As Long, _
                                      ByVal lMonth As Long, _
                                      ByVal lNth As Long, _
                                      ByVal lDayOfWeek As VbDayOfWeek) As Date

    Dim dt1stDate       As Date
    Dim lDayOfWeek1st   As Long
    Dim lOffset         As Long

    '指定年月の1日を取得
    dt1stDate = DateSerial(lYear, lMonth, 1)

    '1日の曜日を取得
    lDayOfWeek1st = Weekday(dt1stDate)

    '指定日へのオフセットを取得
    lOffset = lDayOfWeek - lDayOfWeek1st

    If lDayOfWeek1st > lDayOfWeek Then
        lOffset = lOffset + 7
    End If

    lOffset = lOffset + 7 * (lNth - 1)

    getNthWeeksDayOfWeek = DateAdd("d", lOffset, dt1stDate)

End Function

ただし、N >= 5を指定した場合、日付が翌月になる場合がありますので、注意が必要です。

Weekday 関数については、こちら
DateAdd 関数については、こちら

3.月は固定だが、日が可変のもの

これは、春分の日秋分の日が該当。
Wikipediaによると
春分の日は、3月19~22日のいずれか。
秋分の日は、22~24日のいずれか。
となっている。
一般に、

'春分の日
Int(20.8431 + 0.242194 * (- 1980)) - Int((- 1980) / 4)
'秋分の日
Int(23.2488 + 0.242194 * (- 1980)) - Int((- 1980) / 4)

という計算で求められるようです。
1980年以降に限定すれば、以下の内容でもOK。
というか、こうしたかったんだけれど、1979年以前の場合、
演算子の結果が、Fix 関数と同等になり、Int 関数とは結果が変わってしまうため
If で振り分ける等の必要が生じてしまうので、上記の式をそのまま使用することにしました。

'春分の日
Int(20.8431 + 0.242194 * (- 1980)) - (- 1980) \ 4
'秋分の日
Int(23.2488 + 0.242194 * (- 1980)) - (- 1980) \ 4

ちなみに、¥演算子を使用した場合は、内部で整数演算が行われるが、
/演算子の場合、整数同士の演算であっても、内部では浮動小数点数で演算が行われます。

4.月日が定まっておらず、他の祝日などに依存するもの

4.1 振替休日

1973年の国民の祝日に関する法律が改正されたことにより制定された。これにより祝日(国民の祝日)が日曜日の場合、その翌日となる月曜日が振替休日となった(ハッピーマンデー制度の法律と同じ)。1973年の天皇誕生日(4月29日)が日曜日で、同年4月30日が最初の適用日となった。

当初は祝日が2日以上連続することがなかったため、「国民の祝日の日曜日の翌日の月曜日」としていた。しかし2005年の国民の祝日に関する法律の改正(2007年施行)で、4月29日が「昭和の日」となり、「みどりの日」が4月29日から5月4日へ変更。5月3日から5月5日まで祝日が3日連続することになり、その直後の国民の祝日の日曜日の翌日の月曜日以降の国民の祝日でない祝日の翌日」を休日とすることと改められ振替先が月曜日固定ではなくなった。

振替休日 - Wikipedia

ということなので、
祝日が日曜日であった場合、適用された年から判断し、その翌日または、それ以降で最初の祝日でない日を求めればOK。
2007年以降では、当然ながらこの日が月曜日とは限りません。
2015年5月3日の憲法記念日の振替休日は、6日の水曜日でした。
 5月3日(日):憲法記念日
 5月4日(月):みどりの日
 5月5日(火):こどもの日
 5月6日(水):5月3日の振替休日

4.2 国民の休日

前後が祝日である平日は、国民の休日となり、休日となる。

国民の休日 - Wikipedia

こちらは、翌々日まで確認が必要なので、要件は振替休日より複雑。

  1. 最初の祝日が月曜日から金曜日の間にあること。
    1. 最初の祝日が日曜日の場合、翌日は振替休日となるので、2項を満足しない。
    2. 最初の祝日が土曜日の場合、翌日は日曜日となるので、平日ではない。
  2. 最初の祝日の翌日が、平日であること。(祝日でなく、休日でもないこと。)
  3. 最初の祝日の翌々日が、祝日であること。

これらの条件を満たした時、最初の祝日の翌日が「国民の休日」となる。

2項、3項の祝日は、年によって日付が変動する祝日であり、「移動祝日」または「移動祝祭日」と呼ぶそうです。

最終的なコード

VBAのクラスとして作成しました。



パブリックメソッド一覧

isNationalHoliday(ByVal dtDate As Date) As Boolean

指定日が国民の祝日(休日)か?

引 数 IN/OUT 内容
dtDate As Date IN 判定したい日付
復帰値 内容
True 国民の祝日(休日)である。
False 国民の祝日(休日)ではない。
isNationalHoliday2(ByVal dtDate As Date, ByRef sHolidayName As String) As Boolean

指定日が国民の祝日(休日)か?そうであれば、その祝日名を合わせて返す

引 数 IN/OUT 内容
dtDate As Date IN 判定したい日付
sHolidayName As String OUT 祝日名
復帰値 内容
True 国民の祝日(休日)である。
False 国民の祝日(休日)ではない。
getNationalHolidays(ByVal lYear As Long, ByRef dtHolidays() As Date) As Long

指定年の国民の祝日を配列に格納して返す

引 数 IN/OUT 内容
lYear As Long IN 取得したい年
dtHolidays() As Date OUT 指定された年の国民の祝日
復帰値
国民の祝日の件数
getNationalHolidayName(ByVal dtHoliday As Date) As String

指定日の国民の祝日名を返す

引 数 IN/OUT 内容
dtHoliday As Date IN 日付
復帰値
指定された日付の国民の祝日

指定された日付が国民の祝日でない場合、復帰値は長さ0の文字列となる。

reInitialize(ByVal lLastYear As Long)

指定年までの祝日データが生成されていなければ、追加生成する。

引 数 IN/OUT 内容
lLastYear As Long IN 生成したい最終年

現在の何年までのデータが生成されているかは、後述のInitializedLastYearプロパティで取得できる。

パブリックプロパティ

InitializedLastYear() As Long

何年までの国民の祝日データが生成されているか
(読み取り専用)

使い方と実行例

クラス名は、CNationalHoliday としています。
使用する前に、Newを付けて、インスタンスを生成して下さい。
(そのタイミングで、デフォルトで現在の年から5年後の年末までの祝日を内部で生成します。)
その後に、必要なメソッドを呼び出します。

日付を指定して、祝日判定をして、祝日名を取得する
Public Sub Test1()

    Dim cnh     As New CNationalHoliday
    Dim dt      As Date
    Dim sHolidayName As String

    dt = #8/11/2015#
    Debug.Print Format$(dt, "yyyy/mm/dd"), cnh.getNationalHolidayName(dt), cnh.isNationalHoliday2(dt, sHolidayName)

    dt = #8/11/2016#
    Debug.Print Format$(dt, "yyyy/mm/dd"), cnh.getNationalHolidayName(dt), cnh.isNationalHoliday2(dt, sHolidayName)

    dt = #8/12/2018#
    Debug.Print Format$(dt, "yyyy/mm/dd"), cnh.getNationalHolidayName(dt), cnh.isNationalHoliday2(dt, sHolidayName)

    dt = #8/12/2019#
    Debug.Print Format$(dt, "yyyy/mm/dd"), cnh.getNationalHolidayName(dt), cnh.isNationalHoliday2(dt, sHolidayName)

    dt = #2/23/2020#
    Debug.Print Format$(dt, "yyyy/mm/dd"), cnh.getNationalHolidayName(dt), cnh.isNationalHoliday2(dt, sHolidayName)

    Set cnh = Nothing

End Sub

実行結果

call Test1
2015/08/11                  False
2016/08/11    山の日        True
2018/08/12                  False
2019/08/12    振替休日      True
2020/02/23    天皇誕生日    True
年を指定して、祝日および祝日名を取得する
Public Sub Test2(ByVal lYear As Long)

    Dim cnh     As New CNationalHoliday
    Dim dt()    As Date
    Dim i As Long

    Call cnh.getNationalHolidays(lYear, dt)

    For i = 0 To UBound(dt)
        Debug.Print dt(i), cnh.getNationalHolidayName(dt(i))
    Next i

    Set cnh = Nothing

End Sub

実行結果

call Test2(2018)
2018/01/01    元日
2018/01/08    成人の日
2018/02/11    建国記念の日
2018/02/12    振替休日
2018/03/21    春分の日
2018/04/29    昭和の日
2018/04/30    振替休日
2018/05/03    憲法記念日
2018/05/04    みどりの日
2018/05/05    こどもの日
2018/07/16    海の日
2018/08/11    山の日
2018/09/17    敬老の日
2018/09/23    秋分の日
2018/09/24    振替休日
2018/10/08    体育の日
2018/11/03    文化の日
2018/11/23    勤労感謝の日
2018/12/23    天皇誕生日
2018/12/24    振替休日
祝日データを、デフォルトの5年後より後の分も生成する
Public Sub Test3()

    Dim cnh As CNationalHoliday
    Dim dt  As Date
    Dim i As Long

    Set cnh = New CNationalHoliday

    Debug.Print "Before reInitialize", cnh.InitializedLastYear

    cnh.reInitialize 2030

    Debug.Print "After reInitialize", cnh.InitializedLastYear

    Set cnh = Nothing

End Sub

実行結果

call Test3
Before reInitialize          2023 
After reInitialize           2030 

お約束

掲載したコードの使用については、特に制限は設けません。ご自由にお使い下さい。
改造でも、再配布でもOK。
使用にあたって、私への連絡等は不要です。

ただし、使用した結果、何らかのトラブル、損害、その他諸々の事象が発生しても、私は一切関与しません。
使用する方ソースコードを組み込んだ方が責任を取れる範囲内で使って下さい。(2018/9/9 一部修正)

バグ、要望、気が付いた事などあれば、コメントしていただければと思います。

参考にしたサイト