gsubの謎
Codewarsでよく分からない回答があったので、自分なりに調べてみた。
問題はカード番号のマスキングのように下4桁以外の文字なり数字を#でマスキングするという物で、 自分は以下のような回答で提出した(色々褒められた物ではないが。。。)
def maskify(cc) cc.to_s[0, cc.length-4] = "#"*(cc.length-4) if cc.length > 4 cc end
実際に他の人の回答を見てみると、以下の回答に支持が集まっていた。
def maskify(cc) cc.gsub(/.(?=....)/, '#') end
確かにシンプルですごいと思ったのだが、
どうしてこれで条件を満たすのかパット見分からなかったので調べてみた。
調査
まずgusbだが、 これは正規表現で一致した文字列を第二引数の文字に変換した文字列を返すという物である。
問題は正規表現の方だが、
.
は任意の1文字の文字列、?=
は肯定先読みという物で、正規表現の一致した部分の前の部分を一致として返す物になる。
この説明だとよく分からないので、肯定先読みの具体例を出すと、hogehの場合、ogehが(?=....)
と一致するので、hが一致した部分として返される事になる。
hogehogehogeの場合はhogeが(?=....)
と一致するのでhogehogeの最後のeが一致した部分として返される。
ちなみにhogeの場合は(?=....)
と一致するが、直前の文字はないのでrubyでは空が返される。
.(?=…)と.*(?=…)の違い
今回は一致した文字の前の文字列も欲しいので、.*(?=....)
という風にするのが、一致させるという意味では正しいと自分は思っていた。
しかし、gsub(/.*(?=....)/,'#')
だとhogehogehogeの場合、hogehogeが一致している事になり、これが#
と置換されてしまう。
本来であればhogehogeを########
と置換して欲しいので、このやり方は上手く行かない。
対して、gsub(/.(?=...)/,'#')
の場合、hogehogehogeは
1回目:hがogehと一致する為一致
2回目:oがgehoと一致する為一致
3回目:gがehogと一致する為一致
・・・
8回目:eがhogeと一致する為一致
9回目:hはその後ろに任意の4文字がないので一致しない
という風に動き、1〜8回目それぞれのタイミングで文字が#と置換される事が実際に試して見て分かった。
つまりgsubは一致した項目を最後まで次々と処理して第二引数の物に置き換えて行くことになり、今回の処理が上手く動くことになるようだ。
ここまで分かって、あの問題を解く人達は素直にすごいと思った。