忍者ブログ
VBAによる実用アプリケーションの構築、およびGAS(Google Apps Script)やOffice Scriptへのモダンな移行パスを検証・解説するテックブログ。現場で即戦力となるコードモジュールや再利用可能な実用部品を継続的に提供します。

【VBA】ADOによるAccess連携:「パラメータ化クエリ」で安全にデータを絞り込み・取得する(テストコード付き)

これまでに構築した共通のコネクションパーツ(OpenDatabaseConnection / CloseDatabaseConnection)をそのまま活かし、今回は新たに「ADODB.Commandオブジェクトを活用したパラメータ化クエリ(プリペアードステートメント)」による安全なデータ抽出処理を実装しました。

実際の実行結果とともに、モジュール全体をお届けします。

1. 実装:パラメータ化クエリによる安全なデータ抽出モジュール

' ==============================================================================
' [機能名] Accessデータベースへの接続確立
' ==============================================================================
Public Function OpenDatabaseConnection(ByVal dbFileName As String) As Object
    Set OpenDatabaseConnection = Nothing
    On Error GoTo ErrorHandler
    
    Dim dbPath As String
    dbPath = ThisWorkbook.Path & "\" & dbFileName
    
    If Dir(dbPath) = "" Then
        Debug.Print "[WARNING] OpenDatabaseConnection: ファイルが見つかりません。 Path: " & dbPath
        Exit Function
    End If
    
    Dim conn As Object
    Set conn = CreateObject("ADODB.Connection")
    
    Dim connStr As String
    connStr = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" & dbPath & "; "
    
    conn.Open connStr
    Debug.Print "[INFO] Database Connected: " & dbFileName
    
    Set OpenDatabaseConnection = conn
    Exit Function

ErrorHandler:
    Debug.Print "[WARNING] OpenDatabaseConnection: 接続エラー (Err:" & Err.Number & " - " & Err.Description & ")"
    Set OpenDatabaseConnection = Nothing
End Function

' ==============================================================================
' [機能名] パラメータ化クエリを用いた条件付きデータ取得と出力
'
' [処理内容]
' ・ADODB.CommandとCreateParameterを使用して、SQLインジェクションを防止しながら
' 指定した条件(部署・年齢など)に一致するレコードを安全に抽出・出力します。
'
' [引数]
' @param {Object} conn : 必須 : 接続済みの ADODB.Connection オブジェクト
' @param {String} tableName : 必須 : 対象のテーブル名(例: "社員マスタ")
' @param {String} targetDep : 必須 : 抽出する部署名条件
' @param {Long} targetAge : 必須 : 抽出する年齢の下限条件
' ==============================================================================
Public Function PrintTableDataParameterized(ByVal conn As Object, ByVal tableName As String, ByVal targetDep As String, ByVal targetAge As Long) As Boolean
    PrintTableDataParameterized = False
    
    If conn Is Nothing Or Trim(tableName) = "" Then
        Debug.Print "[WARNING] PrintTableDataParameterized: 引数が無効です。"
        Exit Function
    End If
    
    Dim cmd As Object
    Dim rs As Object
    On Error GoTo ErrorHandler
    
    ' Commandオブジェクトの生成
    Set cmd = CreateObject("ADODB.Command")
    
    With cmd
        Set .ActiveConnection = conn
        .CommandType = 1 ' adCmdText = 1
        ' プレースホルダーとして ? を使用(AccessのOLEDBでは位置ベース)
        .CommandText = "SELECT * FROM [" & tableName & "] WHERE [部署] = ? AND [年齢] >= ?"
        
        ' パラメータの追加(adVarChar = 200, adInteger = 3, adParamInput = 1)
        .Parameters.Append .CreateParameter("pDep", 200, 1, 50, targetDep)
        .Parameters.Append .CreateParameter("pAge", 3, 1, , targetAge)
        
        ' 実行してレコードセットを取得
        Set rs = .Execute
    End With
    
    Debug.Print "=================================================="
    Debug.Print " [Parameterized Query Data] " & tableName & " (部署: " & targetDep & " / 年齢以上: " & targetAge & ")"
    Debug.Print "=================================================="
    
    ' フィールド名のヘッダー行を作成
    Dim headerStr As String
    Dim i As Long
    headerStr = ""
    For i = 0 To rs.Fields.Count - 1
        headerStr = headerStr & Left(rs.Fields(i).Name & Space(15), 15) & " | "
    Next i
    Debug.Print headerStr
    Debug.Print String(Len(headerStr), "-")
    
    ' レコードのループ出力
    Do While Not rs.EOF
        Dim rowStr As String
        rowStr = ""
        For i = 0 To rs.Fields.Count - 1
            Dim valStr As String
            valStr = CStr(Nz(rs.Fields(i).Value, ""))
            rowStr = rowStr & Left(valStr & Space(15), 15) & " | "
        Next i
        Debug.Print rowStr
        
        rs.MoveNext
    Loop
    
    Debug.Print "=================================================="
    
    rs.Close
    Set rs = Nothing
    Set cmd = Nothing
    
    PrintTableDataParameterized = True
    Exit Function

