正規表現による検索置換を使っていて突き当たる問題として、ある条件の時のみ検索置換を行いたい、ということがある。
Kodomo-Ginkou-Ken-Japanese-Toy-Money
たとえばIT用語で、カタカナ列の末尾にある音引き(ー)を取りたいとする。
(ちなみにぼくは、以前書いたが、IT用語の末尾の音引き削除は意味のない習慣で、やめるべきだと思っている。最近はマイクロソフトをはじめ音引きを付ける企業も多い)

 (置換前)コンピューターが使えて便利だ
 (置換後)コンピュータが使えて便利だ

単純に「ー」を全削除すると

 (置換後)コンピュタが使えて便利だ

となってしまう。

そこで、カタカナ以外の前にある音引きを削除する、と考える。
しかし、カタカナ以外の1文字は[^ァ-ン]だが、

 s/ー[^ァ-ン]//g;

のようにすると、

 (置換後)コンピュータ使えて便利だ

という、ちょっとカタコトな感じになってしまう。そこで新しい技術を導入する。

 s/ー(?=[^ァ-ン])//g;

ここで(?<!PATTERN)は、ルックアラウンドアサーション(look arround assertion)の一種で、肯定の先読み(positive lookahead)というものだ。
これだと正しく

 (置換後)コンピュータが使えて便利だ

となるはずだ。

説明だけ書いていると不安になるのでプログラムを書いてみよう。
#! /usr/local/bin/perl
#
# regTestLookarround.pl -- 正規表現のテスト

use 5.010;
use strict;
use warnings;
use utf8;
binmode STDOUT, ":encoding(UTF-8)";

while () {
 s/ー(?=[^ァ-ン])//g;
 say;
}

__DATA__
コンピューターが使えて便利だ
実行。

$ ./regTestLookarround.pl
コンピュータが使えて便利だ

確かに!
では説明する。

(?=パターン)は、パターンがこの先登場する場所にマッチする。

「^」が行頭にマッチしたり、「$」が行末にマッチしたりするのと同様に、上の例だとひらがなの「が」の直前にマッチする。

これはもともとPerlが拡張した正規表現だが、いまはほとんどの正規表現処理系で使われている。
ルックアラウンドアサーションをまとめると、

 (?=パターン)  肯定の先読み  この先にパターンが出てくる場所にマッチ
 (?!パターン)  否定の先読み  この先にパターンが出てこない場所にマッチ
 (?<=パターン) 肯定の後読み  この前にパターンが出てきた場所にマッチ
 (?<!パターン) 否定の後読み  この前にパターンが出てこなかった場所にマッチ

となる。

後ろに戻る時だけ<を使う。
否定は!だ。
上の例では肯定の先読みを使って「カタカナ以外の字がこの先出てくる場所にマッチ」と書いたが、否定の先読みを使って

s/ー(?![ァ-ン])//g;

と書いて、「カタカナがこの先出てこない場所にマッチ」としても良い。

さて、なぜここで急にルックアラウンドアサーションなどというフツーのPerlの知識を縷々説明したかというと、すごく面白い例を見たからである。

書いたのは神戸の森卓司さんという人で、新聞の組版をやっている超なんでも知っているすごい人だ。
拙著「すぐわかるPerl」がお目に止まってメールを下さってからのお付き合いなので、もう15年になんなんとしているが、いまだにリアルで会ってない。
ネットも長くなるとこういう付き合いの人が世界各地にポツポツ増えて面白い。

Sus scrofa liaodongensis: 漢数字を無思慮に洋数字に変える

新聞は縦書きなので、基本数字は漢数字である。
しかしネットにアゲるときは横書きになるので、漢字は洋数字(インド数字)にしたい。
そこで、漢数字を洋数字に変えるスクリプトである。
(テスト実行用に変更しました)
#! /usr/local/bin/perl
#
# regTestKanYou.pl -- 漢数字を洋数字に変える

use strict;
use warnings;
use utf8;
binmode STDOUT, ":encoding(UTF-8)";

