【ExcelVBA】日時操作用のユーティリティ関数を自作してみた

# はじめに

[【ExcelVBA】文字列操作用のユーティリティ関数を自作してみた](https://qiita.com/nkojima/items/a1702d5b0964a119c07b)に続いて、日時操作用のユーティリティ関数を自作してみました。
今後良い処理を思いついたら、随時書き足していく予定です。

※とりあえず関数はABC順に並べていますが、後で読みやすくするために順番を変えるかもしれません。

# 作成した関数群


## CalcFiscalYear

* 引数の日付の「会計年度」を求める関数です。
 * 会計年度が10月1日から始まる場合は、`CalcFiscalYear({Dateオブジェクト}, 10)`とすればOKです。
* 処理については、[[Java] 年度を求めるスマートな書き方（考え方は他の言語でも一緒）](https://so-kai-app.sakura.ne.jp/blog/1593/2017/03/03/)を参考にさせてもらいました。

```vb:DateUtils.bas
'--------------------------------------------------------------------------------
' 引数の日付の会計年度を返す。
'
' d：日付。
' startMonth：新年度の開始月。新年度が4月から始まる場合は4を指定する。
' return：YYYY形式の西暦で表された年度。
'--------------------------------------------------------------------------------
Public Function CalcFiscalYear(d As Date, startMonth) As Integer
    CalcFiscalYear = Year(DateAdd("m", 1 - startMonth, d))
End Function
```

## CalcLastDayOfMonth

* 引数の日付の"月"の月末日を求める関数です。
 * 引数に「2020年2月1日」を指定すると「2020年2月29日」が返されます。
* [こちらの記事](https://qiita.com/nkojima/items/09e7b37cbca40d797bcf)で作成した処理を修正して、日付オブジェクトを返すようにしたものです。

```vb:DateUtils.bas
'--------------------------------------------------------------------------------
' 引数の日付の月における「月末日」を返す。
'
' d：日付。
' return：引数で指定した日付における月末日。
'--------------------------------------------------------------------------------
Public Function CalcLastDayOfMonth(d As Date)
  CalcLastDayOfMonth = DateSerial(Year(d), Month(d) + 1, 0)
End Function
```


## CalcQuarter

* 引数の日付が四半期（1Q,2Q,3Q,4Q）のどれに属しているかを求める関数です。
 * 会計年度が10月1日から始まる場合は、`CalcQuarter({Dateオブジェクト}, 10)`とすればOKです。
* 業務システムだと「〇〇期2Q」など、月単位だけでなくクォーター単位を扱うこともあるので、たまに出番があるかも...という関数だと思います。

```vb:DateUtils.bas
'--------------------------------------------------------------------------------
' 引数の日付が属する四半期（1Q,2Q,3Q,4Q）を返す。
'
' d：日付。
' startMonth：新年度の開始月。新年度が4月から始まる場合は4を指定する。
' return：四半期を表す文字列（1Q,2Q,3Q,4Q）。
'--------------------------------------------------------------------------------
Public Function CalcQuarter(d As Date, startMonth As Integer) As String
    Dim monthDiff As Integer, currentMonth As Integer
    currentMonth = Month(d)
    
    If (currentMonth < startMonth) Then
        monthDiff = 12 - Abs(currentMonth - startMonth)
    Else
        monthDiff = Abs(currentMonth - startMonth)
    End If
​
    Dim quarter As String
    quarter = CStr(Application.RoundDown(monthDiff / 3, 0) + 1)
    CalcQuarter = quarter & "Q"
End Function
```

## FindWantDayOfWeek

* 指定した年月において、指定した曜日（日曜～土曜）の日付を返す関数です。
* [【C#】「〇曜日の日付」を取得する方法](https://qiita.com/nkojima/items/22b41efca35f0b2370e5)の真似をしようと思いましたが、もっと端折って楽な書き方をしてしまいました。
* 第3引数を0～6（あるいは1～7）の整数値で指定させず、VbDayOfWeek列挙型を使って曜日指定を間違えないようにしているのがちょっとしたポイントです。

```vb:DateUtils.bas
'--------------------------------------------------------------------------------
' 指定した年月において、指定した曜日の日付を返す。
' FindWantDayOfWeek(2019, 11, vbMonday)とした場合、2019/11/4, 2019/11/11, 2019/11/18, 2019/11/25が返される。
'
' year：西暦年。
' month ：月。
' wantDayOfWeek：曜日。VbDayOfWeek列挙型で表される。
' return：指定した年月における、指定した曜日のリスト。
'--------------------------------------------------------------------------------
Public Function FindWantDayOfWeek(year As Integer, month As Integer, wantDayOfWeek As VbDayOfWeek) As Collection
    
    Dim firstDay As Date, lastDay As Date
    firstDay = DateSerial(year, month, 1)       ' 指定年月の初日
    lastDay = CalcLastDayOfMonth(firstDay)      ' 指定年月の末日
    
    Dim i As Integer
    Dim dateBuff As Date, dateList As Collection
    Set dateList = New Collection
    dateBuff = firstDay
    
    For i = 1 To Day(lastDay)
        If (Weekday(dateBuff) = wantDayOfWeek) Then
            dateList.Add dateBuff
        End If
        dateBuff = DateAdd("d", 1, dateBuff)
    Next i
    
    Set FindWantDayOfWeek = dateList
End Function
```

## IsLeapYear

* 引数の「年」がうるう年であるかを判定する関数です。
* Microsoftの[年が閏年かどうかを判断するメソッド](https://docs.microsoft.com/ja-jp/office/troubleshoot/excel/determine-a-leap-year)というページの情報を参考にして作成しました。
* なお、上記のMicrosoftのページに書かれていたうるう年の条件は以下の2つです。
 1. 「年が4で割り切れる」かつ「年が100で割り切れない」
 2. 「年が100で割り切れる」かつ「年が400で割り切れる」

```vb:DateUtils.bas
'--------------------------------------------------------------------------------
' 引数の年がうるう年であるかを判定する。
'
' year：判定対象の西暦年。
' return：うるう年であればtrue、そうでなければfalseを返す。
'--------------------------------------------------------------------------------
Public Function IsLeapYear(year As Integer) As Boolean
    IsLeapYear = ((year Mod 4 = 0) And (year Mod 100 <> 0)) Or (year Mod 400 = 0)
End Function
```