2018年3月4日日曜日

[Java9] テキスト中の数値をインクリメントしつつ置換する


猫と一緒にガジェットライフ♪ムチャです。

仕事で「テキスト中に埋め込まれた通し番号を一律増やしたい」という状況になりました。
サクッとできなそうだったので、ちょっと調べてJava9で新しく追加されたメソッドを使って解決したので、ご紹介したいと思います。


要件

:
:
obj.setRec123("");
obj.setRec124(foo.getName());
obj.setRec125("固定文字");
if(foo.isBar()){
    obj.setRec126(foo.getParam1());
    obj.setRec127("1");
}else{
    obj.setRec126("");
    obj.setRec127(foo.getParam2());
}
:
:
こんな感じのコードがありました。クソコードですねw

objに詰めたものは最終的にCSVかなんかになるので変数自体にあまり意味はないとはいえ、もし途中に項目が追加になったら大変です。

で、10個ほど追加することになったのですが(;´∀`)
手で直してもいいですが、面倒だし間違えてもエラーになったりせずチェックが面倒です。

正規表現でサクッとできそうな感じもありますが、マッチさせて後方参照した後に計算するのは難しそうです。どうもPerlだとできるぽいのですが、サクラエディタやJavaだとできませんでした。


対応

じゃあどうするかなと。Java9のAPIドキュメント見てたら9でjava.util.regex.Matcherクラスに新しく追加になったメソッドで解決できそうでした。

それが以下です。

public String replaceAll​(Function<MatchResult,String> replacer)
元々Stringを取るreplaceAllはありましたが、java.util.function.Functionを取るものが追加されていました。
ラムダ式ですね。

このメソッドがなかなか優れもので、マッチしていなければ元の文字列をそのまま、マッチしていたらラムダ式を呼び出してマッチ部分を置換した文字列を返してくれます。

普通にやるとif文と文字列結合をあれこれしないといけないですが、1行でスッキリ書けます。


結果

というわけで、書いたコードがこちら。
//問題のテキストをファイルにしておいて読み込み
Path in = Paths.get("c:/temp", "input.txt");

//RecNNNという通し番号をヒットさせる
Pattern pt = Pattern.compile("(Rec)(\\d+)");

//ファイルを開いて1行ずつのストリームに
Files.lines(in, StandardCharsets.UTF_8)
    //マッチさせてMatcherに
    .map(s -> pt.matcher(s))
    //replaceAllにラムダ式を渡す
    .map(m -> m.replaceAll(mr -> mr.group(1) + (Integer.parseInt(mr.group(2)) + 12)))
    .forEach(System.out::println);
 
java.nio.file.Files#linesはファイルを開いて1行ずつストリームに流してくれます。
開くためにはjava.nio.file.Pathクラスじゃないとダメなので最初に取得しています。

正規表現は何かしら接頭辞がないと関係無いところまでヒットしてしまうので、合わせて()で後方参照できるようにしておきます。

普段通りのマッチ方法でStringからMatcherにマップし、次の行で置換を行っています。

該当のメソッドはjava.util.function.Functionを引数に取ります。これは引数1つ、戻り値有のapplyメソッドを持つ関数型インタフェースです。ですのでラムダ式を渡すことができます。関数型インタフェースとラムダ式については過去記事で解説してますのでご参照ください。

ラムダ式を通常通り書くと以下のようになります。
new Function<MatchResult, String>() {
        @Override
        public String apply(MatchResult mr) {
            return mr.group(1) + (Integer.parseInt(mr.group(2)) + 12);
        }
    }

ラムダ式にはjava.util.regex.MatchResultインタフェースが引数で渡されます。同パッケージのMatcherと似てますが、マッチした結果だけを扱うことができます。

groupメソッドでマッチした文字列を得られます。引数0は元の文字列で、1からが後方参照の番号になります。
今回使っている正規表現には()が2つあるので、1つ目が接頭辞に当たる「Rec」、2つ目は数値の繰り返しにマッチします。

接頭辞はそのまま、数値部分だけintにしてから増やしたい数値を足します。今回12を足しています。

メソッドから戻した文字列は元の文字列から該当箇所だけ入れ替わります。マッチしなかった行はラムダ式を呼び出さず、そのままストリームの次の処理へ行きます。

最後はそのままコンソールへ出力しています。
入力ファイルはUTF-8としているので、他の文字コードで保存していると例外になるので注意です。

【実行結果】
:
:
obj.setRec135("");
obj.setRec136(foo.getName());
obj.setRec137("固定文字");
if(foo.isBar()){
    obj.setRec138(foo.getParam1());
    obj.setRec139("");
}else{
    obj.setRec138("");
    obj.setRec139(foo.getParam2());
}
:
:
バッチリ増やせました。


おしまいのひとこと

と、い・う・わ・け・で。

仕事でもJava9が使いたいです。・゚・(ノД`)・゚・。

とわがままを言っても仕方ないので、自分で色々書いてみないとダメですね。

それではみなさまよきガジェットライフを(´∀`)ノ




▼こちらの記事もどうぞ

▼ブログを気に入っていただけたらRSS登録をお願いします!
▼ブログランキング参加中!応援よろしくお願いします。

スポンサーリンク