正規表現間違い探しクイズシリーズです。
正規表現は単体テストを書いている場合でもバグを発見しづらいものです。そのため、自身での検証が欠かせません。 今回は仕事中に見つけたものでなく、個人開発中にネットで見つけた間違いのうち印象的だったものを少し変えて紹介します。
問題編
仕様
- 行頭及び行末の
#
記号を取り除くString#strip_hash
を定義する #
記号は連続している可能性がある#
記号がない場合もある
動作イメージ
###本文
→本文
#大切#
→大切
普通
→普通
ソースコード
class String def strip_hash sub(/^#*([^#]+)#*$/, '\1') end end
正規表現^#*([^#]+)#*$
は、行頭に続き#
の0回以上の繰り返し、続いて#
以外の文字の繰り返し、#
の0回以上の繰り返し、行末、を意味します。subメソッドを使用して、1つ目のグループ([^#]+)
を抜き出しています。
さて、この正規表現には明らかな間違いがあります。どのような間違いでしょうか。また、どのように修正するべきでしょうか。
テスト
以下のテストはパスしています。RSpecで書いています。
describe '#strip_hash' do subject { str.strip_hash } context '#が含まれているとき' do context '行頭に#がある場合' do let(:str) { '###文字列' } it { is_expected.to eq '文字列' } end context '行末に#がある場合' do let(:str) { '文字列###' } it { is_expected.to eq '文字列' } end context '行頭と行末に#がある場合' do let(:str) { '###文字列###' } it { is_expected.to eq '文字列' } end end context '#が含まれていないとき' do let(:str) { '文字列' } it { is_expected.to eq '文字列' } end end
解答編
・
・
・
少し考えてから解答編を見てください
・
・
・
よいですか?
・
・
間違いの部分
入力する文字列の中間に#
が存在する場合に、正規表現にマッチせず、行頭及び行末の#
が削除されません。また、入力に#
だけ含まれる場合もマッチしません。
テストを追加してみましょう。
describe '#strip_hash' do subject { str.strip_hash } context '#が含まれているとき' do # 略 context '行頭と行末と文中に#がある場合' do let(:str) { '###文#字#列###' } it { is_expected.to eq '文#字#列' } end context '#だけの場合' do let(:str) { '###' } it { is_expected.to eq '' } end end end
これを実行すると、以下の理由でテストが失敗します。
# 行頭と行末と文中に#がある場合 expected: "文#字#列" got: "###文#字#列###" # #だけの場合 expected: "" got: "###"
どう直すか
このように書き換えてみました。
class String def strip_hash gsub(/(^#+)|(#+$)/, '') end end
正規表現(^#+)|(#+$)
は、行頭に続く#
の1回以上の繰り返し、または`#``の1回以上の繰り返しの直後に行末にマッチします。gsubはパターンにマッチする部分をすべて置き換えるので、行頭と行末両方を置き換えることができます。
テストも無事通りました。
あとがき
行頭と行末の全角空白と半角空白両方を削除する正規表現を探していて、問題に掲載したような正規表現が、"コレでできるよ"って紹介されていて、信じてしまったのですが、全然うまく動かねえ、と思って書いたのがこの記事です。
参考
元ネタはこちらです。
文中の正規表現可視化ツールはこちらです→Regulex:JavaScript Regular Expression Visualizer