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
で十分かもしれませんね。あったら便利とは思うのですけど。