【VBA】「4で割れる」だけじゃない!正確な「うるう年判定関数」の作り方(単体テストコード付き)
日付やスケジュールの自動計算、月別集計などのマクロを作成する際、必ずと言っていいほど登場するのが「指定した西暦年がうるう年(閏年)かどうか」の判定です。
「4で割り切れたらうるう年でしょ?」と安易に year Mod 4 = 0 だけで済ませていると、「100年に1度の例外ルール」に引っかかり、将来の計算でバグを引き起こす原因になります。
今回は、グレゴリオ暦の正確なルールを整理し、「契約による設計(DbC)」のコメントフォーマットに則った安全な汎用関数とテストコードを作成します。
1. うるう年(閏年)決定の「3つのルール」
現在使われているグレゴリオ暦において、うるう年は以下の優先順位(ルール)に従って決定されます。
- 基本ルール: 西暦年が 4 で割り切れる 年はうるう年(例: 2024年)
- 例外ルール①: ただし、100 で割り切れる 年はうるう年ではない(例: 1900年、2100年)
- 例外ルール②: ただし、400 で割り切れる 年は例外的にうるう年(例: 2000年、2400年)
この条件をVBAの論理演算子(And / Or)で表現すると、次の一行に集約されます。
IsLeapYear = (Year Mod 4 = 0 And Year Mod 100 <> 0) Or (Year Mod 400 = 0)
2. 実装:安全なうるう年判定関数 (IsLeapYear)
事前条件(1以上の西暦年)と事後条件を明記した関数です。不正な数値(0以下など)が渡された場合もクラッシュさせず、エラーログを出力して安全に False を返します。
' [機能名] うるう年(閏年)判定
'
' [処理内容]
' ・指定された西暦年(targetYear)がうるう年かどうかをグレゴリオ暦の定義に従って判定します。
'
' [引数]
' @param {Long} targetYear : 必須 : 判定対象の西暦年 (1 以上の整数)
'
' [事前条件 (Requires)]
' ・targetYear >= 1 であること(グレゴリオ暦の正の西暦年)。
'
' [事後条件 (Ensures)]
' ・うるう年の場合 : True を返す。
' ・うるう年でない場合 : False を返す。
' ・事前条件違反の場合 : False を返し、[ERROR] ログを出力する。
'
' [戻り値]
' @return {Boolean} うるう年判定結果
'
' [例外処理 (Throws)]
' ・targetYear < 1 の場合はエラーログを出力して False を返却。
'
' [履歴]
' 2026/10/08 [作成者名] 新規作成
' ==============================================================================
Public Function IsLeapYear(ByVal targetYear As Long) As Boolean
' 1. 事前条件(Requires)のチェック
If targetYear < 1 Then
Debug.Print "[ERROR] IsLeapYear: 西暦年は 1 以上の整数を指定してください。 (指定値: " & targetYear & ")"
IsLeapYear = False
Exit Function
End If
' 2. グレゴリオ暦に基づく判定ロジック
' ・4で割り切れ、かつ100で割り切れない年
' ・または、400で割り切れる年
If (targetYear Mod 4 = 0 And targetYear Mod 100 <> 0) Or (targetYear Mod 400 = 0) Then
IsLeapYear = True
Else
IsLeapYear = False
End If
End Function
3. 境界値(エッジケース)を網羅する単体テストコード
うるう年判定で最もバグが起きやすい「100の倍数年(1900年など)」や「400の倍数年(2000年など)」を含むテストケースを一括検証します。
エントリーポイントのため、RAIIパターン(CAspect)で実行時間とログ出力を自動化しています。
' [機能名] IsLeapYear 関数の網羅的テスト
' ==============================================================================
Public Sub Test_IsLeapYear()
' エントリーポイント用アスペクト(ログ・実行時間自動化)
Dim aspect As New CAspect: aspect.Begin "Test_IsLeapYear"
' テストケース: Array(検証する年, 期待される判定結果, テストの意図)
Dim testCases As Variant
testCases = Array( _
Array(2024, True, "4で割り切れる年 (平年の直近うるう年)"), _
Array(2023, False, "4で割り切れない年 (平年)"), _
Array(1900, False, "100で割り切れるが400で割り切れない年 (例外平年)"), _
Array(2100, False, "100で割り切れるが400で割り切れない年 (未来の例外平年)"), _
Array(2000, True, "400で割り切れる年 (例外うるう年)"), _
Array(1600, True, "400で割り切れる過去の年 (例外うるう年)"), _
Array(0, False, "事前条件違反: 0年"), _
Array(-400, False, "事前条件違反: 負の数") _
)
Debug.Print "=================================================="
Debug.Print " IsLeapYear 単体テスト開始"
Debug.Print "=================================================="
Dim passCount As Long: passCount = 0
Dim failCount As Long: failCount = 0
Dim i As Long
For i = LBound(testCases) To UBound(testCases)
Dim checkYear As Long: checkYear = CLng(testCases(i)(0))
Dim expected As Boolean: expected = CBool(testCases(i)(1))
Dim memo As String: memo = testCases(i)(2)
Dim actual As Boolean
actual = IsLeapYear(checkYear)
If actual = expected Then
passCount = passCount + 1
Debug.Print "[PASS] " & memo & " (" & checkYear & "年) => " & actual
Else
failCount = failCount + 1
Debug.Print "[FAIL] " & memo & " (" & checkYear & "年)"
Debug.Print " 期待: " & expected & ", 実際: " & actual
End If
Next i
Debug.Print "--------------------------------------------------"
Debug.Print " 結果: SUCCESS=" & passCount & " / FAIL=" & failCount
Debug.Print "=================================================="
End Sub
4. 実行結果(イミディエイトウィンドウ)
==================================================
IsLeapYear 単体テスト開始
==================================================
[PASS] 4で割り切れる年 (平年の直近うるう年) (2024年) => True
[PASS] 4で割り切れない年 (平年) (2023年) => False
[PASS] 100で割り切れるが400で割り切れない年 (例外平年) (1900年) => False
[PASS] 100で割り切れるが400で割り切れない年 (例外平年) (2100年) => False
[PASS] 400で割り切れる年 (例外うるう年) (2000年) => True
[PASS] 400で割り切れる過去の年 (例外うるう年) (1600年) => True
[ERROR] IsLeapYear: 西暦年は 1 以上の整数を指定してください。 (指定値: 0)
[PASS] 事前条件違反: 0年 (0年) => False
[ERROR] IsLeapYear: 西暦年は 1 以上の整数を指定してください。 (指定値: -400)
[PASS] 事前条件違反: 負の数 (-400年) => False
--------------------------------------------------
結果: SUCCESS=8 / FAIL=0
==================================================
10:00:00 [END] Test_IsLeapYear (0.001s)
5. 補足:VBA組み込み関数を使う裏ワザとの比較
VBAには、DateSerial 関数を使って「2月29日が存在するか(または3月0日の日付が29日か)」を判定するテクニックもあります。
Public Function IsLeapYearDateSerial(ByVal targetYear As Long) As Boolean
IsLeapYearDateSerial = (Day(DateSerial(targetYear, 3, 0)) = 29)
End Function
- 算術演算版 (Mod 使用): 外部の日付オブジェクト生成がなく超高速で、処理の意図(400年周期のロジック)が明確。
- DateSerial 版: 記述が1行で済むが、VBAの内部日付シリアル値変換が入るため大量計算時のオーバーヘッドがわずかに大きい。
業務システムや共通ライブラリとして組み込む場合は、事前条件ガード(範囲チェック)を併設できる算術演算版 (IsLeapYear) の方が堅牢でおすすめです。
6. まとめ
- 「100の倍数」と「400の倍数」の例外を忘れない:
Mod 4 = 0だけでは 1900年や 2100年を誤って「うるう年」と判定してしまいます。 - 境界値テスト(2000年・1900年)を自動テストに含める:ロジックが正しいかを検証する際、2000年(うるう年)と1900年(平年)の両方をテストケースに入れておくことで、実装ミスを100%防ぐことができます。