Rubyのリファレンスマニュアルに載っていない正規表現機能が、モヤモヤしていた問題を解決してくれた。
困っていたこと
文字列 str = "1+2*(3*(4+5)+6*78)+9"
の n+1 文字目から始まる数字の列 /\d+/
を抜き出したい。( n+1 文字目が数字でなければマッチ失敗)
n | マッチ |
---|---|
0 | "1" |
15 | "78" |
14 | 失敗 ( "*" だから) |
思いついた方法をいくつか試してみる。
str = "1+2*(3*(4+5)+6*78)+9"
# 案1
regexp1 = /\d+/
regexp1.match(str, 0) #=> #<MatchData "1"> # OK
regexp1.match(str, 15) #=> #<MatchData "78"> # OK
regexp1.match(str, 14) #=> #<MatchData "78"> # NG
# 案2
regexp2 = /\A\d+/
regexp2.match(str, 0) #=> #<MatchData "1"> # OK
regexp2.match(str, 15) #=> nil # NG
regexp2.match(str, 14) #=> nil # OK(に見えるだけであり期待しない動作)
# 案3
regexp3 = regexp2
regexp3.match(str[ 0..-1]) #=> #<MatchData "1"> # OK
regexp3.match(str[15..-1]) #=> #<MatchData "78"> # OK
regexp3.match(str[14..-1]) #=> nil # OK
match_data = regexp3.match(str[15..-1])
match_data.offset(0) #=> [0, 2] # str[(15+0)...(15+2)] == match_data.to_s
match_data.pre_match #=> ""
-
案1:
Regexp#match
は第2引数にマッチ開始位置を指定できる。これを利用すれば文字列の途中から調べられる。- しかし3番目がNGなように、マッチが部分一致のため、マッチ開始位置が数字でないことに気付けない。
-
案2: 正規表現に「文字列先頭」のアンカー
\A
を追加し、部分一致でなく前方一致で動作させる。- これはマッチ開始位置の指定と相性が悪い。文字列途中からマッチ開始したら、
\A
にマッチしないので必ず失敗する。
- これはマッチ開始位置の指定と相性が悪い。文字列途中からマッチ開始したら、
-
案3: しかたないのでマッチ開始位置の指定を諦め、 n+1 文字目以降の文字列を切り出してからマッチを試す。
- 動作自体は完璧。テストケースは全部成功している。
- でも
#match
に用意された機能が使えず、新しい文字列を作らなければいけないというのは、どう考えても効率が悪い。もっとスマートな方法があるのではないか? - 今回の要件とは関係ないが、
MatchData
の内容が元のstr
に対するものでない問題もある。
解決策
Rubyの正規表現エンジンである鬼雲のドキュメントを見ていたら、アンカー(錨)に知らないものがあった。
Onigmo/doc/RE.ja
5. 錨
^ 行頭
$ 行末
\b 単語境界
\B 非単語境界
\A 文字列先頭
\Z 文字列末尾、または文字列末尾の改行の直前
\z 文字列末尾
\G 照合開始位置
照合開始位置 \G
。名前からして今回の用途に向いていそうな雰囲気がある。
というわけで案2の \A
をこれに変えて試してみる。
str = "1+2*(3*(4+5)+6*78)+9"
# 案4
regexp4 = /\G\d+/
regexp4.match(str, 0) #=> #<MatchData "1"> # OK
regexp4.match(str, 15) #=> #<MatchData "78"> # OK
regexp4.match(str, 14) #=> nil # OK
match_data = regexp4.match(str, 15)
match_data.offset(0) #=> [15, 17] # str[15...17] == match_data.to_s
match_data.pre_match #=> "1+2*(3*(4+5)+6*"
全てのテストケースで正しく動作した。
文字列を切り出すなんてことはせず #match
の機能で済んでいるので、案3の不満点を解消できた(最善かはわからないけど)。 MatchData
の内容は元の文字列 str
に対する情報になっているのも嬉しい。
宿題
正規表現によるマッチを利用する他のメソッドでも、 \A
より \G
のほうがいいだろうか?
Regexp#match?
String#index
String#rindex
- 開始位置を指定できない(暗に0になっている)メソッド