まず最初に今回の記事のオチを読もう。
概要
Excel VBA にて正規表現クラスを作った。正規表現の取り扱いは JScript 側に一任し,これを ScriptControl 経由で呼び出すようにした。クラス名やメソッド,プロパティなどインタフェースは VBScript の RegExp
クラスに限りなく寄せた,いわゆるラッパーを作ったというもの。なお,ScriptControl を用いたことにより,基本的に 32bit 版 Office でしか使えない。
はじめに
VBScript が廃止されるらしい。
Windows Script Host (WSH) で VBScript のソフト資産がある人は JScript や PowerShell に移行せざるを得ないが,まだ VBScript が動いている間に移行テストを行うべきだろう。
問題は Excel VBA で正規表現を使うとき,実は内部で VBScript の正規表現クラス RegExp
を使っているのだ。VBScript が廃止されたら当然,これも使えなくなる。最近 Office 365 の VBA で正規表現がサポートされたが,買い切り版では(少なくともこの記事を書いている時点での最新版の Office 2024 でも)未サポートである。おそらく次のアップデート Office 2027(?) まで待たなくてはならない。
それまで買い切り版 Office ユーザーとしては代替案を考えなくてはならない。
- VBA を用いてフルスクラッチで自作する。
- SeleniumVBA の Regex-VBA を使う。
- vb2clr を使用して .NET Framework 4 の機能を利用する。
このうちフルスクラッチで自作することはあり得ない。自作に挑戦した記事を見てみたが,投入するコストに対するリターンが釣り合わない。そういう意味では SeleniumVBA のプロジェクトを尊敬する。彼らはフルスクラッチで実装したからだ。次の vb2clr のプロジェクトは既にある他言語の正規表現機能を利用するという意味でヒントになった。
そう,正規表現が基本言語仕様として組み込まれている JavaScript (JScript) を使えばいいことに気づいた。VBA からは ScriptControl を用いて JavaScript (JScript) のコードを簡単に呼び出せるからだ。であれば,大したコストはかからないので,ちょっと欲張って既存の RegExp
クラスの互換品を作ってみることにした。
準備
Excel の VBA エディタの参照設定で Microsoft Script Control 1.0 にチェックを付けておくこと。
実装コード
以下の4つのファイルから構成されている。Excel の VBA エディタからインポートして読み込むこと。
RegExp2.cls
のコードはコチラ
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "RegExp2"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit
'-------------------------------------------------------------------------------
'【非公開メンバ変数】
'-------------------------------------------------------------------------------
Private oScriptControl As ScriptControl
'-------------------------------------------------------------------------------
'【コンストラクタ】
'-------------------------------------------------------------------------------
Private Sub Class_Initialize()
Set oScriptControl = New ScriptControl
oScriptControl.Language = "JScript"
Dim s As String
Dim n As String
'---------------------------------------------------------------------------
'【メンバ変数】
'---------------------------------------------------------------------------
s = "": n = vbCrLf
s = s & n & "var regExp = null;"
s = s & n & "var lastIndex = null;"
s = s & n & "var globalFlag = false;"
s = s & n & "var ignoreCase = false;"
s = s & n & "var multiLine = false;"
s = s & n & "var pattern = null;"
'---------------------------------------------------------------------------
'【アクセサ】globalFlag
'---------------------------------------------------------------------------
s = s & n & "function setGlobalFlag(flag) {"
s = s & n & " if(globalFlag == flag) return;"
s = s & n & " globalFlag = flag;"
s = s & n & " regexp = null;"
s = s & n & "}"
s = s & n & "function getGlobalFlag(flag) {"
s = s & n & " return globalFlag;"
s = s & n & "}"
'---------------------------------------------------------------------------
'【アクセサ】ignoreCase
'---------------------------------------------------------------------------
s = s & n & "function setIgnoreCase(flag) {"
s = s & n & " if(ignoreCase == flag) return;"
s = s & n & " ignoreCase = flag;"
s = s & n & " regexp = null;"
s = s & n & "}"
s = s & n & "function getIgnoreCase() {"
s = s & n & " return ignoreCase;"
s = s & n & "}"
'---------------------------------------------------------------------------
'【アクセサ】multiLine
'---------------------------------------------------------------------------
s = s & n & "function setMultiLine(flag) {"
s = s & n & " if(multiLine == flag) return;"
s = s & n & " multiLine = flag;"
s = s & n & " regexp = null;"
s = s & n & "}"
s = s & n & "function getMultiLine() {"
s = s & n & " return multiLine;"
s = s & n & "}"
'---------------------------------------------------------------------------
'【アクセサ】pattern
'---------------------------------------------------------------------------
s = s & n & "function setPattern(pat) {"
s = s & n & " pattern = pat;"
s = s & n & " regexp = null;"
s = s & n & "}"
s = s & n & "function getPattern() {"
s = s & n & " return pattern;"
s = s & n & "}"
'---------------------------------------------------------------------------
'【メソッド】初期化
'---------------------------------------------------------------------------
s = s & n & "function initial() {"
s = s & n & " if(regexp == null) {"
s = s & n & " var flags = '';"
s = s & n & " if(globalFlag) flags += 'g';"
s = s & n & " if(ignoreCase) flags += 'i';"
s = s & n & " if(multiLine) flags += 'm';"
s = s & n & " if(flags == '')"
s = s & n & " regexp = new RegExp(pattern);"
s = s & n & " else"
s = s & n & " regexp = new RegExp(pattern, flags);"
s = s & n & " }"
s = s & n & " regexp.lastIndex = lastIndex = 0;"
s = s & n & "}"
'---------------------------------------------------------------------------
'【メソッド】Test
'---------------------------------------------------------------------------
s = s & n & "function commandTest(str) {"
s = s & n & " return regexp.test(str);"
s = s & n & "}"
'---------------------------------------------------------------------------
'【メソッド】Exec
'---------------------------------------------------------------------------
s = s & n & "function commandExec(str) {"
s = s & n & " var ret = regexp.exec(str);"
s = s & n & " if(ret == null) {"
s = s & n & " return {"
s = s & n & " FirstIndex: -1,"
s = s & n & " Length: -1,"
s = s & n & " Value: '',"
s = s & n & " SubMatches: []"
s = s & n & " };"
s = s & n & " } else {"
s = s & n & " var all = ret[0];"
s = s & n & " var pos = str.indexOf(all, lastIndex);"
s = s & n & " lastIndex = regexp.lastIndex;"
s = s & n & " return {"
s = s & n & " FirstIndex: pos,"
s = s & n & " Length: all.length,"
s = s & n & " Value: all,"
s = s & n & " SubMatches: ret.slice(1)"
s = s & n & " };"
s = s & n & " }"
s = s & n & "}"
'---------------------------------------------------------------------------
'【メソッド】Replace
'---------------------------------------------------------------------------
s = s & n & "function commandReplace(str, rep) {"
s = s & n & " return str.replace(regexp, rep);"
s = s & n & "}"
oScriptControl.AddCode s
End Sub
'-------------------------------------------------------------------------------
'【アクセサ】GlobalFlag
'-------------------------------------------------------------------------------
Public Property Let GlobalFlag(flag As Boolean)
CallByName oScriptControl.CodeObject, "setGlobalFlag", vbMethod, flag
End Property
Public Property Get GlobalFlag() As Boolean
GlobalFlag = CallByName(oScriptControl.CodeObject, "getGlobalFlag", vbMethod)
End Property
'-------------------------------------------------------------------------------
'【アクセサ】IgnoreCase
'-------------------------------------------------------------------------------
Public Property Let IgnoreCase(flag As Boolean)
CallByName oScriptControl.CodeObject, "setIgnoreCase", vbMethod, flag
End Property
Public Property Get IgnoreCase() As Boolean
IgnoreCase = CallByName(oScriptControl.CodeObject, "getIgnoreCase", vbMethod)
End Property
'-------------------------------------------------------------------------------
'【アクセサ】MultiLine
'-------------------------------------------------------------------------------
Public Property Let MultiLine(flag As Boolean)
CalByName oScriptControl.CodeObject, "setMultiLine", vbMethod, flag
End Property
Public Property Get MultiLine() As Boolean
IgnoreCase = CallByName(oScriptControl.CodeObject, "getMultiLine", vbMethod)
End Property
'-------------------------------------------------------------------------------
'【アクセサ】Pattern
'-------------------------------------------------------------------------------
Public Property Let Pattern(pat As String)
CallByName oScriptControl.CodeObject, "setPattern", vbMethod, pat
End Property
Public Property Get Pattern() As String
Pattern = CallByName(oScriptControl.CodeObject, "getPattern", vbMethod)
End Property
'-------------------------------------------------------------------------------
'【メソッド】Test
'-------------------------------------------------------------------------------
Public Function Test(str As String) As Boolean
CallByName oScriptControl.CodeObject, "initial", vbMethod
Test = CallByName(oScriptControl.CodeObject, "commandTest", vbMethod, str)
End Function
'-------------------------------------------------------------------------------
'【メソッド】Execute
'-------------------------------------------------------------------------------
Public Function Execute(str As String) As MatchCollection2
Dim oCollection As New MatchCollection2
Dim GlobalFlag As Boolean
Dim oMatch As Match2
Dim oSubMatches As SubMatches2
Dim obj As Object
Dim arr As Object
Dim i As Long
Dim n As Long
Dim pos As Long
CallByName oScriptControl.CodeObject, "initial", vbMethod
GlobalFlag = CallByName(oScriptControl.CodeObject, "getGlobalFlag", vbMethod)
Do While True
Set obj = CallByName(oScriptControl.CodeObject, "commandExec", vbMethod, str)
pos = CallByName(obj, "FirstIndex", vbGet)
If pos < 0 Then Exit Do
Set oMatch = New Match2
oMatch.FirstIndex = pos
oMatch.Length = CallByName(obj, "Length", vbGet)
oMatch.Value = CallByName(obj, "Value", vbGet)
Set oSubMatches = New SubMatches2
Set arr = CallByName(obj, "SubMatches", vbGet)
n = CallByName(arr, "length", vbGet)
For i = 0 To n - 1
oSubMatches.Add CallByName(arr, i, vbGet)
Next
Set oMatch.SubMatches = oSubMatches
oCollection.Add oMatch
If Not GlobalFlag Then Exit Do
Loop
Set Execute = oCollection
End Function
'-------------------------------------------------------------------------------
'【メソッド】Replace
'-------------------------------------------------------------------------------
Public Function Replace(str As String, rep As String) As String
CallByName oScriptControl.CodeObject, "initial", vbMethod
Replace = CallByName(oScriptControl.CodeObject, "commandReplace", vbMethod, str, rep)
End Function
MatchCollection2.cls
のコードはコチラ
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "MatchCollection2"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit
'-------------------------------------------------------------------------------
'【非公開メンバ変数】
'-------------------------------------------------------------------------------
Private oCollection As Collection
'-------------------------------------------------------------------------------
'【コンストラクタ】
'-------------------------------------------------------------------------------
Private Sub Class_Initialize()
Set oCollection = New Collection
End Sub
'-------------------------------------------------------------------------------
'【アクセサ】Count
'-------------------------------------------------------------------------------
Public Property Get Count() As Long
Count = oCollection.Count
End Property
'-------------------------------------------------------------------------------
'【アクセサ】Item
'-------------------------------------------------------------------------------
Public Property Get Item(n As Long) As Match2
Attribute Item.VB_UserMemId = 0
Set Item = oCollection(n + 1)
End Property
'-------------------------------------------------------------------------------
'【アクセサ】NewEnum
'-------------------------------------------------------------------------------
Public Property Get NewEnum() As IUnknown
Attribute NewEnum.VB_UserMemId = -4
Set NewEnum = oCollection.[_NewEnum]
End Property
'-------------------------------------------------------------------------------
'【メソッド】Add
'-------------------------------------------------------------------------------
Public Sub Add(obj As Match2)
oCollection.Add obj
End Sub
Match2.cls
のコードはコチラ
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "Match2"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit
'-------------------------------------------------------------------------------
'【公開メンバ変数】
'-------------------------------------------------------------------------------
Public FirstIndex As Long
Public Length As Long
Public SubMatches As SubMatches2
Public Value As String
Attribute Value.VB_VarUserMemId = 0
SubMatches2.cls
のコードはコチラ
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "SubMatches2"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit
'-------------------------------------------------------------------------------
'【非公開メンバ変数】
'-------------------------------------------------------------------------------
Private oCollection As Collection
'-------------------------------------------------------------------------------
'【コンストラクタ】
'-------------------------------------------------------------------------------
Private Sub Class_Initialize()
Set oCollection = New Collection
End Sub
'-------------------------------------------------------------------------------
'【アクセサ】Count
'-------------------------------------------------------------------------------
Public Property Get Count() As Long
Count = oCollection.Count
End Property
'-------------------------------------------------------------------------------
'【アクセサ】Item
'-------------------------------------------------------------------------------
Public Property Get Item(n As Long) As String
Attribute Item.VB_UserMemId = 0
Item = oCollection(n + 1)
End Property
'-------------------------------------------------------------------------------
'【アクセサ】NewEnum
'-------------------------------------------------------------------------------
Public Property Get NewEnum() As IUnknown
Attribute NewEnum.VB_UserMemId = -4
Set NewEnum = oCollection.[_NewEnum]
End Property
'-------------------------------------------------------------------------------
'【メソッド】Add
'-------------------------------------------------------------------------------
Public Sub Add(str As String)
oCollection.Add str
End Sub
比較表
VBScript 版の RegExp
クラスにクラス名やプロパティ,メソッド名は限りなく寄せたが,敢えて全く同じ名前にするのは避けてクラス名の末尾にはすべて 2
を付けた。また,プロパティの Global
は予約語として存在するため,GlobalFlag
に名前を変えた。
※印は既定のプロパティを示し,表記を省略することができるというもの。
クラス | 項目 | VBScript版 | JScript版 |
---|---|---|---|
正規表現 | クラス | RegExp |
RegExp2 |
プロパティ |
Global IgnoreCase MultiLine
|
GlobalFlag IgnoreCase MultiLine
|
|
メソッド |
Test Execute Replace
|
Test Execute Replace
|
|
マッチコレクション | クラス | MatchCollection |
MatchCollection2 |
プロパティ |
Count Item ※
|
Count Item ※
|
|
メソッド | Add |
||
マッチオブジェクト | クラス | Match |
Match2 |
プロパティ |
FirstIndex Length Value ※SubMatches
|
FirstIndex Length Value ※SubMatches
|
|
メソッド | |||
サブマッチコレクション | クラス | SubMatches |
SubMatches2 |
プロパティ |
Count Item ※
|
Count Item ※
|
|
メソッド | Add |
正規表現のコレクションは読み出し専用のため,Remove
メソッドは用意していないし,本来 Add
メソッドも見えてはいけないように思うのだが,隠し方が分からなかった。
使い方
ループカウンタ変数を用いてインデクス参照するときの例を示す。クラス名の変更(末尾に 2
を付ける)およびプロパティ名の変更 Global
→ GlobalFlag
だけで動く。
For Each
ループを用いる場合の例を示す。こちらの変更点も同じである。
ちなみに実行結果は下記のようになる。いずれのマクロも同じ結果となる。
まとめ
- 今回,アイディアを思いついてから突貫で作ったので,全くもってテスト不足である。バグだらけかもしれない点はご容赦願いたい。とくに大規模運用するとパフォーマンスが不足するかもしれない。
- 今回 VBScript の既存クラスと名前が衝突しないようにクラス名の末尾に一律
2
を付けたが,同じ名前にしてしまうのもアリかもしれない。その場合の変更点はプロパティ名の変更Global
→GlobalFlag
だけでよい。 - ScriptControl を用いるとデバッグがとても難しい。とくに JavaScript (JScript) 側でエラーが発生するともうお手上げである。
- 今回の記事は JScript 版の正規表現クラスを使ってみたいという人向けに実装コードを中心にまとめたものである。技術的な解説には興味ない人も多いだろうから,それは別の記事でまとめたい。
今回の記事のオチ
この記事を公開する直前に気づいたが,どうやら買い切り版の Office にも正規表現クラス RegExp
が追加されたようでサポート期限が2025年10月14日の Office 2016 / 2019 にも追加されている。もともとセキュリティ上の問題から VBScript を廃止する流れなので,セキュリティアップデートの一環として買い切り版の Office にも正規表現クラスを追加してくれたのだろう。64bit 版 Office でも使えるので嬉しい反面,本記事の JScript 版がただの習作になってしまい悲しくもある。
Excel 2019 のバージョンを確認すると下記の通り。
VBA エディタのオブジェクトブラウザを開くと RegExp
クラスが二つあることに気づく。このうち一つは既存の VBScript 版のほうである。参照設定で VBScript RegExp 5.5 のチェックを外すとコチラは消える。一方,新たに追加された RegExp クラスは VBScript 版とメンバーが同じであることが分かる。おそらく完全互換・代替品のつもりで作っていると思われる。
もちろん他のクラス Match
, MatchCollection
, SubMathces
も二つずつある。