ここで実装する Formats 関数は、かなり手抜きの実装のため、残念なバグがあります。ご注意ください。
VBA は文字列処理が弱いとか。まあ気持ちはわかる。しかし、ねぇ、必要最小限の機能は用意されている。さらに必要なものがあるのなら作りゃいいさ。お手本には事欠かないだろう。あの言語からこの言語へ。車輪なら輸入すればよい。
さて VBA である。Windows である。真似るなら C# あたりが妥当か。僕はこういうことがしたい。
[Immediate]
?Formats("{0:000} {{{1:yyyy/mm/dd}}} {2}", 1, Now, "Simple is best.")
001 {2012/04/10} Simple is best.
C# で言うところの String.Fromat だ。VBA にも Format があるが、こちらはあくまで単一の値の書式指定であり、複数の値を相手にすることはできない。この不便を解消したいのである。
おぜんだて
でどうするか。とりあえず正規表現で何とかなりそうな気もする。とりあえず正規表現。正規表現ならきっと何とかしてくれる。というわけで、VBScript.RegExp なわけだけども、これ、生で使うの、すげーだるい。VBA のオブジェクト全般に言えることだが、コンストラクターが引数を持たないために、いったん New してからいちいちプロパティにチマチマ設定していかなければならず、かったるい、かっこわるい。じゃあどうするかというと、RegExp オブジェクトをさくっと生成するための CreateRegExp 関数をまず書く。
PrivateFunction WithIncrIf( _
ByVal expr AsVariant, ByVal incif AsVariant, ByRef cntr AsLong _
) AsVariant
If expr = incif Then cntr = cntr + 1
WithIncrIf = expr
EndFunction
PublicFunction CreateRegExp( _
ByVal ptrnFind AsString, OptionalByVal regexpOption AsString = "" _
) AsObject
Dim cnt AsLong: cnt = 0
Set CreateRegExp = CreateObject("VBScript.RegExp")
CreateRegExp.Pattern = ptrnFind
CreateRegExp.Global = WithIncrIf(InStr(regexpOption, "g") > 0, True, cnt)
CreateRegExp.IgnoreCase = WithIncrIf(InStr(regexpOption, "i") > 0, True, cnt)
If cnt <> Len(regexpOption) Then Err.Raise 5
EndFunction
これでひとまず CreateRegExp("{\d+}", "g") とか書けるようにはなった。だけどまだだるい。まだるっこしい。RegExp オブジェクトの生成はシンプルになったが、こいつにマッチさせて返ってくるのが「Match オブジェクトを含む Matches コレクション」なのだ、煩わしいことに。素直に配列でくれ、文字列の配列でくれ。別にいらんし、FirstIndex とか、Length とか、ほとんどの場合は。なわけで、さくっとマッチするための RegExpGMatchs 関数を書く。
PublicFunction RegExpGMatchs( _
ByVal expr AsString, ByVal ptrnFind AsString, _
OptionalByVal iCase AsBoolean = False _
) AsVariant
Dim ret AsVariant: ret = Array()
Dim regex AsObject: Set regex = CreateRegExp(ptrnFind, IIf(iCase, "i", "") & "g")
Dim ms AsObject: Set ms = regex.Execute(expr)
If ms.Count < 1 ThenGoTo Ending
ReDim ret(ms.Count - 1)
Dim arr AsVariant: ReDim arr(ms(0).SubMatches.Count)
Dim i AsInteger, j AsInteger
For i = 0 To UBound(ret)
ret(i) = arr
ret(i)(0) = ms.Item(i).Value
For j = 1 To UBound(arr): ret(i)(j) = ms(i).SubMatches.Item(j - 1): Next
Next
Ending:
RegExpGMatchs = ret
EndFunction
これで RegExpGMatchs("{0} {1} {2}", "{\d+}") とかして (("{0}"), ("{1}"), ("{2}")) てな配列を得られるようになった。RegExpGMatchs("{0} {1} {2}", "{(\d+)}") なら (("{0}", "0"), ("{1}", "1"), ("{2}", "2")) だ。
なんだっけ?
なんのためにこんな、RegExp のラッパーのような、ユーティリティ関数書いてんだっけ? ああ、そう、書式指定文字列。{インデックス番号:書式指定子} の形式で書式文字列を書きたいんだった。そんでそう、正規表現で {インデックス番号:書式指定子} な箇所見つけて Replace かければいいんじゃん、てな目論見なんだった。そうそう。それ。
じわじわじっそう
とはいえ、はじめっから {インデックス番号:書式指定子} のような文字列にマッチする正規表現を考えるのは難しい。だけならまだしも、プレースホルダーをあらわす { } のエスケープ問題について最初から考慮するのは骨が折れる。そんなの後でいい。まずは簡単に。まずは動くものを。なにげにすでに文中で例示しているが、エスケープ非対応な {インデックス番号} 形式の書式指定文字列をサポートするような Formats 関数を書いてみよう。
PublicFunction Formats(ByVal strTemplate AsString, ParamArray vals() AsVariant) AsString
Dim ms AsVariant: ms = RegExpGMatchs(strTemplate, "{(\d+)}")
Formats = strTemplate
Dim m AsVariant
ForEach m In ms
Formats = Replace(Formats, m(0), vals(m(1)))
Next
EndFunction
' [Immediate]
' ?Formats("{0} {1} {2}", "Foo", "Bar", "Baz")
' Foo Bar Baz
いけそうな気がする。てか、イケるでしょ、これ。うん、ここから育てる。エスケープ対応はまだ後回しにして、次は {インデックス番号:書式指定子} 形式の書式指定文字列をサポートしてみよう。
PublicFunction Formats(ByVal strTemplate AsString, ParamArray vals() AsVariant) AsString
Dim ms AsVariant: ms = RegExpGMatchs(strTemplate, "{(\d+)(:(.*?))?}")
Formats = strTemplate
Dim m AsVariant
ForEach m In ms
Formats = Replace(Formats, m(0), Format(vals(m(1)), m(3)))
Next
EndFunction
' [Immediate]
' ?Formats("{0:yyyy/mm/dd}: {1}", Now, "What's going on?")
' 2012/04/10: What's going on?
やほーい。んじゃあとはエスケープに対応するだけ。それぞれ2つ重ねて、{{ とすれば { に、}} とすれば } になるようする。Perl のような先読みができないからって泣かない。あきらめないぜ、ここまできたら。
PublicFunction Formats(ByVal strTemplate AsString, ParamArray vals() AsVariant) AsString
Dim ms AsVariant: ms = RegExpGMatchs(strTemplate, "[^{]?({(\d)(:(.*?[^}]?))?})")
Formats = Replace(Replace(strTemplate, "{{", "{"), "}}", "}")
Dim m AsVariant
ForEach m In ms
Formats = Replace(Formats, m(1), Format(vals(m(2)), m(4)))
Next
EndFunction
' [Immediate]
' ?Formats("{0:000} {{{1:yyyy/mm/dd}}} {2}", 1, Now, "Simple is best.")
' 001 {2012/04/10} Simple is best.
できたっぽい。
おわりに
VBA が貧弱なことに変わりはありませんが、まあマクロ言語ですし、それにしては割に融通が利く方だと思います。良きにつけ悪しきにつけ(白目