PatternとMatcherを使い、マッチングした内容を加味して置換を行いたい

良くやろうと思いついて書くけど、書くたびにドキュメント見に行くのがつらかったので記事にしておこう。

単純な置換であれば、replaceAllとかで間に合うのですが、
なんちゃってテンプレートエンジンをPatternで作ろうとか思うと、マッチングした内容を加味する必要があります。

例えば、以下のテンプレート文があったとします。

hoge is "${hoge}"
hoge is "${fuga}"


与えるMap値として以下を与えます。

map.put("hoge", "ほげ");
map.put("fuga", "ふが");


実行結果として以下を期待するとします。

hoge is "ほげ"
hoge is "ふが"


こういった場合、replaceAllとかでは出来ません。
Matcher#find と Matcher#appendReplacement と Matcher#appendTail を使ってガリガリ書く必要があります。



(特に意味はないですが、1回置換と全置換の2つ書いてます。意味はないです。)

package regexpreplaceexample;

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

public class RegexpReplace {
    public static void main(String[] args) {
        String exampleString = "" +
                "hoge is \"${hoge}\"\n"+
                "fuga is \"${fuga}\"\n";
        Map<String, String> varmap = new HashMap<String, String>();
        varmap.put("hoge", "ほげ");
        varmap.put("fuga", "ふが");
        
        Pattern pattern = Pattern.compile("\\$\\{([^}]+)\\}");
        
        // 1回だけ置換
        {
            Matcher m = pattern.matcher(exampleString);
            StringBuffer sb = new StringBuffer(exampleString.length());
            if(m.find()){
                m.appendReplacement(sb, varmap.get(m.group(1)));
            }
            m.appendTail(sb);
            System.out.println("---replaceOnce---");
            System.out.println(sb.toString());
        }
        
        // 全て置換
        {
            Matcher m = pattern.matcher(exampleString);
            StringBuffer sb = new StringBuffer(exampleString.length());
            while(m.find()){
                m.appendReplacement(sb, varmap.get(m.group(1)));
            }
            m.appendTail(sb);
            System.out.println("---replaceAll---");
            System.out.println(sb.toString());
        }
    }
}
"---replaceOnce---"
hoge is "ほげ"
fuga is "${fuga}"

"---replaceAll---"
hoge is "ほげ"
fuga is "ふが"

ここまでならまぁ大した記述量でもないし、頑張るかなーとか思うのですが、
この置換の処理って結構需要あると思うんですよね。
"マッチした文字列を使った加工"さえできればよいので、ここを無名クラスで切り出してやります。

public class RegexpReplace {
    // このInterfaceを使って、置換する文字列の処理を無名クラスで書く
    public static interface ReplaceIF {
        String call(MatchResult matchResult);
    }
    
    public static void main(String[] args) {
        String exampleString = "" +
                "hoge is \"${hoge}\"\n"+
                "fuga is \"${fuga}\"\n";
        final Map<String, String> varmap = new HashMap<String, String>();
        varmap.put("hoge", "ほげ");
        varmap.put("fuga", "ふが");
        
        Pattern pattern = Pattern.compile("\\$\\{([^}]+)\\}");
        
        // 1回のみ置き換え
        String replaceOnce = RegexpReplace.once(exampleString, pattern, new ReplaceIF() {
            @Override
            public String call(MatchResult m) {
                return varmap.get(m.group(1));
            }
        });
        
        // 全て置き換え
        String replaceAll = RegexpReplace.all(exampleString, pattern, new ReplaceIF() {
            @Override
            public String call(MatchResult m) {
                return varmap.get(m.group(1));
            }
        });
        
        System.out.println(replaceOnce);
        System.out.println(replaceAll);
    }
    
    public static String once(String str, Pattern pattern, ReplaceIF replace){
        Matcher m = pattern.matcher(str);
        StringBuffer sb = new StringBuffer(str.length());
        if(m.find()){
            m.appendReplacement(sb, replace.call(m.toMatchResult()));
        }
        m.appendTail(sb);
        return sb.toString();
    }
    
    public static String all(String str, Pattern pattern, ReplaceIF replace){
        Matcher m = pattern.matcher(str);
        StringBuffer sb = new StringBuffer(str.length());
        while(m.find()){
            m.appendReplacement(sb, replace.call(m.toMatchResult()));
        }
        m.appendTail(sb);
        return sb.toString();
    }
}

なんか下手糞な感じがしますが、これならMatchResultを覚えれば簡単に書けそうです。(フラグは考慮してないです)

本日初登場のMatchResultにはマッチしたときの状態が入っています。
何がマッチしたか、何文字目から何文字目までがマッチしたのか、n番目のグループの文字は何かなど。


Java8のLambdaを使うと少し幸せに。無名クラス使ってるところをLambda式に置き換えます。

        // 1回のみ置き換え
        String replaceOnce = RegexpReplace.once(exampleString, pattern, (m) -> varmap.get(m.group(1)));
        // 全て置き換え
        String replaceAll = RegexpReplace.all(exampleString, pattern, (m) -> varmap.get(m.group(1)));

Pattern#matcherがLambdaの登場により置換処理をLambdaで書けるようなPatternクラスが拡張された夢を見たのが元ネタだったり。