Javaの文字列置換で、関数を使って動的に置換文字列を作成する

JavaScriptのreplace関数は

// 各数字を2倍に置換する
"12345".replace(/\d/g, function(str) {
  return parseInt(str, 10) * 2;
});
// => "246810"

のように、関数を使って置換文字列を作ることができるのがうれしい。これをJavaでもできないだろうか。

まず、Matcherクラス に用意されている機能を使った簡単な実装から。

package example;

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

public final class RegexUtils {
    public static interface Replacement {
        String replace(MatchResult result);
    }

    public static String replaceAll(
            CharSequence cseq, Pattern pattern, Replacement replacement) {
        Matcher matcher = pattern.matcher(cseq);
        StringBuffer sb = new StringBuffer();

        while (matcher.find()) {
            String replaced = replacement.replace(matcher.toMatchResult());
            matcher.appendReplacement(sb, Matcher.quoteReplacement(replaced));
        }
        matcher.appendTail(sb);

        return sb.toString();
    }

    private RegexUtils() {}
}

使用方法

RegexUtils.replaceAll("12345", Pattern.compile("\\d"),
                new RegexUtils.Replacement() {
                    @Override
                    public String replace(MatchResult result) {
                        int digit = Integer.valueOf(result.group());
                        return String.valueOf(digit * 2);
                    }
                });

// => "246810"

appendReplacement()/appendTail() は、まさにこういった「置換文字列をその場で作成する」ためにある機能のようだ。

ただ、いまどき StringBuffer は無いだろうというのと、appendReplacement() には $1 をキャプチャした文字列に置換するような余計な機能がついておりそれを打ち消すために quoteReplacement() するとか、なんか無駄な感じなので、やっぱり完全に自前で実装してみる。あとついでに、一度もマッチしない場合には余計なオブジェクトを生成しないようにもしてみる*1

// 改良版
public static String replaceAll(
        CharSequence cseq, Pattern pattern, Replacement replacement) {
    Matcher matcher = pattern.matcher(cseq);

    if (matcher.find()) {
        StringBuilder sb = new StringBuilder();

        int previousEnd = 0;
        do {
            sb.append(cseq.subSequence(previousEnd, matcher.start()));
            sb.append(replacement.replace(matcher.toMatchResult()));
            previousEnd = matcher.end();
        } while (matcher.find());
        sb.append(cseq.subSequence(previousEnd, cseq.length()));

        return sb.toString();
    }

    return cseq.toString();
}

*1:Matcher#replaceAll の実装と似たような感じ