ErrorHandler:
    Debug.Print "[WARNING] PrintTableDataParameterized: パラメータクエリ実行エラー (Err:" & Err.Number & " - " & Err.Description & ")"
    On Error Resume Next
    If Not rs Is Nothing Then rs.Close: Set rs = Nothing
    Set cmd = Nothing
    On Error GoTo 0
    
    PrintTableDataParameterized = False
End Function

' ==============================================================================
' [機能名] データベース接続の安全な切断・解放
' ==============================================================================
Public Sub CloseDatabaseConnection(ByRef conn As Object)
    On Error Resume Next
    If Not conn Is Nothing Then
        If conn.State = 1 Then
            conn.Close
            Debug.Print "[INFO] Database Disconnected."
        End If
        Set conn = Nothing
    End If
    On Error GoTo 0
End Sub

' Null安全対策ヘルパー
Private Function Nz(ByVal varValue As Variant, ByVal valueIfNull As Variant) As Variant
    If IsNull(varValue) Then Nz = valueIfNull Else Nz = varValue
End Function

2. 単体テスト用コード(パラメータ化クエリの実行テスト)

' ==============================================================================
' [機能名] パラメータ化クエリによるデータ取得テスト
' ==============================================================================
Public Sub Test_GetParameterizedEmployeeData()
    Dim startTime As Double
    startTime = Timer
    
    Debug.Print "[START] Test_GetParameterizedEmployeeData (" & Format(Now, "yyyy/mm/dd hh:nn:ss") & ")"
    Debug.Print "=================================================="
    Debug.Print " パラメータ化クエリ データ取得テスト開始"
    Debug.Print "=================================================="
    
    Dim targetDbName As String
    targetDbName = "SampleDB.accdb"
    
    ' 1. 接続 (Connect) - 共通関数を使用
    Dim conn As Object
    Set conn = OpenDatabaseConnection(targetDbName)
    
    If conn Is Nothing Then
        Debug.Print "[FAIL] 接続テスト: DB接続に失敗しました。"
        GoTo Test_End
    Else
        Debug.Print "[PASS] 接続テスト: DB接続成功"
    End If
    
    ' 2. パラメータ化クエリによるデータ取得 (部署 = '営業部', 年齢 >= 30)
    Dim targetTable As String
    targetTable = "社員マスタ"
    
    Dim isSuccess As Boolean
    isSuccess = PrintTableDataParameterized(conn, targetTable, "営業部", 30)
    
    If isSuccess Then
        Debug.Print "[PASS] パラメータクエリテスト: データの安全な抽出 OK"
    Else
        Debug.Print "[FAIL] パラメータクエリテスト: データの取得に失敗しました。"
    End If

Test_End:
    ' 3. 切断 (Disconnect) - 共通Subを使用
    CloseDatabaseConnection conn
    
    If conn Is Nothing Then
        Debug.Print "[PASS] 切断テスト: コネクションの安全な破棄 OK"
    Else
        Debug.Print "[FAIL] 切断テスト: コネクションの解放漏れ"
    End If
    
    Debug.Print "=================================================="
    
    Dim elapsed As Double
    elapsed = Timer - startTime
    If elapsed < 0 Then elapsed = elapsed + 86400
    
    Debug.Print "[END] Test_GetParameterizedEmployeeData (Elapsed: " & Format(elapsed, "0.000") & " sec)"
End Sub

3. 実行結果(イミディエイトウィンドウの出力)

[START] Test_GetParameterizedEmployeeData (2026/10/11 19:01:22)
==================================================
パラメータ化クエリ データ取得テスト開始
==================================================
[INFO] Database Connected: SampleDB.accdb
[PASS] 接続テスト: DB接続成功
==================================================
[Parameterized Query Data] 社員マスタ (部署: 営業部 / 年齢以上: 30)
==================================================
ID | 氏名 | 部署 | 年齢 |
------------------------------------------------------------------------
2 | 山田太郎 | 営業部 | 30 |
==================================================
[PASS] パラメータクエリテスト: データの安全な抽出 OK
[INFO] Database Disconnected.
[PASS] 切断テスト: コネクションの安全な破棄 OK
==================================================
[END] Test_GetParameterizedEmployeeData (Elapsed: 0.438 sec)

PR

【VBA】文字化け地獄を完全解決!BOM有無・UTF-8/Shift-JISを自動判別して安全にCSVを読み込む「文字コード自動判定関数」

VBAの標準機能に「文字コードを自動判別する」スイッチはありませんが、ファイルをバイナリ(生のバイト列)として読み込み、「BOMの有無」や「UTF-8特有のバイト並びルール」を判定する自作関数を作成すれば自動判別可能です。

以下のコードをモジュールに貼り付けることで、UTF-8(BOMあり/なし)かShift-JISかを判定し、適切な文字コードでファイルを読み込めるようになります。

1. 文字コード自動判別関数

Option Explicit

