GNU ls の -v オプションのようにソートするComparator

GNU ls の -v オプション

-v
    バージョン名とバージョン番号でソートする。
    (訳注: バージョンの一番低いものが最初にくる。デフォルトのソートのように動作するが、
    10 進の数字のシーケンスは、インデックス番号またはバージョン番号として数値的に扱われる。
    ゼロを前にもつ数値部分は小数として扱われる。

       ls -1           ls -1v
       bar-1.gz        bar-1.gz
       bar-100.gz      bar-2.gz
       bar-12.gz       bar-12.gz
       bar-2.gz        bar-100.gz
       foo-1.007.gz    foo-1.007.gz
       foo-1.012b.gz   foo-1.01a.gz
       foo-1.01a.gz    foo-1.012b.gz

    )
http://www.linux.or.jp/JM/html/GNU_fileutils/man1/ls.1.html

のようにソートするComparatorが必要だったので、commons-langやcommons-collectionsを探してみたけど見つからなかった。なので自分で書いた。ただし「ゼロを前にもつ数値部分は小数として扱われる」とはなってません。今回はその必要がなかったので手抜きです。

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class VersionedStringComparator implements Comparator<String> {

	private static final Pattern _num_pattern = Pattern.compile("[0-9]+");

	private final Map<String, List<Comparable>> _cache = new HashMap<String, List<Comparable>>();


	private List<Comparable> split(String s) {
		List<Comparable> list = new ArrayList<Comparable>();

		int i = 0;

		Matcher m = _num_pattern.matcher(s);
		while (m.find()) {
			list.add(s.substring(i, m.start()));
			list.add(Integer.valueOf(m.group()));
			i = m.end();
		}

		if (i < s.length()) {
			list.add(s.substring(i));
		}

		return list;
	}

	private List<Comparable> toList(String s) {
		List<Comparable> list = _cache.get(s);
		if (list == null) {
			list = split(s);
			_cache.put(s, list);
		}
		return list;
	}

	public int compare(String o1, String o2) {
		List<Comparable> list1 = toList(o1);
		List<Comparable> list2 = toList(o2);

		for (int i = 0; i < list1.size(); ++i) {
			if (list2.size() < i+1) {
				return -1;
			}

			Comparable e1 = list1.get(i);
			Comparable e2 = list2.get(i);

			if (e1.getClass() != e2.getClass()) {
				e1 = e1.toString();
				e2 = e2.toString();
			}

			int result = e1.compareTo(e2);
			if (result != 0) {
				return result;
			}
		}

		return 0;
	}

}

文字列を数字部分とそれ以外とに切り分けたものをインスタンス変数(_cache)にキャッシュしてるので、使い回しできません。毎回インスタンス作って使ってください。まぁ _cache.clear() するメソッドを作れば一応使い回しできるようになるけど、そのメソッドをうっかり呼び忘れてメモりリークするようなことになるよりは、「使い回しできない」としちゃった方が良いかなと。

使用例:

List<String> list = Arrays.asList(new String[] {
		"bar-1.gz",
		"bar-100.gz",
		"bar-12.gz",
		"bar-2.gz",
		"foo-1.007.gz",
		"foo-1.012b.gz",
		"foo-1.01a.gz"
});

Collections.sort(list, new VersionedStringComparator());

for (String s : list) {
	System.out.println(s);
}

実行結果:
bar-1.gz
bar-2.gz
bar-12.gz
bar-100.gz
foo-1.01a.gz
foo-1.007.gz
foo-1.012b.gz

foo-1.01a.gz と foo-1.007.gz が GNU ls とは逆になってます。