diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/comparator/WindowsExplorerStringComparator.java b/hutool-core/src/main/java/org/dromara/hutool/core/comparator/WindowsExplorerStringComparator.java new file mode 100644 index 000000000..2bac97696 --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/comparator/WindowsExplorerStringComparator.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2024. looly(loolly@aliyun.com) + * Hutool is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * https://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +package org.dromara.hutool.core.comparator; + +import org.dromara.hutool.core.text.StrUtil; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Windows 资源管理器风格字符串比较器 + * + *

此比较器模拟了 Windows 资源管理器的文件名排序方式,可得到与其相同的排序结果。

+ * + *

假设有一个数组,包含若干个文件名 {@code {"abc2.doc", "abc1.doc", "abc12.doc"}}

+ *

在 Windows 资源管理器中以名称排序时,得到 {@code {"abc1.doc", "abc2.doc", "abc12.doc" }}

+ *

调用 {@code Arrays.sort(filenames);} 时,得到 {@code {"abc1.doc", "abc12.doc", "abc2.doc" }}

+ *

调用 {@code Arrays.sort(filenames, new WindowsExplorerStringComparator());} 时,得到 {@code {"abc1.doc", "abc2.doc", + * "abc12.doc" }},这与在资源管理器中看到的相同

+ * + * @author YMNNs + * @see + * Java - Sort Strings like Windows Explorer + */ +public class WindowsExplorerStringComparator implements Comparator { + + /** + * 单例 + */ + public static final WindowsExplorerStringComparator INSTANCE = new WindowsExplorerStringComparator(); + + private static final Pattern splitPattern = Pattern.compile("\\d+|\\.|\\s"); + + @Override + public int compare(final CharSequence str1, final CharSequence str2) { + final Iterator i1 = splitStringPreserveDelimiter(str1).iterator(); + final Iterator 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()) { + return -1; + } + //first has more parts than i2 -> comes after + if (!i2.hasNext()) { + return 1; + } + + final String data1 = i1.next(); + final String data2 = i2.next(); + int result; + try { + //If both data are numbers, then compare numbers + result = Long.compare(Long.parseLong(data1), Long.parseLong(data2)); + //If numbers are equal than longer comes first + if (result == 0) { + result = -Integer.compare(data1.length(), data2.length()); + } + } catch (final NumberFormatException ex) { + //compare text case insensitive + result = data1.compareToIgnoreCase(data2); + } + + if (result != 0) { + return result; + } + } + } + + private List splitStringPreserveDelimiter(final CharSequence str) { + final Matcher matcher = splitPattern.matcher(str); + final List list = new ArrayList<>(); + int pos = 0; + while (matcher.find()) { + list.add(StrUtil.sub(str, pos, matcher.start())); + list.add(matcher.group()); + pos = matcher.end(); + } + list.add(StrUtil.subSuf(str, pos)); + return list; + } +} diff --git a/hutool-core/src/test/java/org/dromara/hutool/core/comparator/WindowsExplorerStringComparatorTest.java b/hutool-core/src/test/java/org/dromara/hutool/core/comparator/WindowsExplorerStringComparatorTest.java new file mode 100644 index 000000000..fe65fc347 --- /dev/null +++ b/hutool-core/src/test/java/org/dromara/hutool/core/comparator/WindowsExplorerStringComparatorTest.java @@ -0,0 +1,67 @@ +package org.dromara.hutool.core.comparator; + +import org.dromara.hutool.core.collection.ListUtil; +import org.dromara.hutool.core.lang.Assert; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class WindowsExplorerStringComparatorTest { + + final List answer1 = ListUtil.of( + "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" + ); + + List answer2 = ListUtil.of( + "abc1.doc", + "abc2.doc", + "abc12.doc" + ); + + @Test + public void testCompare1() { + final List toSort = new ArrayList<>(answer1); + while (toSort.equals(answer1)) { + Collections.shuffle(toSort); + } + toSort.sort(WindowsExplorerStringComparator.INSTANCE); + Assert.equals(toSort, answer1); + } + + @Test + public void testCompare2() { + final List toSort = new ArrayList<>(answer2); + while (toSort.equals(answer2)) { + Collections.shuffle(toSort); + } + toSort.sort(WindowsExplorerStringComparator.INSTANCE); + Assert.equals(toSort, answer2); + } +}