LoginSignup
4

More than 5 years have passed since last update.

`Regexp#match` では `\A` より `\G` が有用

Last updated at Posted at 2018-10-27

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 #=> ""
  • 案1Regexp#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になっている)メソッド

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4