Perlの正規表現のeオプション(eval)をJava(JavaScript)でやってみた

Perl だと正規表現で置換を行うときに、eオプションを付けることで置換後の文字列に式を利用できる。
例えば、「数字を検索して、見つかったらそれを2倍した値で置換する」といったことができる。これを Java でやりたい。もともとは会社の先輩が正規表現のサンプルについて書いていて、eオプションはそのままだとできそうにないなーと話していて、面白そうなので考えてみた。

Perl でやってみた

まずは Perl のeオプションについて調べた。

e 置換に式を利用する

eオプションを使用すると置換の結果に式を使用することができます。
次のサンプルはマッチした数値を2倍するサンプルです。

s/(\d+)/$1 * 2/e;
実践で役立つPerl正規表現 完全解説 - サンプルコードによるPerl入門

すごく便利だ。ちょっと補足して、今回は↓のサンプルを元に考えていく。
"10 20 a"という文字列から数字を検索して、見つかったらそれを2倍した値で置換する。これを実行すると 20 40 a と出力される。

use strict;
use warnings;

my $str = "10  20 a";
$str =~ s/(\d+)/$1 * 2/eg;
print("$str\n");

できる限りのことを Java でやってみた

このeオプションって、結局のところは eval で、もちろん Java に eval なんてない。一応 RegExp 周りの javadoc を読んでみたが、やっぱりeオプション相当のことはできなさそう。
# Java正規表現を扱うのはちょっと面倒だなと改めて感じた。


そんなわけで、 JavaScript で eval を行うことにした。きっと誰もが思いつくと思うけど。できる限りのことを Java でやったら、やっぱりかなり冗長になった。真面目に Java正規表現を扱ったことがあまりなかったので、
Matcher#appendTail とか初めて使った。

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

public class Reg {

	public static void main(String[] args) throws ScriptException {
		String input = "10 20 a";
		String regexp = "(\\d+)";

		Pattern p = Pattern.compile(regexp);
		Matcher m = p.matcher(input);

		ScriptEngineManager manager = new ScriptEngineManager();
		ScriptEngine engine = manager.getEngineByName("js");

		StringBuffer sb = new StringBuffer();
		while (m.find()) {
			// JavaScriptのコード。eval した結果は String で戻す。
			String script = "String(eval(" + m.group() + " * 2));";
			// JavaScriptで eval するコードを、ScriptEngine で eval して実行する。
			String s = (String) engine.eval(script);
			m.appendReplacement(sb, s);
		}
		m.appendTail(sb);
		System.out.println(sb.toString());
	}
}

できた…。

ほとんど JavaScript でやってみた

どうせ JavaScript 使うなら、正規表現の部分から JavaScript でやっちゃえばいい。当初の目的からは離れていくけど。
そもそも JavaScript ならeオプションあるんじゃないかなと思って調べてみると、Firefox にはなくて、Chromeではなんだか動きが怪しかった。
Chromeで試したのはこんなコード。実行結果が "0 0 a" とでてきたり、"40 40 a" となったりする。なんだこれは…。

"10 20 a".replace(/(\d+)/eg,RegExp.$1 * 2);

気をとりなおして考える。Java で動く JavaScriptRhino なので、Firefox と同様にeオプションを使うとエラーになる。
これでは JavaScript で実現できないではないか、といろいろ調べてみると、素晴らしいエントリがあった。

Perlなどの置換系の正規表現ではeというオプション(フラグ)をつけると、置換後の文字列をプログラム・コードとみなしてくれるわけですが、JavaScriptのreplace()の第一引数で指定する正規表現にはeオプションなどというモノはありません。しかし、replace()の第二引数である置換後の文字列にはStringオブジェクトや文字列リテラル以外にも関数を指定することもできるので、事実上eオプション相当のことが実現できます。エミュレートというのは正確ではない気がするけど気にしない。

正規表現のeオプションをJavaScriptでエミュレート - Weblog - hail2u.net

なるほど、なるほど。このあとにサンプルコードがあって、それを参考にさせてもらった。これは Chrome でも Firefox でもちゃんと動いた。

"10 20 a".replace(/(\d+)/g,
  function(num,idx,old){
    return eval('num*2');
  }
)

ここまでくれば、あとはJavaに組み込むだけ。当たり前だけど、import文から java.util.regex 系がいなくなった。

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

public class RegJs {
	public static void main(String[] args) throws ScriptException {
		String input = "10 20 a";

		ScriptEngineManager manager = new ScriptEngineManager();
		ScriptEngine engine = manager.getEngineByName("js");
		
		String script = 
			"var s;"+ 
			"print( s.replace(/(\\d+)/g," +
			"function(num,idx,old){" +
			"  return eval('num*2');" +
			"}) );";
		engine.put("s", input);
		engine.eval(script);
	}
}

できた!!

まとめ

やっぱり正規表現を扱うなら、Perlは便利。調べてないけど RubyPython はどうなんだろう。
Java から JavaScript を扱うのはとても簡単だった。どこが使いどころなのか、いまひとつわからないけど。仕事では…使わないよね。いろいろ考えてみるのは、なかなか楽しかった。