Java - 像 Windows 资源管理器一样对字符串进行排序

Posted

技术标签:

【中文标题】Java - 像 Windows 资源管理器一样对字符串进行排序【英文标题】:Java - Sort Strings like Windows Explorer 【发布时间】:2014-06-05 23:13:30 【问题描述】:

我正在尝试在另一个问题上使用 Sander Pham 建议的代码。我需要像 Windows 资源管理器那样对字符串名称的 java ArrayList 进行排序。他的代码适用于所有问题,但只有一个问题。我本来想对这个问题发表评论,但我需要更多的声誉点来发表评论。无论如何......他建议使用自定义比较器实现类并使用它来比较字符串名称。这是该类的代码:

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;
       

在使用它时,它工作得很好,除非字符串名称后面没有数字。如果它没有数字,则将其放在列表的末尾,这是错误的。如果它没有数字,它应该在开头。

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

目前它的排序...

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

我需要在代码中进行哪些更改才能更正此行为?

谢谢

【问题讨论】:

这种排序是否有可用的规则集?如果有 file5b7.jpg 之类的名称怎么办,那么其他扩展名呢?扩展点之前的最后一个数字总是特殊处理吗?将文件名分成三部分名称,数字,分机并让比较器比较名字和相等的数字然后分机会更简单吗?该数字将转换为 int。 对。扩展点之前的文件名和编号是被排序的点。几乎只需要准确模仿 Windows 资源管理器的排序方式。更多示例是... filename00.jpg filename0.jpg filename0b.jpg filename0b1.jpg filename0b02.jpg filename0c.jpg filename1.jpg 我相信,在当前给出的代码中,这就是行为。我注意到唯一不起作用的是,如果文件名后面根本没有数字,它会排在其他所有内容之后,而不是之前。 不会有其他扩展。所以不同的扩展并不是一个真正的问题。 【参考方案1】:

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

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

这是我对 WindowsExplorerSort - 算法的理解:

文件名是部分比较的。至于现在,我确定了以下部分:numbers、'.'、spacesrest。。李> 文件名中的每个数字都被考虑用于可能的数字比较。 数字作为数字进行比较,但如果它们相等,则首先出现较长的基本字符串。前导零会发生这种情况。 文件名00.txt,文件名0.txt 如果将数字部分与非数字部分进行比较,则会将其作为文本进行比较。 文本将不区分大小写。

此列表部分基于尝试和错误。我增加了测试文件名的数量,以解决更多在 cmets 中提到的缺陷,并针对 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 将文件名拆分为已经提到的部分,并对两个文件名进行部分比较。正确地说,新的比较器使用 Strings 作为其输入,因此必须创建一个适配器比较器,如

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;
        
    

【讨论】:

尤里卡!完美!非常感谢你的帮助。你是最棒的。 你是对的。但正如你所见,我进行了四处测试,至少在当时没有具体的文档说明这种自然排序是如何工作的。我相信还有更多有问题的角色,例如$. 嘿,您的解决方案在某种程度上可以工作.....我想出在窗口中显示的名称与使用此解决方案显示的名称不同.. 所以举一些例子。然而,这是一种模仿行为的尝试。【参考方案2】:

如果您要排序的内容是或可以表示为文件集合,您可能需要查看 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

【讨论】:

谢谢。这可能是最接近我想要的那个图书馆的东西。但是,filename03.jpg 需要在 3 之前排序,而不是 1。【参考方案3】:

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

if (Character.isDigit(ch1))

    result = Character.isDigit(ch2) ? compareNumbers() : 1;

else if (Character.isLetter(ch1))

    result = Character.isLetter(ch2) ? compareOther(true) : 1;

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

【讨论】:

谢谢。这确实解决了这个问题,但它也最终破坏了其他东西。当我切换 -1 和 1 时,01 和 01a 的顺序搞砸了。它正确排序为 01,然后是 01a。切换-1和1时,先排序01a再排序01。 没关系。在你的帮助下,我想通了。我没有切换 -1 和 1,而是将 -1 更改为 1 并单独留下另一个 1。这解决了 01 和 01a 排序的问题。谢谢 啊。我有点不清楚为什么会这样,但我很高兴你解决了它。我已编辑我的答案以反映您找到的解决方案。 事实证明,这种同时拥有 1 和 1 而不是 -1 和 1 的方法有时才有效。然后最终,它产生了一个异常,说比较方法违反了它的一般合同。如果 -1 用于“isDigit”,则文件名将出现在文件名 2 和文件名 3 之后,而不是之前。如果 -1 表示“isLetter”,则 filename0a 位于 filename0 之前,而不是之后。【参考方案4】:

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

//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);

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

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

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 资源管理器排序算法之后,很明显,这个答案确实是原始帖子和测试数据的解决方案 - 这就是我不会删除它的原因 - 但不是模仿 Windows 资源管理器行为的完整解决方案。因此,我将为该问题提供另一个希望更完整的解决方案。

【讨论】:

