【VBA / Excel】祝日休日の判定、取得用データを生成し、実際に判定、取得してみる
この記事では、予め用意された祝日テーブルを使用するのではなく、
クラスモジュール内で、祝日テーブルを自前で生成し、Dictionary に格納して判定処理を行っています。
祝日の定義は、クラスモジュール内に、汎用性を考慮した状態でハードコーディングしてあります。
このため、法の変更等により、祝日が変わらなければ、このまま使用できます。
別途、祝日テーブルを用意する必要はありません。
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
目次みたいなもの
- 1.月、日が固定のもの
- 2.月、第N ○曜日が固定のもの
- 3.月は固定だが、日が可変のもの
- 4.月日が定まっておらず、他の祝日などに依存するもの
- 最終的なコード
- パブリックメソッド一覧
- isNationalHoliday(ByVal dtDate As Date) As Boolean
- isNationalHoliday2(ByVal dtDate As Date, ByRef sHolidayName As String) As Boolean
- getNationalHolidays(ByVal lYear As Long, ByRef dtHolidays() As Date) As Long
- getNationalHolidayName(ByVal dtHoliday As Date) As String
- reInitialize(ByVal lLastYear As Long)
- パブリックプロパティ
- 使い方と実行例
- お約束
「Excelで祝日判定をしたい」というと、「祝日データを用意して、それを・・・」とお決まりの文言が出てくるのが一般的です。
では、その祝日データはどうすれば用意できるのでしょうか?
ここのように、期間を指定して生成してくれるようなところもあるようです。
が、必要なデータが変わるたびにサイトにアクセスしてというのも面倒です。
となれば、「自分で作ってしまえ作れるようにしてしまえ」となりました。
以上
???普通ならそうはならない???
まぁ、いいじゃないですか。私がそう思ったんだから。
それに、ここで終わったら、ただの落書きで終わってしましますよ。
そろそろ本題に。
まず、処理するに当たって祝日を分類してみる。カッコ内は例。
- 月、日が固定のもの(元日:1月1日)
- 月、第N ○曜日が固定のもの(成人の日:1月第2月曜日)
- 月は固定だが、日が可変のもの(春分の日:3月19〜22日)
- 月、日が定まっておらず、他の祝日などに依存するもの(振替休日、国民の休日)
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を指定した場合、日付が翌月になる場合がありますので、注意が必要です。
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
こちらは、翌々日まで確認が必要なので、要件は振替休日より複雑。
- 最初の祝日が月曜日から金曜日の間にあること。
- 最初の祝日が日曜日の場合、翌日は振替休日となるので、2項を満足しない。
- 最初の祝日が土曜日の場合、翌日は日曜日となるので、平日ではない。
- 最初の祝日の翌日が、平日であること。(祝日でなく、休日でもないこと。)
- 最初の祝日の翌々日が、祝日であること。
これらの条件を満たした時、最初の祝日の翌日が「国民の休日」となる。
2項、3項の祝日は、年によって日付が変動する祝日であり、「移動祝日」または「移動祝祭日」と呼ぶそうです。
最終的なコード
VBAのクラスとして作成しました。
▶クリックでソースを展開/縮小
2021/4/11
都合により、
VBAによる「祝日判定処理」を「休日判定処理」に拡張してみた - 空腹おやじのログと備忘録
と、コードを統合した上で、
こちら に移動しました。
CCompanyHoliday.clsのみ
https://github.com/Z1000R/determining-and-retrieving-holidays/blob/main/Source/CCompanyHoliday.cls
その他諸々を含めて一式
github.com
パブリックメソッド一覧
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