while(<DATA>) {
 # 単位語の前に数字がない場合「一」を補う
 s/(?<![一二三四五六七八九千百十])(?=[兆億万])/一/g;
 s/(?<![一二三四五六七八九])(?=[千百十])/一/g;
 # 欠けている桁をゼロとして復活させる
 s/(?<=兆)(?![一二三四五六七八九][十百千万]?億)/〇億/g;
 s/(?<=億)(?![一二三四五六七八九][十百千]?万)/〇万/g;
 s/(?<=[兆億万])(?![一二三四五六七八九]千)/〇千/g;
 s/(?<=千)(?![一二三四五六七八九]百)/〇百/g;
 s/(?<=百)(?![一二三四五六七八九]十)/〇十/g;
 s/(?<=十)(?![〇一二三四五六七八九])/〇/g;
 # 単位語のうち千百十を消す
 s/[千百十]//g;
 # 億万でゼロを整理
 s/〇〇〇〇[億万]//g;
 s/(?<=[兆億万])〇+//g;
 # 洋数字に変換
 y/〇一二三四五六七八九/0123456789/;
 # 3桁毎にコンマを挿入
 s/(?<=\d)(?=(\d\d\d)+(?!\d))/,/g;
 print $_;
}

__DATA__
あのう、千円ください
今度は五千円ください
あのダムを作るのに五百億円は掛かっている
ウルトラマンに「三億五千年前の化石だ!」というセリフがあったが、絶対「三億五千万年」の間違いだと思う
実行してみる。
$ regTestKanYou.pl
あのう、1,000円ください
今度は5,000円ください
あのダムを作るのに500億円は掛かっている
ウルトラマンに「3億5,000年前の化石だ!」というセリフがあったが、絶対「3億5,000万年」の間違いだと思う
あははは出来てるわ。
「億」「兆」はプラクティカルに妥協して漢数字を残しているが、他の漢数字は洋数字に変換して3桁毎にカンマ編集している。

これは徹頭徹尾ルックアラウンドアサーションを使った検索置換である。
 s/(?<![一二三四五六七八九])(?=[千百十])/一/g;
という置換演算子に
 あのう、千円ください
という文字列を作用させると
 あのう、一千円ください
になる。

[一二三四五六七八九]は一から九の漢数字である。
(Unicodeに漢数字が出現する順番がバラバラなので[一-九]とは出来ない。)
これが前に存在しない、かつ、後に[千百十]が後ろに存在する、「空間」を、「一」に置換できる。
だから「、」と「千」の間に一を結果的に挿入している。

次に
 s/(?<=千)(?![一二三四五六七八九]百)/〇百/g;
 s/(?<=百)(?![一二三四五六七八九]十)/〇十/g;
 s/(?<=十)(?![〇一二三四五六七八九])/〇/g;
を連続的に作用させることによって、
 あのう、一〇百円ください
 あのう、一〇〇十円ください
 あのう、一〇〇〇円ください
となる。この〇は「ぜろ」を検索置換すると出てくる、言うところの「漢数字のゼロ」である。

で、
y/〇一二三四五六七八九/0123456789/;

 あのう、1000円ください
となる。y///はtr///と一緒で、translitalation(逐字変換)を行う。
yというのはあらゆるコマンドが英字1文字からなる変態環境sedの名残りである。
s/(?<=\d)(?=(\d\d\d)+(?!\d))/,/g;
がすごくてカンマ編集を行う。
これは、前に数字があって、後ろに数字が3桁あり、かつその後ろに数字が来ない空間にカンマを入れる。
 1234567
の場合は
 1234,567
 1,234,567
となる。

へぇー。
ぼくはこれをループを使ってやっていた。

以上、面白いだけでなく、実際に役に立っているプログラムである。
逆に、洋数字を漢数字に変換するのはこちら。

Sus scrofa liaodongensis: 洋数字を単位語入りの漢数字に変換するぞ

どちらも、百千万は十刻みだが億兆は一万刻みになる、「一万飛んで十」のようなケースがある、一が省略されるケースがあるなどの、「文化」の違いを考慮してプログラミングされている。