MatchDataを渡してくれるscanが欲しかった話

"string".scan(/regexp/) {}は引数の正規表現にマッチした文字列または文字列の配列をブロック引数に渡しますが、MatchDataを渡してくれるscanがあればなと思うことが時々ありました。

ずっと前に、どこかでそんな話をした時には「良い名前があればね」「そうですね」みたいなところまでで終わってしまった(ような記憶がある)のですが、ふとRegexp#scanはどうかなと思い付いたので実装してみました。

gsubやsubもあっても良いかもしれないと思ったので、それもついでに。

# frozen_string_literal: true

class Regexp
  def scan(string)
    if block_given?
      string.scan(self) { yield Regexp.last_match }
    else
      string.to_enum(:scan, self).map { Regexp.last_match }
    end
  end

  %i[gsub sub].each do |m|
    eval <<~CODE, binding, __FILE__, __LINE__ + 1
      def #{m}(string)
        string.#{m}(self) { yield Regexp.last_match }
      end
    CODE
  end
end

HR = '-' * 30

def t(t)
  print "#{t}\n#{HR}\n"
  yield
  print "#{HR}\n\n"
end

regexp = /(?<a>\S(?<b>\S(?<c>\S)))/
string = "foo\nbar"

t 'String#scan {}' do
  string.scan(regexp) { p _1 }
end

t 'Regexp#scan {}' do
  regexp.scan(string) { p _1 }
end

t 'String#scan' do
  p string.scan(regexp)
end

t 'Regexp#scan' do
  p regexp.scan(string)
end

t 'String#gsub' do
  puts string.gsub(regexp) { "block arg = #{_1.inspect}\n$1, $2, $3 = #{[$1, $2, $3].inspect}\n"  } 
end

t 'Regexp#gsub' do
  puts regexp.gsub(string) { "block arg = #{_1.inspect}\n" }
end
$ ruby -v regexp_scan_gsub_sub.rb
ruby 3.3.5 (2024-09-03 revision ef084cc8f4) [arm64-darwin24]
String#scan {}
------------------------------
["foo", "oo", "o"]
["bar", "ar", "r"]
------------------------------

Regexp#scan {}
------------------------------
#<MatchData "foo" a:"foo" b:"oo" c:"o">
#<MatchData "bar" a:"bar" b:"ar" c:"r">
------------------------------

String#scan
------------------------------
[["foo", "oo", "o"], ["bar", "ar", "r"]]
------------------------------

Regexp#scan
------------------------------
[#<MatchData "foo" a:"foo" b:"oo" c:"o">, #<MatchData "bar" a:"bar" b:"ar" c:"r">]
------------------------------

String#gsub
------------------------------
block arg = "foo"
$1, $2, $3 = ["foo", "oo", "o"]

block arg = "bar"
$1, $2, $3 = ["bar", "ar", "r"]
------------------------------

Regexp#gsub
------------------------------
block arg = #<MatchData "foo" a:"foo" b:"oo" c:"o">

block arg = #<MatchData "bar" a:"bar" b:"ar" c:"r">
------------------------------

実装してみて、軽く動かしてみて思ったのですが、Regexp.last_matchで十分かもしれませんね。あったら便利とは思うのですけど。