哇,谢谢。这确实很好用。我只能看到一个问题。那就是 filename00 在 filename0 之后出现,而它应该在之前出现。有没有简单的解决方法? 发现另一个小问题。 filename00a 出现在列表的末尾,而不是紧跟在 filename00 之后。所以它排序 filename > filename0 > filename00 > filename2 > filename03 > filename3 > filename00a 需要排序 filename > filename00 > filename00a > filename0 > filename2 > filename03 > filename3 但是你说首先比较名称,因为 filename00a 是名称(末尾没有数字),这遵循原始规则。还是我错过了什么?对于 00 期,我将在一段时间内更改我的帖子。 感谢 00 更新。我想这很难解释。如果您有 Windows 系统,您可以看到它对文件夹中的文件进行排序的方式。我希望它模仿它。我会分解它。我想这更像是比较每个字符,而不是最后是否有数字。在下面的示例中,您可以看到排序器首先看到有 3 个零。因此,任何有 3 个零的东西都会排在有 2 个零的任何东西之前,而不是后面有一个字母。然后它将在零之后排序,以查看哪个文件名在前。 (文件名000 > 文件名000a) filename000 > filename000a >filename00 > filename00a > filename00b > filename0 > filename1b2a3 > filename1b2b > filename1c > filename2 我希望这更清楚。【参考方案5】:

使用操作系统本机调用的仅限 Windows 的解决方案:https://***.com/a/60099813/4494577

在 Windows 中按名称排序很棘手,而且比您的实现要复杂得多。它也是可配置的和版本相关的。

注意:我为本文后面的内容创建了一个演示。 Check it out on GitHub.

使用StrCmpLogicalWComparator function对文件名进行排序

根据某些(例如here),Windows 使用StrCmpLogicalW 按名称对文件进行排序。

您可以尝试通过使用 JNA 调用此系统函数来实现您的比较器(不要忘记在您的项目中包含 JNA library):

比较器:

public class StrCmpLogicalWComparator implements Comparator<String> 

    @Override
    public int compare(String o1, String o2) 
        return Shlwapi.INSTANCE.StrCmpLogicalW(
            new WString(o1), new WString(o2));
    

JNA 部分:

import com.sun.jna.WString;
import com.sun.jna.win32.StdCallLibrary;

public interface Shlwapi extends StdCallLibrary 

    Shlwapi INSTANCE = Native.load("Shlwapi", Shlwapi.class);

    int StrCmpLogicalW(WString psz1, WString psz2);

处理包含数字的文件名

我之前提到过 Windows 资源管理器对文件进行排序的方式是可配置的。您可以更改文件名中数字的处理方式并切换所谓的“数字排序”。您可以阅读如何配置此here。文档中解释的数字排序:

排序时将数字视为数字,例如将“2”排在“10”之前。

-- https://docs.microsoft.com/en-us/windows/win32/api/stringapiset/nf-stringapiset-comparestringex#SORT_DIGITSASNUMBERS

启用数字排序后,结果为:

而在禁用数字排序的情况下,它看起来像这样:

这让我觉得 Windows 资源管理器实际上使用 CompareStringEx function 进行排序,可以参数化以启用此功能。

使用CompareStringEx function对文件名进行排序

JNA 部分:

import com.sun.jna.Pointer;
import com.sun.jna.WString;
import com.sun.jna.win32.StdCallLibrary;

public interface Kernel32 extends StdCallLibrary 

    Kernel32 INSTANCE = Native.load("Kernel32", Kernel32.class);
    WString INVARIANT_LOCALE = new WString("");

    int CompareStringEx(WString lpLocaleName,
                        int dwCmpFlags,
                        WString lpString1,
                        int cchCount1,
                        WString lpString2,
                        int cchCount2,
                        Pointer lpVersionInformation,
                        Pointer lpReserved,
                        int lParam);

    default int CompareStringEx(int dwCmpFlags,
                                String str1,
                                String str2) 
        return Kernel32.INSTANCE
            .CompareStringEx(
                INVARIANT_LOCALE,
                dwCmpFlags,
                new WString(str1),
                str1.length(),
                new WString(str2),
                str2.length(),
                Pointer.NULL,
                Pointer.NULL,
                0);
    

数字排序比较器:

public class CompareStringExNumericComparator implements Comparator<String> 

    private static int SORT_DIGITSASNUMBERS = 0x00000008;

    @Override
    public int compare(String o1, String o2) 
        int compareStringExComparisonResult =
            Kernel32.INSTANCE.CompareStringEx(SORT_DIGITSASNUMBERS, o1, o2);

        // CompareStringEx returns 1, 2, 3 respectively instead of -1, 0, 1
        return compareStringExComparisonResult - 2;
    

非数字排序比较器:

public class CompareStringExNonNumericComparator implements Comparator<String> 

    private static String INVARIANT_LOCALE = "";
    private static int NO_OPTIONS = 0;

    @Override
    public int compare(String o1, String o2) 
        int compareStringExComparisonResult =
            Kernel32.INSTANCE.CompareStringEx(NO_OPTIONS, o1, o2);

        // CompareStringEx returns 1, 2, 3 respectively instead of -1, 0, 1
        return compareStringExComparisonResult - 2;
    

参考文献

Martin Liversage'sanswer to "What is the shortest way in .NET to sort strings starting with 1, 10 and 2 and respect the number ordering? hmuelner'sanswer to "What is the first character in the sort order used by Windows Explorer?"

-- https://***.com/a/60099813/4494577

【讨论】:

以上是关于Java - 像 Windows 资源管理器一样对字符串进行排序的主要内容,如果未能解决你的问题,请参考以下文章

像任务管理器(Windows 8)一样杀死进程,因为访问被拒绝

带有WPF的Aero玻璃框架上类似Windows资源管理器的搜索框

ubuntu12.04如何打开像任务管理器一样的东西?见过,现在忘了

在 Java 中按名称排序文件与 Windows 资源管理器不同

像Linux一样安装软件(2):包管理器一锅炖

如何像 Windows 任务管理器中显示的那样获得可用的物理内存