Help us understand the problem. What is going on with this article?

【ExcelVBA】祝日を加味して営業日を調べる方法

はじめに

業務用のちょっとしたツールを作っていると、「その月の最終営業日はいつなのか?」「〇〇日は営業日か?」といったことを判定できるプログラムが欲しくなる時があります。

「平日であるか?」ということを調べるのであれば簡単なのですが、祝日を踏まえた上で「営業日であるか?」ということを調べるのは少々面倒です。
なぜ営業日を調べるのが面倒なのかというと、「一部の祝日の日付が年によって変わるため、営業日であるかの判定が年ごとに変わってしまう」というのが大きな理由です。

そこで今回は「内閣府の祝日CSV」を使って、営業日を調べるプログラムを作ってみました。

DateCheckerクラス

  • 営業日を求めるためのメイン処理となるクラスモジュールです。
  • 内閣府の祝日CSVをダウンロードして、CSVから取得した祝日を営業日の判定に利用しています。
  • 以前に投稿した、日時操作用の自作関数の知見を踏まえたコードとなっています。
    • 本当はループを使わずにスマートに書ければ良かったのですが、中々良い方法を思いつきませんでした...
DateChecker.bas
Option Explicit

'--------------------------------------------------------------------------------
' 「日本の祝日一覧」のクラス。
'--------------------------------------------------------------------------------
' 祝日一覧(キーが日付文字列、値がJpHoliday型)
Public holidaysDic As Dictionary

'--------------------------------------------------------------------------------
' コンストラクタ
'--------------------------------------------------------------------------------
Public Sub Class_Initialize()
    Set Me.holidaysDic = New Dictionary
    Call GetJpHolidays
End Sub

'--------------------------------------------------------------------------------
' 内閣府の祝日情報CSVを取得して、祝日リストにセットする。
'--------------------------------------------------------------------------------
Private Sub GetJpHolidays()

    ' HTTPSで内閣府の祝日情報CSVを取得する。
    Dim httpObj As HttpClient
    Set httpObj = New HttpClient
    Dim responseCsv As String
    responseCsv = httpObj.GetPage("https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv")
    Set httpObj = Nothing

    ' 取得したCSVを「日本の祝日」の構造体のCollectionに詰める
    Dim rows As Variant
    rows = Split(responseCsv, vbCrLf)

    Dim i As Integer
    For i = LBound(rows) To UBound(rows)
        ' ヘッダ行と空行(最終行)をスキップして読み込む
        If (i <> 0 And rows(i) <> "") Then
            Dim holiday As JpHoliday
            Set holiday = New JpHoliday
            Call holiday.SetByCsvRow(CStr(rows(i)))
            Me.holidaysDic.Add CStr(holiday.DateOfHoliday), holiday
        End If
    Next i
End Sub

'--------------------------------------------------------------------------------
' 引数の日付が営業日(※)であるかを判定する。
' ※土日および祝日以外を営業日と定義。
'
' d:判定対象の日付。
' return:引数の日付が営業日であればTrue、土日もしくは祝日であればFalseを返す。
'--------------------------------------------------------------------------------
Public Function IsBusinessDay(d As Date) As Boolean
    ' 日付から曜日を取得する。
    Dim day As VbDayOfWeek
    day = Weekday(d, vbSunday)

    If (day = vbSaturday Or day = vbSunday) Then
        ' 土日であれば営業日ではない。
        IsBusinessDay = False
    Else
        ' 月~金の場合、祝日であるかを調べて営業日を判定する。
        If holidaysDic.Exists(CStr(d)) Then
            IsBusinessDay = False
        Else
            IsBusinessDay = True
        End If
    End If

End Function

'--------------------------------------------------------------------------------
' 引数の日付の年月における、最終営業日を求める。
' ※土日および祝日以外を営業日と定義。
'
' d:最終営業日を求める年月の日付。
' return:引数の日付の年月における最終営業日。
'--------------------------------------------------------------------------------
Public Function CalcLastBusinessDay(d As Date) As Date
    ' 日付から月末日を取得する。
    Dim lastDate As Date
    lastDate = DateSerial(year(d), month(d) + 1, 0)

    If (IsBusinessDay(lastDate)) Then
        CalcLastBusinessDay = lastDate
    Else
        Do
            lastDate = DateSerial(year(lastDate), month(lastDate), day(lastDate) - 1)
            If (IsBusinessDay(lastDate)) Then
                CalcLastBusinessDay = lastDate
                Exit Do
            End If
        Loop
    End If
