正規表現(10) 置換オプション /e

置換演算子 s/// 専用のオプションとして「/e」修飾子があります。この「/e」オプションが Perl正規表現置換をより強力なモノにしている一因ではないでしょうか。
「/e」オプションを付けると、置換後の文字列を「Perl の式として評価して」「評価した結果で置換」します。このオプションは、マッチした文字列をサブルーチンで処理して、返ってきた文字列で置き換える、という処理を行ないたい時に有効です。
例えば、「P」で始まる英単語を uc() 関数で大文字にしたい時は、以下のように書く事ができます。

use strict;
 
my $text = "I am Palmo the Perl student.";
 
$text =~ s/(P\w*)/uc($1)/egi;
print $text;

評価をする為の「/e」オプション、全て置換する為の「/g」オプション、大文字小文字を区別しない為の「/i」オプションを指定しています。
置換後の文字列として「uc($1)」としていますが、「/e」オプションがついているので実行されて、大文字になります。
実行結果はこのようになりました。

I am PALMO the PERL student.

見事に大文字になっていますね。(^_^) 使い方次第で色々できそうです。


また、有名な裏技(?)として「/ee」があるそうです。置換文字列を評価したの返り値を、さらに評価して、その返り値を置換に使うようになります。つまり「二重評価」ですね。
面白い使い道を思いついたので、こんなものを作ってみました。

use strict;
 
sub calculate {
    my $ex = shift;
    1 while ($ex =~ s:(\d+[\*/]\d+):$1:ee);
    1 while ($ex =~ s:(\d+[\+-]\d+):$1:ee);
    return $ex;
}
 
my $ex = "10+5*3-10/2";
print calculate($ex);

なんと四則演算(加減乗除)を文字列の式から計算します。ちゃんと演算子の優先度も考えてくれます(カッコは使えませんけど…)。置換と/eeオプションでこんな事もできちゃいました。(^_^)
実行結果はこの通り。

20

calculate が計算式の文字列から答えを導き出す関数です。ご覧の通り、中身は置換をしているだけです。
1 while (条件式); というのは、条件式が真の間何度も繰り返すための常套句です。文修飾ですね。まだきちんと勉強していませんが、文の後ろに「if」「for」「while」などを付けると、文を修飾する事になります。この場合の文は「1」です。
で、本題の置換なのですが、正規表現の中で「/」(スラッシュ)を使っているので、区切り文字を「:」(コロン)に変えています。
「(\d+[\*/]\d+)」の意味ですが、「\d+」が数字、「[\*/]」が * (掛け算)、/ (割り算)のどちらかの記号にマッチするので、上のコードなら「5*3」と「10/2」にマッチします。そして、式全体をキャプチャして、$1 として使っています。
さて、ここで「/e」ひとつだけだと、「$1」が評価されて "5*3" や "10/2" という文字列に展開されます。でも、そこで置換されるので、結局何もしない事になってしまいます("5*3"にマッチしても、結局"5*3"に置換するので意味がない)。

"5*3" =~ s:(\d+[\*/]\d+):$1:e;  # "5*3" のまま

そこで「/ee」を使うと、「$1」が評価されて返ってきた "5*3" や "10/2" を再評価するので、"5*3"→15 に、"10/2"→5 にそれぞれ置換される、というわけです。
演算子の優先順位を守る為に、まず「*」と「/」から置換し、その後「+」と「-」を置換しています。また、最初は「/g」オプションを使って一気に置換しようと考えたのですが、例えば「5*2*3」という式があると、一度置換して「10*3」になったあと、「10」が検索の対象にならなくなってしまう*1ので、「10*3」のままになってしまいます。

"5*2*3" =~ s:(\d+[\*/]\d+):$1:eeg;  # "10*3"

そこで、while を使って1度でもマッチしたら、最初から検索し直すようにしました。これで常に一番左から計算されるようにしました。(^_^)


カッコへの対応も再帰などを使えばできそうですね。今度やってみます。
本来なら式を解析してから計算を行なうのですが、解析しつつ計算ができてしまう、というのは楽チンな事極まりないです。すごいなぁ。

Yet another way

ところで、さきほどの「Pで始まる英単語を大文字にする」場合は実のところ /e オプションを使わなくても実現可能でした。

use strict;
 
my $text = "I am Palmo the Perl student.";
 
$text =~ s/(P\w*)/\U$1\E/gi;
print $text;

実行結果は以下の通りです。

I am PALMO the PERL student.

エスケープ文字「\U」と「\E」で囲んだ文字は大文字になります。こちらの方法もシンプルでいいですね。(^_^)
また、「\L」と「\E」で囲んだ文字は小文字になりますし、「\u」の次の1文字は大文字になって、「\l」の次の1文字は小文字になります。
例えば文章に含まれる英単語の先頭を大文字にして、残りを小文字にしたい場合は

use strict;
 
my $text = "I am Palmo the Perl student.";
 
$text =~ s/(\w)(\w*)/\u$1\L$2\E/g;
print $text;

のようにすれば……

I Am Palmo The Perl Student.

となります。覚えておくと良い事があるかも。(^_^)

*1:/g オプションの場合、最後にマッチした部分の次から検索が始まるからです。