Time to Read

7~8分

先読み

例えば、「1000の累乗」の数値表現(たとえば 1000, 1000000, 1000000000)にマッチする正規表現を考える。素直に考えれば、

1
/1(?:000)+/

となると思うが、これだと、以下のような文字列でもマッチしてしまう。

1
2
"hogehoge10000000hogehoge".scan(/(1(?:000)+/)
=> ["1000000"]

「10000000=10,000,000」は当然1000の累乗ではないが、後ろのゼロ1つを除いた形でマッチしてしまうということだ。

そこで、先読みが登場する。

(思った以上に長々しくなったので以下を畳みます。。)

1
2
3
4
5
6
7
POWER_OF_1000 = /1(?=(?:000)+(?:[^\d]|$))0+/
"hogehoge10000000hogehoge".scan(POWER_OF_1000)
#=> []
"hogehoge1000000000hogehoge".scan(POWER_OF_1000)
#=> ["1000000000"]
"hogehoge1000000000".scan(POWER_OF_1000)
#=> ["1000000000"]

これは、「『後ろにちょうど3の倍数個の”0″が続いて、その後で数字以外か文字列終端が続いているような”1″』で始まり、”0″が1以上続く」文字列にマッチする正規表現である。

つまり、まず “1″ にマッチし(*1)、その後の先読みにより、「後ろにちょうど3の倍数個の”0″が続いて、その後で数字以外か文字列終端が続いているかどうか」ということ自体にマッチする(*2)、ということだ。
なので、「1 / 000 000 0 / hogehoge」のような場合は、(*2)にマッチしないので全体としてマッチしようとしない。

で、ちなみにこの正規表現だと、 “11000” のような場合にも後ろの “1000″ が引っかかるので、「数値先頭の一つ前が文頭か数値以外」という条件も追加でマッチさせたい。

だが、以下のようにしてはならない。

1
2
3
POWER_OF_1000 = /(?=(?:[^\d]|^))1(?=(?:000)+(?:[^\d]|$))0+/
"hogehoge1000000hogehoge".scan(POWER_OF_1000)
#=> []

こういう表現だと、まず「数値以外か先頭であること」にマッチした上で、マッチの位置は変わらずに「”1″」にマッチしようとするので、当然先に「数値以外」にマッチしているから失敗する。あくまでも「先読み」なのである。

#頭に置く先読みは、「re1かつre2」ということを表現したい場合に有効かな? と思う。「4文字以上続く文字列の先頭の”g”」みたいな。

1
2
3
"fugafuga".gsub(/(?=\w{4})(g)/, '<\1>')
#=> "fu<g>afuga"
# 確かに、2番目の "g" にはマッチしていない

戻り読み

話は戻って、この場合、 /([^\d]|^)(1(?=(?:000)+(?:[^\d]|$))0+)/ のようにした上で $2 を参照するか、鬼車を入れて戻り読みを利用するしかない。JRuby/Ruby 1.9.xでは鬼車は組み込まれているが、Ruby 1.8.xでgemとして利用するには以下のようにする。

1
2
sudo aptitude install libonig-dev # on Debian/Ubuntu
sudo gem install oniguruma
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if RUBY_VERSION > "1.9"
  # 正規表現リテラルを使うと、1.8.xでコンパイルエラーになる…
  POWER_OF_1000 = Regexp.compile '(?<=(?:[^\d]|^))1(?=(?:000)+(?:[^\d]|$))0+'
else
  require 'oniguruma'
  POWER_OF_1000 = Oniguruma::ORegexp.new '(?<=(?:[^\d]|^))1(?=(?:000)+(?:[^\d]|$))0+'
  # String#scanは引数に組み込みのRegexpを期待するため、調整
  class String
    alias scan_orig scan
    def scan(re)
      case re
      when Oniguruma::ORegexp
        (re.scan(self) || []).map(&:to_s)
      else
        scan_orig(re)
      end
    end
  end
end
 
"hogehoge11000000hogehoge".scan(POWER_OF_1000)
#=> []
"hogehoge1000000hogehoge".scan(POWER_OF_1000)
#=> ["1000000"]

否定先読み

「~していない」という状態にマッチする。たとえば、「.asp」で終わらないURLにマッチさせる(ホスト名マッチの正規表現は適当ですよ……)。

1
2
3
4
5
RE_NOT_ASP = %r{^https?://\w[-\w]*(?:\.\w[-\w]*)*/?(?!.*\.asp).*$}
"http://example.jp/hoge/fuga.rb".scan(RE_NOT_ASP)
#=> ["http://example.jp/hoge/fuga.rb"]
"http://example.jp/hoge/fuga.asp".scan(RE_NOT_ASP)
#=> []

この場合、とりあえず http://host.name にマッチ(^https?://\w[-\w]*(?:\.\w[-\w]*)*/?)し、その上で「最後が .asp で終わっていないこと」にマッチ((?!.*\.asp)し、文末まで全部にマッチ(.*)する。

以上のまとめとして、Rubyリファレンスマニュアルにある例。

1
2
"1234567890".gsub(/(\d)(?=(?:\d\d\d)+(?!\d))/, '\1,')
#=> "1,234,567,890"

scanすると、

1
2
"1234567890".scan(/(\d)(?=(?:\d\d\d)+(?!\d))/)
#=> [["1"], ["4"], ["7"]]

「後ろにちょうど3の倍数個数字が並んでいる位置の数字」にマッチするので、それを '\1,' で置き換えれば数字三つ区切りになるという仕掛け、というわけ。

「ちょうど」であることを表現するのに、否定先読みが使われている(ここで問題、 /(\d)(?=(?:\d\d\d)+(?=[^\d]))/ との違いは何でしょう?)。

エクスキューズ

[!]ここで記述しているTipsは、あくまで私の理解での範囲の内容で、誤解や不正確な表現を含んでいる場合がありますのでご留意ください。