uehaj's blog

Grな日々 - GroovyとかGrailsとかElmとかRustとかHaskellとかReactとかFregeとかJavaとか -

Groovyの奇妙な正規表現(Groovyの奇妙な演算子(3)改め)

今回は、正規表現の処理にかかわる以下の演算子/記法についてです。

   /.../    ~    =~     ==~     

「/.../」文字列定数の表記。

/.../はGroovyにおける文字列定数の表記法のひとつです。正規表現専用ではないのですが、正規表現が格納された文字列に使うと便利なように、バックスラッシュ文字をエスケープ文字として解釈しません。

 > println "abc\ndef"
 abc
 def
 > println /abc\ndef/
 abc\ndef

なお、$ではじまる式や変数の展開は行います。

 > a = 'xyz'
 > println /abc$a/
 abcxyz
 > println "abc$a"
 abcxyz
 > println 'abc$a'
 abc$a
 > 

まとめると

文字列定数表記法 バックスラッシュ文字 $変数名、${}の展開
"..." エスケープ文字として扱う 行なう
'...' エスケープ文字として扱う 行わない
/.../ エスケープ文字として扱わない 行う

ちなみに、Perlなどのように、/.../iとかはつけて大文字小文字を区別しない、ということはできません(ただの文字列なので)。埋め込みフラグ表現(?i)などをパターンに含めておきましょう。

 > println "AbC" =~ /(?i)abc/
 true

「~」Patternオブジェクト生成演算子

~Aはnew Pattern(A)と等価。java.util.regex.Patternオブジェクトを生成します。case節で指定する場合とgrepに渡す場合以外はあまり使いません。

「A ==~ B」Matcher#matches()実行演算子

A ==~Bは「Pattern.compile(B).matcher(A).matches()」と等価*1です。結果はboolen値となります。
Pattern#matches()は、Java正規表現APIの仕様として、パターンが対象文字列全体にマッチしたときのみ真を返します(パターンが対象文字列に含まれるかどうか、ではない)。

「A =~ B」Matcherオブジェクト生成演算子

A =~ Bは、「Pattern.compile(B).matcher(A)」と等価で、その返り値である、Matcherクラスのインスタンスを返します*2

> ("abc"=~/a../) instanceof java.util.regex.Matcher
true

Matcherオブジェクトがbooleanに変換される場合はMatcher#find()を実行

=~で生成されたかどうかにかかわらず、Matcherオブジェクトは、Groovyにおいて、if文やwhile文の条件式に指定されたり、boolean値に代入されたり、asでbooleanに変換された場合、Matcher#find()が呼び出されその結果が得られます。たとえばif文の条件式に指定されると、

 if ("abc" =~ /a../) {
     ..
 }

次の処理が実行されます。

  RegexSupport.setLastMatcher(matcher); /// (1)
  return matcher.find();

つまりif(A =~ B){..}はif(Pattern.compile(B).matcher(A).find()){...}と((1)の副作用があることを除き)等価です。

ちなみに、Matcher#find()は、Java Regexp APIの仕様として、全体がマッチしたときではなく、対象文字列中にパターンがマッチする部分があれば真を返します。

  • (1)における副作用として、スレッドローカル変数currentMatcher(org.codehaus.groovy.runtime.RegexSupport#currentMatcher)が設定されますが、これはスレッドごとに1つ存在するデフォルトの処理対象のMatcherです(isCaseの処理でも使われている)。
  • これによって、以下のような処理が可能になります。
 if ("abc" =~ /a(.)c/) {
    println Matcher.getLastMatcher().group(1); // bが出力される。
 }
 b

現スレッドのcurrentMatcherはMatcher#getLastMatcher()で取得できるからです。

  • なお、残念なことに、
 while ("abcaxc" =~ /a(.)c/) {
   println Matcher.getLastMatcher().group(1);
 }

と書いても、期待する動作*3、つまり

 Matcher m = "abcaxc" =~ /a(.)c/
 while (m.find()) {
    println m.group(1);
 }

のようには動作しません。なぜかというと、前者においては、"abc" =~ /a(.)b/と等価な「Pattern.compile(B).matcher(A)」が複数回呼ばれ、matcherメソッドの呼出し毎に異なるMatcherインスタンスが返ってきて、それぞれに対して1回目のfind()び出しが実行されてしまうためです。後者のようにMatcher mが共有されないのです。これはちょっと残念な仕様ですね。

そういうときには、以下のようにします。

 > ("abcaxc" =~ /a(.)c/).each {m0,m1-> // m0はマッチ部分全体
     println m1
   }
 b
 x

あるいは

 > "abcaxc".eachMatch(/a(.)c/) {m0, m1 -> // m0はマッチ部分全体
     println m1
  }
 b
 x

ちなみにMatcherはiterator()メソッドを持っているので、

 > for (i in ("abcaxc" =~ /a(.)c/)) {
    println i; // => "abc", "axc"
  }
 abc
 axc

のようにもできますが、部分文字列を取得する方法はありません。

おまけ

_1, _2という名前のプロパティ参照で、 Matcher.getLastMatcher().group(n);を動的によびだすようなメソッドを作るのは読者への課題とします。staticなコンテキストからも呼び出せるようにするのは難しいな・・*4

*1:「Pattern.matches(B, A)」とも等価でもあり、「A.matches(B)」とも等価。

*2:つまり「A==~B」は、「(A=~B).matches()」と等価。

*3:Perlで/gをつけたときの動作。

*4:白状すると、ちょっと調べた限りできませんでした。MOPを学ばねば・・・。