日々常々

ふつうのプログラマがあたりまえにしたいこと。

Groovyの文字列検索メソッド(eachMatch)の結果取得

GWだからGroovyの勉強してます!
本音は 第6回「Groovyイン・アクション読書会」IN 関西 に参加申込してしまったので慌てて勉強してるだけだったりします。

発端

Groovyイン・アクションの写経をしていたら assert 通らないのがあったんです。

myFairStringy = 'The rain in Spain stays mainly in the plain!'

BOUNDS= /\b/
rhyme = /$BOUNDS\w*ain$BOUNDS/
found = ''

myFairStringy.eachMatch(rhyme) { match ->
    found += match[0] + ' '
}
assert found == 'rain Spain plain '

実行するとこうなりました。

Exception thrown
2011/05/05 20:41:33 org.codehaus.groovy.runtime.StackTraceUtils sanitize

警告: Sanitizing stacktrace:

Assertion failed: 

assert found == 'rain Spain plain '
       |     |
       |     false
       r S p 

match が String になっているため、[0]で1文字目が引っ張り出されています。これは63ページに書かれている挙動なんですが、ここでやりたいのはそれではありません。
とりあえず写経した際に間違えたわけではありませんでした。これに関しては Manning: Groovy in Action からダウンロード出来る Listing_3_6_Each_Match.groovy を実行しても同じ結果なので間違いないでしょう。


目的の挙動

単純に本来の目的の挙動をさせるなら、掲載されているコードでは match がリスト(ArrayList)にならないので、カッコを rhyme の適当な場所に入れます。ここでは面倒なので全体に。

myFairStringy = 'The rain in Spain stays mainly in the plain!'

BOUNDS= /\b/
rhyme = /($BOUNDS\w*ain$BOUNDS)/
found = ''

myFairStringy.eachMatch(rhyme) { match ->
    assert match instanceof ArrayList
    found += match[0] + ' '
}
assert found == 'rain Spain plain '

また、 rhyme はそのままにして match[0] を match にする解決もあります。正規表現にカッコが含まれていない場合はStringになるみたいです。

myFairStringy = 'The rain in Spain stays mainly in the plain!'

BOUNDS= /\b/
rhyme = /$BOUNDS\w*ain$BOUNDS/
found = ''

myFairStringy.eachMatch(rhyme) { match ->
    assert match instanceof String
    found += match + ' '
}
assert found == 'rain Spain plain '

こういう場合は誤植を疑いたくなるのですが、ダウンロード出来るコードもこのとおりであることと、30ページのこの記述からコードが失敗するなら出力されない事になるので、誤っていない可能性が高いです。

本書は、実際にアサーションを活用してかかれました。(略)もしアサーションが失敗すればエラーメッセージが出力され、このプロセスは停止します。

と、言う事は――

バージョンの違い

Groovyイン・アクションは付録のインストールを見る限り Groovy1.5.4 のようです*1が、当方は Groovy1.8.0 です。これだけの差があると何が違っても不思議じゃないですよね。
と言う事で手っ取り早く取得できる 1.5.x の最新版である 1.5.8 をダウンロードして実行……すんなり通りました。どうやらどこかのタイミングで変わったようです*2

結果

結果だけ見ればバージョンによる挙動の違いで片付けられるんですが、正規表現を使ってると特に後で使う目的じゃなくカッコを使ったりすることもある*3ので、カッコ有無で取得方法を変えなきゃいけないのはなんか微妙な気もしたり。

groovy:000> 'abc'.eachMatch(/./){println it}
a
b
c
===> abc
groovy:000> 'abc'.eachMatch(/(.)/){println it}
[a, a]
[b, b]
[c, c]
===> abc

マッチ結果の取得

カッコをつけた場合、クロージャの引数の数で取得の仕方が変わるみたいです。引数が1つの場合はリストになるので、インデックスアクセスになります。0番目がマッチ全体、1番目以降はカッコの順番。

groovy:000> 'abc de fgh i'.eachMatch(/\b(\w)\w+\b/){a -> println a}
[abc, a]
[de, d]
[fgh, f]
===> abc de fgh i
groovy:000> 'abc de fgh i'.eachMatch(/\b(\w)\w+\b/){println it[1]}
a
d
f
===> abc de fgh i

引数が2つ以上になると、リストの1番目からがそのまま入るみたいです。

groovy:000> 'abc de fgh i'.eachMatch(/\b(\w)\w+\b/){a, b -> println 'a=' + a + ', b=' + b}
a=abc, b=a
a=de, b=d
a=fgh, b=f
===> abc de fgh i

面白いのが、カッコの数(+1)と引数の数が一致していない場合エラーになったところ。いい事なのやら悪い事なのやら。

groovy:000> 'abc de fgh i'.eachMatch(/\b(\w)(\w+)\b/){a,b -> println a}
ERROR groovy.lang.MissingMethodException:
No signature of method: groovysh_evaluate$_run_closure1.doCall() is applicable for argument types: (java.util.ArrayList)
 values: [[abc, a, bc]]
Possible solutions: doCall(java.lang.Object, java.lang.Object), isCase(java.lang.Object), isCase(java.lang.Object)
        at groovysh_evaluate.run (groovysh_evaluate:2)
        ...
groovy:000> 'abc de fgh i'.eachMatch(/\b(\w)(\w+)\b/){a,b,c -> println a}
abc
de
fgh
===> abc de fgh i

参考

GroovyなJDK、それがGDK(String編その3) - No Programming, No Life
String のメソッドいっぱいですねー。 eachMatch と似たのに findAll ってのがあるみたいです。単独処理としてマッチしたのを Closure でごにょごにょ出来るのは同じっぽいのですが、 findAll だと List が返ってくるのが差なんでしょうかね?

groovy:000> 'abc'.eachMatch(/./){}
===> abc
groovy:000> 'abc'.findAll(/./){it+'z'-'a'}
===> [z, bz, cz]

*1:他にバージョン書かれている場所が脚注くらいしか見当たらなかったんです。

*2:1.7.6は1.8.0と同じ挙動、つまりエラーになりました。

*3:orとか繰り返しとか。