Java – 对Windows资源pipe理器等string进行sorting

我正在尝试使用Sander Pham提出的另一个问题的代码。 我需要我的Java ArrayList的string名称进行sorting像Windows资源pipe理器。 他的代码只适用于一个问题。 我想就这个问题发表意见,但是我需要更多的声望来评论。 无论如何…他build议使用自定义的比较器实现的类,并使用它来比较string名称。 这是这个类的代码:

class IntuitiveStringComparator implements Comparator<String> { private String str1, str2; private int pos1, pos2, len1, len2; public int compare(String s1, String s2) { str1 = s1; str2 = s2; len1 = str1.length(); len2 = str2.length(); pos1 = pos2 = 0; int result = 0; while (result == 0 && pos1 < len1 && pos2 < len2) { char ch1 = str1.charAt(pos1); char ch2 = str2.charAt(pos2); if (Character.isDigit(ch1)) { result = Character.isDigit(ch2) ? compareNumbers() : -1; } else if (Character.isLetter(ch1)) { result = Character.isLetter(ch2) ? compareOther(true) : 1; } else { result = Character.isDigit(ch2) ? 1 : Character.isLetter(ch2) ? -1 : compareOther(false); } pos1++; pos2++; } return result == 0 ? len1 - len2 : result; } private int compareNumbers() { // Find out where the digit sequence ends, save its length for // later use, then skip past any leading zeroes. int end1 = pos1 + 1; while (end1 < len1 && Character.isDigit(str1.charAt(end1))) { end1++; } int fullLen1 = end1 - pos1; while (pos1 < end1 && str1.charAt(pos1) == '0') { pos1++; } // Do the same for the second digit sequence. int end2 = pos2 + 1; while (end2 < len2 && Character.isDigit(str2.charAt(end2))) { end2++; } int fullLen2 = end2 - pos2; while (pos2 < end2 && str2.charAt(pos2) == '0') { pos2++; } // If the remaining subsequences have different lengths, // they can't be numerically equal. int delta = (end1 - pos1) - (end2 - pos2); if (delta != 0) { return delta; } // We're looking at two equal-length digit runs; a sequential // character comparison will yield correct results. while (pos1 < end1 && pos2 < end2) { delta = str1.charAt(pos1++) - str2.charAt(pos2++); if (delta != 0) { return delta; } } pos1--; pos2--; // They're numerically equal, but they may have different // numbers of leading zeroes. A final length check will tell. return fullLen2 - fullLen1; } private int compareOther(boolean isLetters) { char ch1 = str1.charAt(pos1); char ch2 = str2.charAt(pos2); if (ch1 == ch2) { return 0; } if (isLetters) { ch1 = Character.toUpperCase(ch1); ch2 = Character.toUpperCase(ch2); if (ch1 != ch2) { ch1 = Character.toLowerCase(ch1); ch2 = Character.toLowerCase(ch2); } } return ch1 - ch2; } } 

在使用它的时候,除了string名字后面没有数字之外,它的效果很好。 如果它没有一个数字,它就放在列表的最后,这是错误的。 如果没有数字,应该是在开始。

 filename.jpg filename2.jpg filename03.jpg filename3.jpg 

目前它sorting…

 filename2.jpg filename03.jpg filename3.jpg filename.jpg 

在代码中需要更改什么来纠正这种行为?

谢谢

这是我第二次尝试回答这个问题。 我用http://www.interact-sw.co.uk/iangblog/2007/12/13/natural-sorting作为开始&#x3002; 不幸的是,我认为我也发现了问题。 但我认为在我的代码中这些问题是正确的。

信息:Windows资源管理器使用API​​函数StrCmpLogicalW()函数进行排序。 那里被称为自然排序

所以这里是我的WindowsExplorerSort uncentanding – 算法:

  • 文件名是部分明智的比较。 到目前为止,我确定了以下几个部分: 数字 ', 空间其他
  • 文件名中的每个数字都被认为是可能的数字比较。
  • 数字作为数字进行比较,但是如果它们相等,则较长的基本字符串首先出现。 这发生在前导零。
    • filename00.txt,filename0.txt
  • 如果将数字部分与非数字部分进行比较,则将其作为文本进行比较。
  • 文本将被比较为不区分大小写。

此列表部分基于尝试和错误。 我增加了测试文件名的数量,以解决更多的注释中提到的陷阱,结果是检查对Windows资源管理器。

所以这是这个的输出:

 filename filename 00 filename 0 filename 01 filename.jpg filename.txt filename00.jpg filename00a.jpg filename00a.txt filename0 filename0.jpg filename0a.txt filename0b.jpg filename0b1.jpg filename0b02.jpg filename0c.jpg filename01.0hjh45-test.txt filename01.0hjh46 filename01.1hjh45.txt filename01.hjh45.txt Filename01.jpg Filename1.jpg filename2.hjh45.txt filename2.jpg filename03.jpg filename3.jpg 

新的比较器WindowsExplorerComparator在已经提到的部分中分割文件名,并对两个文件名进行部分明智的比较。 为了正确,新的比较器使用字符串作为输入,所以必须创建一个适配器比较器

 new Comparator<File>() { private final Comparator<String> NATURAL_SORT = new WindowsExplorerComparator(); @Override public int compare(File o1, File o2) {; return NATURAL_SORT.compare(o1.getName(), o2.getName()); } } 

所以这里是新的比较器源代码和测试:

 import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; public class WindowsSorter { public static void main(String args[]) { //huge test data set ;) List<File> filenames = Arrays.asList(new File[]{new File("Filename01.jpg"), new File("filename"), new File("filename0"), new File("filename 0"), new File("Filename1.jpg"), new File("filename.jpg"), new File("filename2.jpg"), new File("filename03.jpg"), new File("filename3.jpg"), new File("filename00.jpg"), new File("filename0.jpg"), new File("filename0b.jpg"), new File("filename0b1.jpg"), new File("filename0b02.jpg"), new File("filename0c.jpg"), new File("filename00a.jpg"), new File("filename.txt"), new File("filename00a.txt"), new File("filename0a.txt"), new File("filename01.0hjh45-test.txt"), new File("filename01.0hjh46"), new File("filename2.hjh45.txt"), new File("filename01.1hjh45.txt"), new File("filename01.hjh45.txt"), new File("filename 01"), new File("filename 00")}); //adaptor for comparing files Collections.sort(filenames, new Comparator<File>() { private final Comparator<String> NATURAL_SORT = new WindowsExplorerComparator(); @Override public int compare(File o1, File o2) {; return NATURAL_SORT.compare(o1.getName(), o2.getName()); } }); for (File f : filenames) { System.out.println(f); } } public static class WindowsExplorerComparator implements Comparator<String> { private static final Pattern splitPattern = Pattern.compile("\\d+|\\.|\\s"); @Override public int compare(String str1, String str2) { Iterator<String> i1 = splitStringPreserveDelimiter(str1).iterator(); Iterator<String> i2 = splitStringPreserveDelimiter(str2).iterator(); while (true) { //Til here all is equal. if (!i1.hasNext() && !i2.hasNext()) { return 0; } //first has no more parts -> comes first if (!i1.hasNext() && i2.hasNext()) { return -1; } //first has more parts than i2 -> comes after if (i1.hasNext() && !i2.hasNext()) { return 1; } String data1 = i1.next(); String data2 = i2.next(); int result; try { //If both datas are numbers, then compare numbers result = Long.compare(Long.valueOf(data1), Long.valueOf(data2)); //If numbers are equal than longer comes first if (result == 0) { result = -Integer.compare(data1.length(), data2.length()); } } catch (NumberFormatException ex) { //compare text case insensitive result = data1.compareToIgnoreCase(data2); } if (result != 0) { return result; } } } private List<String> splitStringPreserveDelimiter(String str) { Matcher matcher = splitPattern.matcher(str); List<String> list = new ArrayList<String>(); int pos = 0; while (matcher.find()) { list.add(str.substring(pos, matcher.start())); list.add(matcher.group()); pos = matcher.end(); } list.add(str.substring(pos)); return list; } } } 

如果您正在排序或者可以将其表示为文件集合,则可能需要查看Apache Commons IO库NameFileComparator类。 这提供了几个预构建的比较器,你可以利用它来完成你正在寻找的东西。 例如,NAME_INSENSITIVE_COMPARATOR应该做你想要的。

 List<File> filenames = Arrays.asList(new File[] { new File("Filename01.jpg"), new File("Filename1.jpg"), new File("filename.jpg"), new File("filename2.jpg"), new File("filename03.jpg"), new File("filename3.jpg")}); Collections.sort(filenames, NameFileComparator.NAME_INSENSITIVE_COMPARATOR); for (File f : filenames) { System.out.println(f); } 

输出:

 filename.jpg Filename01.jpg filename03.jpg Filename1.jpg filename2.jpg filename3.jpg 

在比较方法中切换第一个-1和第一个的符号:

 if (Character.isDigit(ch1)) { result = Character.isDigit(ch2) ? compareNumbers() : 1; } else if (Character.isLetter(ch1)) { result = Character.isLetter(ch2) ? compareOther(true) : 1; } 

这些决定了当第一个字符串有一个数字但是第二个字符串没有,或者第一个字符串不是第二个字符串时的顺序。

只是从评论中完成我的建议。 这是一个恕我直言更好的可读版本的比较器,(希望)按您需要的方式排序。 主要逻辑就像我所说的那样:

 //Compare the namepart caseinsensitive. int result = data1.name.compareToIgnoreCase(data2.name); //If name is equal, then compare by number if (result == 0) { result = data1.number.compareTo(data2.number); } //If numbers are equal then compare by length text of number. This //is valid because it differs only by heading zeros. Longer comes //first. if (result == 0) { result = -Integer.compare(data1.numberText.length(), data2.numberText.length()); } //If all above is equal, compare by ext. if (result == 0) { result = data1.ext.compareTo(data2.ext); } 

如您所见,这是一个动态版本,可以处理名称和扩展名,而不需要任何假设。 我在这个小测试程序中包含了你的第一个和你在评论中添加的测试数据。

所以这里是你的测试数据的排序输出:

 filename.jpg filename00.jpg filename0.jpg Filename01.jpg Filename1.jpg filename2.jpg filename03.jpg filename3.jpg filename0b.jpg filename0b1.jpg filename0b02.jpg filename0c.jpg 

最后但并非最不重要的完整代码:

 import java.io.File; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; public class WindowsSorter { public static void main(String args[]) { List<File> filenames = Arrays.asList(new File[]{new File("Filename01.jpg"), new File("Filename1.jpg"), new File("filename.jpg"), new File("filename2.jpg"), new File("filename03.jpg"), new File("filename3.jpg"), new File("filename00.jpg"), new File("filename0.jpg"), new File("filename0b.jpg"), new File("filename0b1.jpg"), new File("filename0b02.jpg"), new File("filename0c.jpg")}); Collections.sort(filenames, new WindowsLikeComparator()); for (File f : filenames) { System.out.println(f); } } private static class WindowsLikeComparator implements Comparator<File> { //Regexp to make the 3 part split of the filename. private static final Pattern splitPattern = Pattern.compile("^(.*?)(\\d*)(?:\\.([^.]*))?$"); @Override public int compare(File o1, File o2) { SplitteFileName data1 = getSplittedFileName(o1); SplitteFileName data2 = getSplittedFileName(o2); //Compare the namepart caseinsensitive. int result = data1.name.compareToIgnoreCase(data2.name); //If name is equal, then compare by number if (result == 0) { result = data1.number.compareTo(data2.number); } //If numbers are equal then compare by length text of number. This //is valid because it differs only by heading zeros. Longer comes //first. if (result == 0) { result = -Integer.compare(data1.numberText.length(), data2.numberText.length()); } //If all above is equal, compare by ext. if (result == 0) { result = data1.ext.compareTo(data2.ext); } return result; } private SplitteFileName getSplittedFileName(File f) { Matcher matcher = splitPattern.matcher(f.getName()); if (matcher.matches()) { return new SplitteFileName(matcher.group(1), matcher.group(2), matcher.group(3)); } else { return new SplitteFileName(f.getName(), null, null); } } static class SplitteFileName { String name; Long number; String numberText; String ext; public SplitteFileName(String name, String numberText, String ext) { this.name = name; if ("".equals(numberText)) { this.number = -1L; } else { this.number = Long.valueOf(numberText); } this.numberText = numberText; this.ext = ext; } } } } 

编辑1:该算法已更改为地址filename00,filename0排序问题。

编辑2:深入探索Windows Explorers排序算法后,很明显,这个答案确实是原始发布和测试数据的解决方案 – 这就是为什么我不会删除它 – 但不是一个完整的解决方案模仿Windows资源管理器的行为。 因此,我将提供另一个希望更完整的解决方案。