Quantcast
Channel: いげ太の日記
Viewing all articles
Browse latest Browse all 26

VBA にも C# のような書式指定文字列がほしい

$
0
0

ここで実装する 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 が貧弱なことに変わりはありませんが、まあマクロ言語ですし、それにしては割に融通が利く方だと思います。良きにつけ悪しきにつけ(白目


Viewing all articles
Browse latest Browse all 26

Trending Articles