「また明日から 2 つのデータの差分を目視で見つける仕事が始まるお……」
「ほんとは diff を使って速度も精度も上げたいんだお……」
「でもこの PC には JDK と Eclipse しか入れちゃいけないお……」
「……だから Eclipse を使って diff っぽいツールを自作するお!!」
ほんとは WinMerge とか入れられればいいんですけどね。「WinMerge 入れられない! 入れにくい!!」な環境でなんとかしてみようと思ったわけです。
使うライブラリ
Eclipse には「比較エディタ」という機能があり、2 つのファイルの差分を見ることができます。このとき差分検出に使われているライブラリ (というかプラグイン) は、classpath を通せば、自分のアプリケーションからも使えるっぽいです。
Eclipse 3.2 の場合でいうと、plugins ディレクトリにあるこの辺の jar を classpath に足せば動くっぽい。
1 つめのが使いたいライブラリで、残り 2 つは org.eclipse.compare が依存しているものです。「なんちゃら」の部分には、バージョン番号と日付が入ってますね。
使い方
大きく分けて、次の 3 つのステップを踏みます。
- IRangeComparator.html の実装クラスを作る
- RangeDifferencer のメソッド (findDifferences() または findRanges()) を呼んで、結果を受け取る。
- RangeDifferencer が返した RangeDifference[] を好きなように使う
これらのクラスはいずれも org.eclipse.compare.rangedifferencer パッケージに含まれています。
IRangeComparator の実装クラスを作る
diff はテキストデータを行単位で比較するツールですが、RangeDifferencer は、それ以外の比較も行えるよう、汎用的なつくりになっています。例えば、テキストデータを
段落単位で比較するとか、Java のソースをコメント除いて比較するとか、Excel ワークシートを行単位で比較するとかですね。
裏を返すと、実際のデータにどのようにアクセスするかは、こちらが教えてあげないといけないということで、IRangeComparator の実装クラスがそれにあたります。実装するメソッドは 3 つ。
メソッド | 説明 |
---|---|
int getRangeCount() | 要素数を取得する |
boolean rangesEqual(int thisIndex, IRangeComparator other, int otherIndex) | 要素の比較を行う |
boolean skipRangeComparison(int length, int maxLength, IRangeComparator other) | 処理が長くなりすぎそうなら true を返す |
簡単なところで、java.util.List 同士を比較する場合でいうと、こんな感じに実装するみたい。
import java.util.List; import org.eclipse.compare.rangedifferencer.IRangeComparator; public class ListRangeComparator<T> implements IRangeComparator { private List<T> list; public ListRangeComparator(List<T> list) { this.list = list; } public int getRangeCount() { return list.size(); } public boolean rangesEqual(int thisIndex, IRangeComparator other, int otherIndex) { return list.get(thisIndex).equals(((ListRangeComparator<T>)other).list.get(otherIndex)); } public boolean skipRangeComparison(int length, int maxLength, IRangeComparator other) { return false; } }
RangeDifferencer を呼んで RangeDifference[] を受け取る
RangeDifferencer には findDifferences() と findRanges() というメソッドがあります。前者は差異だけを返すのに対し、後者は差異がなかった部分も報告してくるらしいです。また、どちらのメソッドもオーバーロードされていて、進行状況の通知を受け取るかどうかと、比較するデータの数 (2 or 3) が選べます。
いちばん簡単な「進行状況の通知なしで 2 ファイル比較」だと、次のような感じです。上で作った IRangeComparator 実装クラスを 2 つ渡します。
public RangeDifference[] compare(List<T> left, List<T> right) { ListRangeComparator<T> cl = new ListRangeComparator<T>(left); ListRangeComparator<T> cr = new ListRangeComparator<T>(right); RangeDifference[] diffs = RangeDifferencer.findDifferences(cl, cr); return diffs; }
ちなみに、ドキュメントやメソッド名なんかを見ると、2 つのデータを比較する場合は left と right、3 つのデータを比較する場合は left と right と ancestor という呼び方をしています。ancestor (先祖) ということは、3 データの比較はどれかひとつを「変更前のもの」として扱うわけですね。
RangeDifference[] を好きに使う
比較した結果が配列で返されます。RangeDifference には以下のメソッドがあって、差異が見つかった箇所の開始・終了位置を取得することができます。
メソッド | 説明 |
---|---|
int kind() | 差異の種類 |
int ancestorStart() | |
int ancestorEnd() | |
int ancestorLength() | |
int leftStart() | |
int leftEnd() | |
int leftLength() | |
int rightStart() | |
int rightEnd() | |
int rightLength() | |
int maxLength() |
まぁ、名前でだいたいわかりますよね。kind() の値は、findDifferences() で 2 ファイル比較の場合は一種類しか返ってきません。それ以外の場合には、状況に応じて「ここは変わってなかった」とか「ここは left と right がコンフリクトしてた」というような情報が得られます。
後は目的に応じて処理すればいいのですが、例えば 2 つの List に対して findDifferences() で差分を取得したとすると、次のようにすれば結果を diff -u っぽくを出力することができます。
public void printDifferences(List<T> left, List<T> right, RangeDifference[] diffs) { for (RangeDifference diff : diffs) { System.out.printf( "@@ +%1$d,%2$d -%3$d,%4$d @@%n", diff.leftStart() + 1, diff.leftLength(), diff.rightStart() + 1, diff.rightLength()); printDifference('-', left, diff.leftStart(), diff.leftEnd()); printDifference('+', right, diff.rightStart(), diff.rightEnd()); } } private void printDifference(char c, List<T> list, int start, int end) { for (T item : list.subList(start, end)) { System.out.printf("%1$c%2$s%n", c, item.toString()); } }