' ファイルの文字コード(UTF-8 または Shift-JIS)を判定する関数
Public Function DetectEncoding(ByVal filePath As String) As String
    Dim fileNum As Integer
    Dim bytes() As Byte
    Dim fileLen As Long
    
    fileNum = FreeFile
    Open filePath For Binary Access Read As #fileNum
    fileLen = LOF(fileNum)
    
    If fileLen = 0 Then
        Close #fileNum
        DetectEncoding = "Shift-JIS"
        Exit Function
    End If
    
    ' 先頭から最大4096バイトを取得して判定
    Dim readLen As Long
    readLen = IIf(fileLen > 4096, 4096, fileLen)
    ReDim bytes(0 To readLen - 1)
    Get #fileNum, 1, bytes
    Close #fileNum
    
    ' 1. UTF-8 BOMの判定 (EF BB BF)
    If readLen >= 3 Then
        If bytes(0) = &HEF And bytes(1) = &HBB And bytes(2) = &HBF Then
            DetectEncoding = "UTF-8"
            Exit Function
        End If
    End If
    
    ' 2. BOMなしUTF-8のバイト構造チェック
    Dim i As Long
    Dim isUtf8 As Boolean
    Dim hasMultiByte As Boolean
    isUtf8 = True
    
    i = 0
    Do While i < readLen
        Dim b As Byte
        b = bytes(i)
        
        If b < &H80 Then
            ' 1バイト文字 (ASCII)
            i = i + 1
        ElseIf (b And &HE0) = &HC0 Then
            ' UTF-8 2バイト文字
            If i + 1 >= readLen Or (bytes(i + 1) And &HC0) <> &H80 Then isUtf8 = False: Exit Do
            hasMultiByte = True
            i = i + 2
        ElseIf (b And &HF0) = &HE0 Then
            ' UTF-8 3バイト文字(日本語の多くはここ)
            If i + 2 >= readLen Or (bytes(i + 1) And &HC0) <> &H80 Or (bytes(i + 2) And &HC0) <> &H80 Then isUtf8 = False: Exit Do
            hasMultiByte = True
            i = i + 3
        ElseIf (b And &HF8) = &HF0 Then
            ' UTF-8 4バイト文字(絵文字など)
            If i + 3 >= readLen Or (bytes(i + 1) And &HC0) <> &H80 Or (bytes(i + 2) And &HC0) <> &H80 Or (bytes(i + 3) And &HC0) <> &H80 Then isUtf8 = False: Exit Do
            hasMultiByte = True
            i = i + 4
        Else
            ' UTF-8のルールに違反(=Shift-JISの可能性が高い)
            isUtf8 = False
            Exit Do
        End If
    Loop
    
    If isUtf8 And hasMultiByte Then
        DetectEncoding = "UTF-8"
    Else
        DetectEncoding = "Shift-JIS"
    End If
End Function

2. 自動判別してCSVを読み込む実装例

判別した文字コードを ADODB.Stream に渡すことで、文字化けを防いで読み込めます。

Public Sub ReadCSVWithAutoDetect(ByVal filePath As String)
    Dim charSet As String
    ' 文字コードを自動判別
    charSet = DetectEncoding(filePath)
    
    Dim stream As Object
    Set stream = CreateObject("ADODB.Stream")
    
    stream.Type = 2 ' adTypeText
    stream.Charset = charSet
    stream.Open
    stream.LoadFromFile filePath
    
    ' 全文読み込み
    Dim content As String
    content = stream.ReadText(-1)
    stream.Close
    
    MsgBox "判定結果: " & charSet & vbCrLf & vbCrLf & "先頭100文字:" & vbCrLf & Left(content, 100)
End Sub

【VBA】絶対パス・相対パス・UNCに対応!安全な「ファイル名抽出関数」の作り方(単体テストコード付き)

Excel VBAでファイル操作を行う際、ファイルパスから「ファイル名(拡張子付き)」だけを取り出したい場面は日常茶飯事です。

一見簡単そうに見えますが、実際の現場では以下のような多様なパス表記(表記ゆれ)やエッジケースが飛び交います。

  • C:\Folder\Sub\sample.xlsm (標準的な絶対パス)
  • .\sample.xlsm や ..\sample.xlsm (相対パス)
  • C:/Folder/Sub/sample.xlsm (Web由来やMac混在環境のスラッシュ区切り)
  • \\Server\Share\sample.xlsm (UNCネットワークパス)
  • C:\Folder\Sub\ や C:\ (末尾が \ で終わるフォルダ・ドライブ指定)

これらを安易な Split や Dir 関数で処理しようとすると、存在しないパスでエラーになったり、フォルダパスから誤った文字列を取得したりする原因になります。

今回は、「契約による設計(DbC)」のコメントフォーマットに則り、どんなパスが渡されても安全かつ超高速にファイル名のみを抽出する汎用関数と、その単体テストコードを解説します。

1. 実装:ファイル名抽出関数 (GetFileNameFromPath)

外部オブジェクト(FileSystemObject)を呼び出さず、VBA標準の文字列操作(InStrRev / Replace)のみで完結させているため、動作が非常に軽量です。