End Function

JpHolidayクラス

  • 祝日情報を入れる「箱」に相当するクラスモジュールです。
  • クラスモジュールではなく、ユーザー定義型で代用することも可能ですが、以下の理由でクラスモジュールを使っています。
    • ダウンロードしたCSVは、読み込み終わるまで祝日が幾つ存在するかが分かりません。
    • 配列の要素数を幾つ用意すればよいかが分からないのでCollectionを使おうとすると、Collectionの要素としてユーザー定義型を指定できずエラーとなってしまいます。
      • Collectionの代わりにRedim Preserve...で動的配列を使えば、ユーザー定義型を使うことが可能です。
      • エラーとなってしまう理由は、こちらの記事に書かれているDictionaryの事例と同じです。
JpHoliday.bas
Option Explicit
'--------------------------------------------------------------------------------
' 「日本の祝日」のクラス。
'--------------------------------------------------------------------------------
' 祝日の日付
Public DateOfHoliday As Date

' 祝日名
Public Name As String

'--------------------------------------------------------------------------------
' 「内閣府の祝日情報CSV」の1行分のデータから、日付と名称を取得してセットする。
'
' row:「内閣府の祝日情報CSV」の1行分のデータ。{祝日の日付,祝日名}の形式。
'--------------------------------------------------------------------------------
Sub SetByCsvRow(row As String)
    Dim buffs As Variant
    buffs = Split(row, ",")

    Me.DateOfHoliday = CDate(buffs(0))
    Me.Name = buffs(1)
End Sub

HttpClientクラス

HttpClient.bas
Option Explicit
'--------------------------------------------------------------------------------
' HTTP通信用クラス。
'--------------------------------------------------------------------------------
' HTTP通信用オブジェクト
Private httpObj As Object

'--------------------------------------------------------------------------------
' コンストラクタ
'--------------------------------------------------------------------------------
Public Sub Class_Initialize()
    Set httpObj = CreateObject("MSXML2.ServerXMLHTTP")  ' TLS1.2に対応
End Sub

'--------------------------------------------------------------------------------
' デストラクタ
'--------------------------------------------------------------------------------
Public Sub Class_Terminate()
    Set httpObj = Nothing
End Sub

'--------------------------------------------------------------------------------
' 引数のURLをGETメソッドで取得する。
'
' url:URL文字列。
' return:取得したページ。
'--------------------------------------------------------------------------------
Public Function GetPage(url As String) As String
    httpObj.Open "GET", url
    httpObj.Send

    ' readyState=4で読み込みが完了
    Do While httpObj.readyState < 4
        DoEvents
    Loop

    ' レスポンスヘッダのContent-Typeに適切な文字コードが含まれていない場合、バイト配列から文字コードを指定して文字列を生成する。
    GetPage = StrConv(httpObj.responseBody, vbUnicode)
End Function

テストコード

IsBusinessDay関数のテスト

Module1.bas
Sub Test_IsBusinessDay()
    Dim checker As DateChecker
    Set checker = New DateChecker

    Debug.Print "2018/10/22は営業日?:" & checker.IsBusinessDay(CDate("2018/10/22"))
    Debug.Print "2018/12/23は営業日?:" & checker.IsBusinessDay(CDate("2018/12/23"))
    Debug.Print "2019/10/22は営業日?:" & checker.IsBusinessDay(CDate("2019/10/22"))
    Debug.Print "2019/12/23は営業日?:" & checker.IsBusinessDay(CDate("2019/12/23"))
End Sub
出力結果
2018/10/22は営業日?:True
2018/12/23は営業日?:False
2019/10/22は営業日?:False
2019/12/23は営業日?:True

CalcLastBusinessDay関数のテスト

Module1.bas
Sub Test_CalcLastBusinessDay()
    Dim checker As DateChecker
    Set checker = New DateChecker

    Debug.Print "2019/04/03の月の最終営業日:" & checker.CalcLastBusinessDay(CDate("2019/4/3"))
End Sub
出力結果
2019/04/03の月の最終営業日:2019/04/26

まとめ

  • VBAでもHTTP通信が出来ることを知っていたので、どうにか営業日を判定するコードを作り上げることができました。
    • プログラムの効率性を考えたら、一度DLしたCSVファイルはExcelのシートに保管(書き込み)しておいた方が良いと思います。
  • こちらの記事のようにLambda+API GatewayなどでAPI化してしまえば、社内の各種システム/ツールで幅広く再利用できると思います。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away