t-hom’s diary

主にVBAネタを扱っているブログ…とも言えなくなってきたこの頃。

VBA IfやForの終了構文を自動補完するマクロ

※タイトルにつられた皆さん。多分イメージしてるものと違いますのでガッカリされないようご注意ください。

Twitterで、IfやForの終了構文を自動補完することができないのかなという疑問が投げかけられた。
VBEには文字入力を検知してマクロを走らせるような機能は無いので、たとえば If ~ Thenと書いてEnterを押すと自動でEnd Ifが入るような仕組みは作れない。
どうしてもやろうと思ったら外部のスニペットソフトを作ってグローバルキーフックという技術もあるけど、大掛かりになるし安定動作するかも分からない。

そこで方向性を変えて、Pythonっぽく終了構文なしでインデントで表されたコードを、正規のVBAコードに変換するようなマクロを書いてみた。
ただし思考実験みたいなものなので、考慮漏れしてるケースも多数あるだろうしまともにテストしていない。
それに、既に終了構文があっても追記してしまうという重大な欠陥がある。

従って、これはただのアイデア紹介のサンプルコードであり、間違ってもこのまま実務で使おうなどと考えないこと。

実行イメージ

マクロ実行前のコードがこちらだとすると、

Sub fnFizzBuzz()
    Dim ret, i
    For i = 1 To 100
        Select Case 0
            Case i Mod 15
                ret = "FizzBuzz"
            Case i Mod 3
                ret = "Fizz"
            Case i Mod 5
                ret = "Buzz"
            Case Else
                ret = CStr(i)

    fnFizzBuzz = ret
End Sub

マクロによりEnd SelectとNextが補完されたのがこちら。

Sub fnFizzBuzz()
    Dim ret, i
    For i = 1 To 100
        Select Case 0
            Case i Mod 15
                ret = "FizzBuzz"
            Case i Mod 3
                ret = "Fizz"
            Case i Mod 5
                ret = "Buzz"
            Case Else
                ret = CStr(i)
        End Select
    Next

    fnFizzBuzz = ret
End Sub

マクロでVBプロジェクトに直接アクセスする手もあるが、今回は単純にクリップボードにコードをコピーした状態でマクロを実行するとイミディエイトウィンドウに出力される仕様とした。

まず必要なのは、このブログではおなじみのStackクラス。
クラスモジュールを挿入してオブジェクト名をStackとし、以下のコードを貼り付け。

Private Items() As Variant

Property Get Count() As Integer
    Count = UBound(Items)
End Property

Property Get Top() As Variant
    Top = Items(UBound(Items))
End Property

Public Function Pop() As Variant
    If UBound(Items) > 0 Then
        Pop = Items(UBound(Items))
        ReDim Preserve Items(UBound(Items) - 1)
    Else
        Pop = Empty
    End If
End Function

Public Sub Push(ByRef x As Variant)
    ReDim Preserve Items(UBound(Items) + 1)
    Items(UBound(Items)) = x
End Sub

Private Sub Class_Initialize()
    ReDim Items(0)
End Sub

そして以下の2点を参照設定
Microsoft VBScript Regular Expressions 5.5
Microsoft Forms 2.0 Object Library

Formsの方はユーザーフォームを挿入してから削除すると参照設定されるのでそのやり方がオススメ。

そしてメインの標準モジュール(名前は任意)を挿入し、以下のコードを張り付け。

'必要な参照設定
'  Microsoft VBScript Regular Expressions 5.5
'
'  Microsoft Forms 2.0 Object Library
'  (こちらはフォームモジュール挿入して削除すれば参照設定される)
Sub FromIndentStyleToCorrectVBAStyle()
    With New DataObject
        .GetFromClipboard
        Dim arr: arr = Split(.GetText, vbNewLine)
    End With
    
    Dim StatementStack As Stack: Set StatementStack = New Stack
    Dim IndentStack As Stack: Set IndentStack = New Stack
    StatementStack.Push "Dummy"
    IndentStack.Push -1
    
    Dim i
    For i = LBound(arr) To UBound(arr)
        indents = GetIndentNumber(arr(i))
        Do While indents <= IndentStack.Top
            indents = IndentStack.Top - 4
            Debug.Print Space(IndentStack.Pop) & StatementStack.Pop
        Loop
        Debug.Print arr(i)
        es = GetEndStatement(arr(i))
        If es <> "" Then
            StatementStack.Push es
            IndentStack.Push indents
        End If
    Next
End Sub

Function GetIndentNumber(codeLine)
    Dim n: n = 1
    Do While Mid(codeLine, n, 1) = " "
        n = n + 1
    Loop
    n = n - 1
    GetIndentNumber = n
End Function

Function GetEndStatement(ByVal begin)
    Dim ret As String
    
    Dim re As RegExp: Set re = New RegExp
    begin = Trim(begin)
    
    re.Pattern = "^For .*"
    If re.Test(begin) Then
        ret = "Next": GoTo Fin
    End If
    
    re.Pattern = "^If .* Then$"
    If re.Test(begin) Then
        ret = "End If": GoTo Fin
    End If
    
    re.Pattern = "^Select Case "
    If re.Test(begin) Then
        ret = "End Select": GoTo Fin
    End If
    
    re.Pattern = "^Do"
    If re.Test(begin) Then
        ret = "Loop": GoTo Fin
    End If
    
Fin:
    GetEndStatement = ret
End Function

後はPythonみたいにインデントのみで書かれたコードをコピーして、FromIndentStyleToCorrectVBAStyleを実行すると、終了構文が補完されたコードがイミディエイトウィンドウに出力される。

一応作ってみたものの、私自身は毎日VBAに触ってるのであまりタイピングが面倒という感覚がなくなってきている。
ツール作成で一番時間を食うのは試行錯誤やデバッグなので、タイピングのロスは誤差の範囲。
だからまぁ、これを自分で使うことは無いだろうなぁ。

追記(言い訳)

今回はとりあえずアイデアを形にするためのやっつけコーディングなので、より良い方法があっても突っ込まないで欲しい。
たとえばスタックを2個使ってるけど、本当は関連項目だからユーザー定義型かクラスにしてデータをまとめて1つのスタックに積むか、スタック自体を改変して複数個を1領域にPushできる仕様にすべきだったと思う。
※Popが多値になるのでそれはそれで扱い注意だけど。

それと、冒頭で述べた既に終了構文があっても追記してしまうという重大な欠陥について、修正できなかった訳じゃないけど、このアイデアにそれほど思い入れが無いので放置することにした。もしこのアイデアを気に入った人がいたら直して使って欲しい。

当ブログは、amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、 Amazonアソシエイト・プログラムの参加者です。