' ==============================================================================
' [機能名] パス文字列からのファイル名抽出
'
' [処理内容]
'   ・絶対パス、相対パス(.\ や ..\)、UNCパス、/区切りなど様々なパス形式から
'     ファイル名(拡張子含む)部分のみを抽出して返します。
'   ・末尾がパス区切り文字(\ や /)の場合やドライブ直下など、ファイル名が
'     指定されていない場合は空文字 ("") を返します。
'
' [引数]
'   @param  {String} pathString : 必須 : 解析対象のパス文字列
'
' [事前条件 (Requires)]
'   ・特になし(任意の文字列・空文字を受け付けます)。
'
' [事後条件 (Ensures)]
'   ・ファイル名が存在する場合 : 拡張子を含むファイル名文字列を返す(例: "sample.xlsm")。
'   ・ファイル名が存在しない場合 : 空文字 ("") を返す。
'
' [戻り値]
'   @return {String} 抽出されたファイル名
'
' [例外処理 (Throws)]
'   ・文字列操作のみを行うため実行時エラーは発生せず、不当な形式は空文字を返す。
'
' [履歴]
'   2026/10/08  [作成者名]  新規作成
' ==============================================================================
Public Function GetFileNameFromPath(ByVal pathString As String) As String
    ' 前後の空白を除去
    Dim cleanPath As String
    cleanPath = Trim(pathString)
    
    ' 1. 空文字チェック
    If cleanPath = "" Then
        GetFileNameFromPath = ""
        Exit Function
    End If
    
    ' 2. パス区切り文字を '\' に統一 (/ を \ に変換)
    cleanPath = Replace(cleanPath, "/", "\")
    
    ' 3. 末尾が '\' または ':' の場合(ディレクトリ指定、ドライブ指定)はファイル名なしと判定
    If Right$(cleanPath, 1) = "\" Or Right$(cleanPath, 1) = ":" Then
        GetFileNameFromPath = ""
        Exit Function
    End If
    
    ' 4. 末尾から最後の '\' を検索
    Dim lastSlashPos As Long
    lastSlashPos = InStrRev(cleanPath, "\")
    
    If lastSlashPos > 0 Then
        ' '\' より右側の文字列を取得
        GetFileNameFromPath = Mid$(cleanPath, lastSlashPos + 1)
    Else
        ' '\' が見つからない場合、ドライブ文字(例 "C:sample.xlsm")の可能性を考慮
        Dim lastColonPos As Long
        lastColonPos = InStrRev(cleanPath, ":")
        
        If lastColonPos > 0 Then
            GetFileNameFromPath = Mid$(lastColonPos + 1)
        Else
            ' パス区切りもコロンもない場合は、入力文字列全体をファイル名とみなす
            GetFileNameFromPath = cleanPath
        End If
    End If
End Function

設計のポイント

  • 表記ゆれの自動吸収:Replace(cleanPath, "/", "\") で区切り文字をあらかじめ統一することで、Linux/Mac由来やHTMLリンク形式のパス(/)も問題なく解析できます。
  • 「ファイル名なし」のガード:末尾が \ や : で終わっている場合(例: C:\Folder\ や C:)は「フォルダまたはドライブ全体の指図」とみなして即座に空文字 "" を返します。
  • ファイルの実在に依存しない:Dir 関数を使わず純粋な文字列解析を行っているため、「まだ保存されていないパス」や「アクセス権限のないネットワークパス」であっても正確にファイル名を抽出できます。

2. 品質を担保する単体テストコード

実務で「本当にあらゆるパターンで動くか?」を検証するために、データ駆動型のテストプロシージャを用意します。
エントリーポイントには以前紹介した RAIIパターン(CAspect)を配置し、テスト実行ログと処理時間を自動で記録します。

' ==============================================================================
' [機能名] GetFileNameFromPath 関数の網羅的テスト
' ==============================================================================
Public Sub Test_GetFileNameFromPath()
    ' エントリーポイント用アスペクト(ログ・実行時間自動化)
    Dim aspect As New CAspect: aspect.Begin "Test_GetFileNameFromPath"
    
    ' テストケース(入力パス と 期待される結果 のペア)
    Dim testCases As Variant
    testCases = Array( _
        Array("C:\Folder\Sub\sample.xlsm",    "sample.xlsm",  "標準的な絶対パス"), _
        Array("C:\sample.xlsm",                  "sample.xlsm",  "ドライブ直下のファイル"), _
        Array(".\sample.xlsm",                      "sample.xlsm",  "カレント相対パス (.\)"), _
        Array("..\Folder\sample.xlsm",              "sample.xlsm",  "親ディレクトリ相対パス (..\)"), _
        Array("sample.xlsm",                        "sample.xlsm",  "ファイル名のみ"), _
        Array("C:/Folder/Sub/sample.xlsm",    "sample.xlsm",  "スラッシュ区切り (/)"), _
        Array("\\Server\Share\sample.xlsm",      "sample.xlsm",  "UNCネットワークパス"), _
        Array("C:\Folder\Sub\",                  "",            "末尾が\(フォルダ指定)"), _
        Array("C:\",                              "",            "ドライブルート (C:\)"), _
        Array("C:",                                "",            "ドライブ指定 (C:)"), _
        Array("\\Server\Share\",                  "",            "UNC共有ルート"), _
        Array("",                                  "",            "空文字") _
    )
    
    Debug.Print "=================================================="
    Debug.Print " GetFileNameFromPath テスト開始"
    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 inputPath As String: inputPath = testCases(i)(0)
        Dim expected As String:  expected = testCases(i)(1)
        Dim memo As String:      memo = testCases(i)(2)
        
        ' 関数の実行
        Dim actual As String
        actual = GetFileNameFromPath(inputPath)
        
        ' 結果判定
        If actual = expected Then
            passCount = passCount + 1
            Debug.Print "[PASS] " & memo & " => """ & actual & """"
        Else
            failCount = failCount + 1
            Debug.Print "[FAIL] " & memo
            Debug.Print "      入力: """ & inputPath & """"
            Debug.Print "      期待: """ & expected & """"
            Debug.Print "      実際: """ & actual & """"
        End If
    Next i
    
    Debug.Print "--------------------------------------------------"
    Debug.Print " 結果: SUCCESS=" & passCount & " / FAIL=" & failCount
    Debug.Print "=================================================="
End Sub

3. 実行結果(イミディエイトウィンドウ)

10:00:00 [START] Test_GetFileNameFromPath (User: developer)
==================================================
 GetFileNameFromPath テスト開始
==================================================
[PASS] 標準的な絶対パス => "sample.xlsm"
[PASS] ドライブ直下のファイル => "sample.xlsm"
[PASS] カレント相対パス (.\) => "sample.xlsm"
[PASS] 親ディレクトリ相対パス (..\) => "sample.xlsm"
[PASS] ファイル名のみ => "sample.xlsm"
[PASS] スラッシュ区切り (/) => "sample.xlsm"
[PASS] UNCネットワークパス => "sample.xlsm"
[PASS] 末尾が\(フォルダ指定) => ""
[PASS] ドライブルート (C:\) => ""
[PASS] ドライブ指定 (C:) => ""
[PASS] UNC共有ルート => ""
[PASS] 空文字 => ""
--------------------------------------------------
 結果: SUCCESS=12 / FAIL=0
==================================================
10:00:00 [END]   Test_GetFileNameFromPath (0.001s)

文字列操作系の便利関数を作成する際は、このように「事前条件・事後条件を明記した汎用関数」と「配列を使った自動テスト」をセットで用意しておくことで、将来の改修でもデグレード(先祖返りバグ)を恐れずに開発を進めることができます。チームでのVBA開発や共通ライブラリ化にぜひ役立ててみてください。


【VBA】Dir 関数の罠を撃退!エラーとファイルを完全判別する「安全なフォルダ存在確認関数」(テストコード付き)

前回の「ファイル存在確認」に続き、今回は「指定したフォルダ(ディレクトリー)が存在するかどうか」を判定する処理を解説します。

「フォルダがあるか確認するだけなら Dir(path, vbDirectory) で一発では?」と思われがちですが、実はVBA標準の Dir 関数でフォルダ確認を行うと、ファイル存在確認以上に凶悪な誤判定やエラーに直面します。

今回は、フォルダ存在確認で陥りがちな罠を整理し、「契約による設計(DbC)」のコメントフォーマットに則った安全な汎用関数を作成します。

1. フォルダ存在確認で陥りやすい「3つの罠」

  • 罠①: Dir(path, vbDirectory) は「ファイル」もヒットしてしまう
    Dir("C:\Test\sample.txt", vbDirectory) のように、ファイルパスを渡しても sample.txt という文字列が返ってきます。つまり Dir の第2引数に vbDirectory を指定しても「フォルダのみを検索する」という意味にはならず、ファイルも判定対象に含まれてしまうため誤判定の原因になります。
  • 罠②: パス末尾の \(スラッシュ・バックスラッシュ)による挙動の違い
    GetAttr や Dir を使う際、C:\TestFolder\ のように末尾に \ が付いていると実行時エラー(エラー53: ファイルが見つかりません)になるというVBA独自の癖があります。ただし、C:\ や D:\ といったドライブルートだけは末尾の \ が必須であるため、一括で削除すると崩れてしまいます。
  • 罠③: 権限不足やネットワーク切断によるクラッシュ
    ファイル時と同様、アクセス権限がない共有フォルダや、切断されたネットワークドライブ(\\Server\Share)を指定すると、Dir や GetAttr が実行時エラー 70 や 76 でマクロを強制終了させます。

2. 実装:安全なフォルダ存在確認関数 (FolderExists)

これらの罠をすべてクリアし、ファイルとの誤認識を防ぎながら安全に Boolean(True / False)を返す関数です。

' ==============================================================================
' [機能名] 安全なフォルダ存在確認
'
' [処理内容]
'   ・指定されたパスに「フォルダ」が存在するかどうかを確認します。
'   ・ファイルパスが渡された場合や、アクセス権限不足・パス不正の場合は False を返します。
'
' [引数]
'   @param  {String} folderPath : 必須 : 確認対象のフォルダパス
'
' [事前条件 (Requires)]
'   ・特になし(空文字やファイルパス、末尾 \ あり/なし いずれも受け付けます)。
'
' [事後条件 (Ensures)]
'   ・指定パスにアクセス可能で、かつ「フォルダ」が存在する場合 : True を返す。
'   ・フォルダが存在しない、ファイルである、または権限不足でアクセス不可の場合 : False を返す。
'
' [戻り値]
'   @return {Boolean} フォルダの存在判定結果
'
' [例外処理 (Throws)]
'   ・アクセス権限エラー(70)やパス不在(53/76)等は内部で捕捉し、[WARNING] ログを出力して False を返す。
'
' [履歴]
'   2026/10/08  [作成者名]  新規作成
' ==============================================================================
Public Function FolderExists(ByVal folderPath As String) As Boolean
    ' 前後の空白を除去
    Dim cleanPath As String
    cleanPath = Trim(folderPath)
    
    ' 1. 空文字チェック
    If cleanPath = "" Then
        FolderExists = False
        Exit Function
    End If
    
    ' 2. パス区切り文字を '\' に統一 (/ を \ に変換)
    cleanPath = Replace(cleanPath, "/", "\")
    
    ' 3. 末尾の '\' の正規化
    ' ドライブルート (例: "C:\" や "D:\") 以外で、末尾に '\' がある場合は除去する
    ' (※ GetAttr は通常フォルダの末尾に '\' がついているとエラーになるため)
    If Right$(cleanPath, 1) = "\" And Not cleanPath Like "?:\" Then
        cleanPath = Left$(cleanPath, Len(cleanPath) - 1)
    End If
    
    ' 4. GetAttr による属性確認(エラーを内部でキャッチ)
    On Error Resume Next
    Dim fileAttr As VbFileAttribute
    fileAttr = GetAttr(cleanPath)
    
    Dim errNum As Long: errNum = Err.Number
    Dim errDesc As String: errDesc = Err.Description
    On Error GoTo 0
    
    ' エラーが発生した場合(パス未存在、アクセス権限不足など)
    If errNum <> 0 Then
        ' 存在しないパスの場合は静かに False(権限エラー等の場合はログを残す)
        If errNum <> 53 And errNum <> 76 Then
            Debug.Print "[WARNING] フォルダアクセス不可 (Err:" & errNum & " - " & errDesc & ") Path: " & cleanPath
        End If
        FolderExists = False
        Exit Function
    End If
    
    ' 5. ビット演算による「フォルダ属性」の確定
    ' 対象がファイルではなく、本当にフォルダ (vbDirectory) か判定
    If (fileAttr And vbDirectory) = vbDirectory Then
        FolderExists = True
    Else
        ' 存在するが「ファイル」だった場合は False
        FolderExists = False
    End If
End Function

3. 単体テスト用コード

正常なフォルダ、ドライブルート、末尾 \ 付きパス、存在しないフォルダ、そして「あえてファイルパスを渡すケース」などを網羅したテストプロシージャです。
エントリーポイント用のアスペクト(CAspect)を併用し、テスト実行ログを自動記録します。

' ==============================================================================
' [機能名] FolderExists 関数の網羅的テスト
' ==============================================================================
Public Sub Test_FolderExists()
    ' エントリーポイント用アスペクト(実行時間・ログ自動化)
    Dim aspect As New CAspect: aspect.Begin "Test_FolderExists"
    
    Debug.Print "=================================================="
    Debug.Print " FolderExists 単体テスト開始"
    Debug.Print "=================================================="
    
    ' 1. 正常系:自ブックが存在するフォルダ(末尾 \ なし)
    Dim validFolderPath As String
    validFolderPath = ThisWorkbook.Path
    
    If FolderExists(validFolderPath) Then
        Debug.Print "[PASS] 正常系: 既存フォルダ判定 OK"
    Else
        Debug.Print "[FAIL] 正常系: 既存フォルダ判定 NG"
    End If
    
    ' 2. 正常系:末尾に '\' が付いたフォルダパス
    If FolderExists(validFolderPath & "\") Then
        Debug.Print "[PASS] 表記ゆれ系: 末尾\付きフォルダ判定 OK"
    Else
        Debug.Print "[FAIL] 表記ゆれ系: 末尾\付きフォルダ判定 NG"
    End If
    
    ' 3. 正常系:ドライブルート (C:\)
    If FolderExists("C:\") Then
        Debug.Print "[PASS] ドライブルート: C:\ 判定 OK"
    Else
        Debug.Print "[FAIL] ドライブルート: C:\ 判定 NG"
    End If
    
    ' 4. 異常系:存在しないフォルダ
    If Not FolderExists("C:\NonExistentFolder_9999") Then
        Debug.Print "[PASS] 異常系: 存在しないフォルダ判定 OK"
    Else
        Debug.Print "[FAIL] 異常系: 存在しないフォルダ判定 NG"
    End If
    
    ' 5. 誤認識防止系:フォルダではなく「ファイルパス」を渡した場合
    Dim currentFilePath As String
    currentFilePath = ThisWorkbook.FullName
    
    If Not FolderExists(currentFilePath) Then
        Debug.Print "[PASS] ファイル排除系: ファイルパスの誤認排除 OK"
    Else
        Debug.Print "[FAIL] ファイル排除系: ファイルをフォルダと誤認識 NG"
    End If
    
    Debug.Print "=================================================="
End Sub

4. 実行結果(イミディエイトウィンドウ)

10:00:00 [START] Test_FolderExists (User: developer)
==================================================
 FolderExists 単体テスト開始
==================================================
[PASS] 正常系: 既存フォルダ判定 OK
[PASS] 表記ゆれ系: 末尾\付きフォルダ判定 OK
[PASS] ドライブルート: C:\ 判定 OK
[PASS] 異常系: 存在しないフォルダ判定 OK
[PASS] ファイル排除系: ファイルパスの誤認排除 OK
==================================================
10:00:00 [END]   Test_FolderExists (0.001s)

5. まとめ

  • GetAttr + ビット演算 (fileAttr And vbDirectory) が最強:Dir 関数に頼らず GetAttr を使うことで、ファイルとフォルダを厳密に区別できます。
  • 末尾の \ トリムは「ドライブルート除外」が必須:C:\Test の末尾 \ は削る必要がありますが、C:\ の \ を削ると C:(カレントドライブ相対パス)に意味が変わってしまうため、Not cleanPath Like "?:\" で条件分岐するのがポイントです。
  • エラーは関数内で隠蔽し、事後条件をシンプルに保つ:アクセス権限不足やネットワーク未接続などの予期せぬエラーをすべて内部で安全に処理(False を返却)することで、呼び出し側は If FolderExists(path) Then と書くだけで安全に後続処理へ進むことができます。

【VBA】再帰処理の疑問をスッキリ解決!フィボナッチ数列で学ぶ「再帰関数」の基本と実装(テストコード付き)

プログラミングの学習で「再帰処理(Recursion)」の題材として必ず登場するのがフィボナッチ数列です。

フィボナッチ数列とは、「前の2つの数を足したものが次の数になる」という以下のような数字の並びです。

1, 1, 2, 3, 5, 8, 13, 21, 34, 55 ...

今回は、初学者の方が抱きがちな「どこまで求めるかを指定するの?」「3つ以上求めないと意味がない?」「VBAで再帰処理ってできるの?」という素朴な疑問に回答しながら、「契約による設計(DbC)」のコメントフォーマットに則った安全な再帰関数とテストコードを作成します。

1. 再帰とフィボナッチに関する3つの疑問に一発回答

  • Q1. どこまで求めるかを指定する?
    A. 「第 N 項目の値を求める(例: N = 10)」という形で指定します。
    「10番目のフィボナッチ数はいくつか?」を指定して呼び出し、単体の値(N = 10 なら 55)を計算して返します。
  • Q2. 3つ以上求めないと意味がない?
    A. その通りです!数学的・プログラミング的に非常に鋭い着眼点です。
    フィボナッチ数列の定義は以下のようになっています。
    • 第1項 (N = 1) : 1 (計算不要・固定値)
    • 第2項 (N = 2) : 1 (計算不要・固定値)
    • 第3項 (N = 3) : 第1項 + 第2項 = 2 (ここで初めて「自分自身を呼び出す計算」が発生する!)
    プログラミングでは、N = 1 や N = 2 のような計算不要の境界条件を「基本ケース(Base Case)」と呼び、直接値を返して処理を終了します。
    自身を再帰呼び出し(Recursive Step)して計算を行うのは N >= 3(3つ以上)になってから なので、「3つ以上でないと再帰の意味がない」というのはまさにその通りです。
  • Q3. VBAで再帰処理はできる?
    A. 全く問題なく処理できます!
    VBAのプロシージャ(Function)は、自分自身を呼び出すことが可能です。
    ただし、後述する「計算量の爆発(N が大きくなるとフリーズする問題)」と「数値のオーバーフロー」には注意が必要です。

2. 実装:再帰によるフィボナッチ計算関数 (Fibonacci)

事後条件・事前条件(N の有効範囲)を明確にした関数です。

' ==============================================================================
' [機能名] 再帰処理によるフィボナッチ数の計算
'
' [処理内容]
'   ・第 n 項のフィボナッチ数を再帰呼び出し (Fibonacci(n-1) + Fibonacci(n-2)) により計算します。
'
' [引数]
'   @param  {Long} n : 必須 : 求めたいフィボナッチ数の項数 (1 以上の整数)
'
' [事前条件 (Requires)]
'   ・n >= 1 であること。
'   ・n <= 40 であること(※単純再帰のため、40を超えると計算時間が膨大になりフリーズします)。
'
' [事後条件 (Ensures)]
'   ・n = 1 または n = 2 の場合 : 1 を返す (Base Case)。
'   ・n >= 3 の場合 : Fibonacci(n - 1) + Fibonacci(n - 2) の計算結果を返す。
'   ・事前条件違反の場合 : 0 を返し、[ERROR] ログを出力する。
'
' [戻り値]
'   @return {Long} 指定された項のフィボナッチ数
'
' [例外処理 (Throws)]
'   ・n < 1 または n > 40 の場合はエラーログを出力して 0 を返却。
'
' [履歴]
'   2026/10/08  [作成者名]  新規作成
' ==============================================================================
Public Function Fibonacci(ByVal n As Long) As Long
    ' 1. 事前条件(Requires)のチェック
    If n < 1 Then
        Debug.Print "[ERROR] Fibonacci: 引数 n は 1 以上の整数を指定してください。 (指定値: " & n & ")"
        Fibonacci = 0
        Exit Function
    End If
    
    ' 単純再帰の計算量増大(フリーズ)を防ぐ上限ガード
    If n > 40 Then
        Debug.Print "[ERROR] Fibonacci: 単純再帰のため n <= 40 に制限されています。 (指定値: " & n & ")"
        Fibonacci = 0
        Exit Function
    End If
    
    ' 2. 基本ケース (Base Case): 再帰を止める終了条件
    ' n = 1 または n = 2 の時は再帰計算を行わず 1 を返す
    If n = 1 Or n = 2 Then
        Fibonacci = 1
        Exit Function
    End If
    
    ' 3. 再帰呼び出し (Recursive Step): n >= 3 の場合
    ' 自身を 2 回呼び出して足し合わせる
    Fibonacci = Fibonacci(n - 1) + Fibonacci(n - 2)
End Function

3. 単体テスト&指定項までの数列一覧出力コード

指定した項数までのフィボナッチ数列を一覧表示し、正しく再帰が機能しているかを検証するテストプロシージャです。
エントリーポイントのため、RAIIパターン(CAspect)で自動ログ出力を行っています。

' ==============================================================================
' [機能名] Fibonacci 関数の動作テストおよび数列出力
' ==============================================================================
Public Sub Test_Fibonacci()
    ' エントリーポイント用アスペクト(ログ・実行時間自動化)
    Dim aspect As New CAspect: aspect.Begin "Test_Fibonacci"
    
    Debug.Print "=================================================="
    Debug.Print " フィボナッチ数列 再帰計算テスト"
    Debug.Print "=================================================="
    
    ' 1. 第 1 項から第 10 項までを順番に計算して表示
    Dim maxTerms As Long: maxTerms = 10
    Debug.Print "--- 第 1 項 〜 第 " & maxTerms & " 項の一覧 ---"
    
    Dim i As Long
    For i = 1 To maxTerms
        Dim result As Long
        result = Fibonacci(i)
        Debug.Print "F(" & Format$(i, "00") & ") = " & result
    Next i
    
    ' 2. 境界値・事前条件違反のテスト
    Debug.Print "--- ガード処理のテスト ---"
    
    ' 不正値 (0 以下)
    Call Fibonacci(0)
    
    ' 上限超え (41 以上)
    Call Fibonacci(45)
    
    Debug.Print "=================================================="
End Sub

4. 実行結果(イミディエイトウィンドウ)

10:00:00 [START] Test_Fibonacci (User: developer)
==================================================
 フィボナッチ数列 再帰計算テスト
==================================================
--- 第 1 項 〜 第 10 項の一覧 ---
F(01) = 1
F(02) = 1
F(03) = 2
F(04) = 3
F(05) = 5
F(06) = 8
F(07) = 13
F(08) = 21
F(09) = 34
F(10) = 55
--- ガード処理のテスト ---
[ERROR] Fibonacci: 引数 n は 1 以上の整数を指定してください。 (指定値: 0)
[ERROR] Fibonacci: 単純再帰のため N <= 40 に制限されています。 (指定値: 45)
==================================================
10:00:00 [END]   Test_Fibonacci (0.002s)

5. コラム:画面フリーズを防ぐ DoEvents は入れるべき?

VBAで重い計算やループを行う際、「Windowsに処理を一時返還して画面フリーズを防ぐ DoEvents 関数」を入れるべきか迷うことがあります。

結論として、今回のコードでは DoEvents は不要(入れない方が良い) です。

  • 事前条件ガードがあるため:今回は N <= 40 という制限をかけているため、計算時間は一瞬(1秒未満)で終了します。画面が固まる心配はありません。
  • 処理速度の激減を防ぐため:再帰処理や高速なループの中で DoEvents を呼び出すと、OSとの対話オーバーヘッドにより処理速度が数十〜数百倍遅くなる副作用があります。

DoEvents は「数十秒以上かかる巨大な処理」において、1,000回に1回など間引いて呼び出すのが基本ルールと覚えておきましょう。

6. まとめ

  • 「基本ケース(Base Case)」の徹底:N >= 3 になって初めて自分自身(再帰)を呼び出すという構造を理解し、停止条件(N = 1, 2)を忘れないことが無限ループ(スタックオーバーフロー)を防ぐ鍵となります。
  • 事前条件(Requires)でガードする:単純再帰は N が大きくなると計算量が指数関数的に増大するため、N <= 40 のような明確な境界線を設けて安全性を確保するのがプログラミングのベストプラクティスです。