JavaScriptの動かないコード (中級編) 正規表現で同じ文字の連続を検出したい - 置換前パターン中での後方参照
以下のJavaScriptコードが意図した動作をしないのは,なぜですか。(制限時間1分)
やりたい事:
- 同じアルファベットの連続した並びを,それぞれ1文字にまとめる。
例:
- "good apple" → "god aple"
<script language="JavaScript"> function matomeru( str ) { // 正規表現を使って置換する。 str = str.replace( // 同一文字の 2 回以上の繰り返しを検出して, // ( ) 内にそのアルファベットを保管する。 /([a-zA-Z]){2,}/g, // ( ) 内に入っている1文字のアルファベットにする "$1" ); return str; } // "god aple" と表示。 alert( matomeru( "good apple" ) ); </script>
発生する不具合
"god aple" ではなく, "d e" と表示されてしまう。
やりたいことが実現できず困る
正規表現を学びたての場合,以下の事は簡単だ。
- (1) 任意の文字の連続した並びを検出する。 → /.+/ で "abc123" にマッチ
- (2) ある特定の一つの文字の連続した並びを検出する。 → /a+/ で "aaa" にマッチ
- (3) ある特定の文字グループの並びを検出する。 → /[a-z]+/ で "abc" にマッチ
では,
- ある特定の文字グループの同じ文字が連続した並びを検出する。
のは,どうやるのか?
// a の2回以上の並びか,もしくは // b の2回以上の並びか,もしくは // c の2回以上の並びか,もしくは // d の2回以上の並びか,もしくは // ・・・・・ // にマッチ。 /(a{2,}|b{2,}|c{2,}|d{2,}|e{2,} ・・・・・ )/g
これは冗長すぎる。
かわりに,冒頭のコードのように, [a-z] でアルファベット全体を表現すると
// 【アルファベットの】2回以上の並びにマッチ。 /([a-z]){2,}/g
となり,"apple" は「アルファベットが5つ並んでいる!」とみなされ,なんと単語全体で1文字に凝縮されてしまう。
やりたい事は,あくまで同じ文字の重複した並び(ここでは,"pp" だけ)なのだが。
そこで後方参照
ここで,後方参照の出番である。
後方参照とは,
- 置換前の文字列中では,「\1」「\2」など,バックスラッシュに続けて数字を書く
- 置換後の文字列中では,「$1」「$2」など,ドル記号に続けて数字を書く
事によって利用するもので,
これを使うと,かっこでくくられた評価済みの文字列を表す。
「評価済みの」というのがポイントである。
([a-z]){2}
のようなグループ記法は,あくまで
[a-z][a-z]
というような事をやっているに過ぎない。
[a-z] という文字クラス中のどの文字にマッチするのか?という評価は,[a-z] が現れるたびに,その都度行なわれる。
なので,直前で評価された結果の文字列を保管できない。
ところが,これを後方参照を使って修正すると
([a-z])\1
となる。 \1 は,1つ目の括弧の中身の評価済み文字列であるから,上記の表現は aa, bb, cc, ... などにマッチする。
2文字を超える並びを検出したいのであれば,+ を使って,\1 が1回以上繰り返されると指定すればよい。
修正案
正しいコードは下記のようになる。
<script language="JavaScript"> function matomeru( str ) { str = str.replace( // 同一文字の 2 回以上の繰り返しを検出して, // ( ) 内にそのアルファベットを保管する。 /([a-zA-Z])\1+/g, // ( ) 内に入っている1文字のアルファベットにする "$1" ); return str; } // "god aple" と表示。 alert( matomeru( "good apple" ) ); </script>
知らないと実装できない
さて,上記の事を達成したいとして,やり方を探したい人は,Googleでどのように検索したらよいのか?
「正規表現 同じ文字 連続 並び クラス内」など必死に検索しても,出てくるサイトの記述は,上で挙げた
- 1,任意の文字の連続した並びを検出する方法(/.+/)
- 2,ある特定の一つの文字の連続した並びを検出する方法(/a+/)
- 3,ある特定の文字グループの並びを検出する方法(/[a-z]+/)
のような物だけかもしれない。
このような困難を回避するためには,後方参照という方法の存在を事前に知っているしかない。
しかし,後方参照について述べているサイトのほとんどは $1 による「置換後のパターン中での後方参照」について述べているのみであり,\1 を使った「置換前のパターン中での後方参照」について述べた資料は非常に少ない。
数少ない言及例として,下記サイトなどを参照。
JavaScript の正規表現の書式
http://homepage2.nifty.com/magicant/s...\1、\2、\3…
\1 や \2 はそれぞれ 1 番目、2 番目の捕捉括弧の内容を表す。例えば、 (a+)(b+)c\2\1 というパターンは abbcbba や aaabcbaaa にマッチする。
情報の需要はあるはずなのだが。
やり方がわからない場合は,文字列を for 文で頭から 1 文字ずつ検査し,前の文字と次の文字が同じかどうか判定していく,などの代替手段があるのだろう。