├── .gitignore ├── LICENSE ├── README.md └── src └── com └── obroom └── filenamesort ├── test ├── WindowsSortTest.java ├── 测试列表1.txt └── 测试列表2.txt └── util ├── AsciiUtil.java └── FileNameComparator.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.ear 17 | *.zip 18 | *.tar.gz 19 | *.rar 20 | 21 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 22 | hs_err_pid* 23 | /.idea 24 | /*.iml 25 | /out 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 ob 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # windows-filename-sort 2 | File name are sorted by windows rule 3 | 4 | 文件名称采用windows资源管理器的排序算法排列 5 | 6 | --- 7 | 下面列一下Windows文件名的排序规则(不一定全) 8 | * 每个文件名按规则拆分成多个比较单元 9 | * 特殊字符采用ascii码位于数字前面 10 | * 特殊字符半角 < 特殊字符全角 11 | * 特殊字符 < 数字 < 字母 < 中文 12 | * 负号(可多个)与任何数字组成负数算作一个比较单元 13 | * 同个比较单元,负号少的 < 负号多的 14 | * 拆分出来的数字单元按数值大小比较,比如: a(1).txt < a(2).txt < a(11).txt 15 | * 数字按绝对值的方式比较,绝对值相同的,负数位于正数前面 16 | * 数值相等的,数字位数较多的位于数字位数较少的前面,比如: 000.txt < 00.txt < 0.txt 17 | * 中文采用拼音的顺序比较,比如: 国.txt < 人.txt < 中.txt 18 | * 还有各种组合的特殊场景比较 19 | * ... 20 | 21 | 具体示例可以参考下 [测试文件1](https://github.com/kookob/windows-filename-sort/blob/master/src/com/obroom/filenamesort/test/%E6%B5%8B%E8%AF%95%E5%88%97%E8%A1%A81.txt) 文件里面的排序结果。 -------------------------------------------------------------------------------- /src/com/obroom/filenamesort/test/WindowsSortTest.java: -------------------------------------------------------------------------------- 1 | package com.obroom.filenamesort.test; 2 | 3 | import com.obroom.filenamesort.util.FileNameComparator; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Collections; 7 | import java.util.Comparator; 8 | import java.util.List; 9 | 10 | /** 11 | * 简单的测试代码 12 | * Created by ob on 2018/3/13. 13 | */ 14 | public class WindowsSortTest { 15 | public static void main(String[] args) throws Exception { 16 | List filenameList = new ArrayList<>(); 17 | filenameList.add("hello.txt"); 18 | filenameList.add("world.txt"); 19 | 20 | Collections.sort(filenameList, new Comparator() { 21 | private final Comparator NATURAL_SORT = new FileNameComparator(); 22 | @Override 23 | public int compare(String o1, String o2) { 24 | return NATURAL_SORT.compare(o1, o2); 25 | } 26 | }); 27 | for (String filename : filenameList) { 28 | System.out.println(filename); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/com/obroom/filenamesort/test/测试列表1.txt: -------------------------------------------------------------------------------- 1 | -!!!.jpg 2 | -!!.jpg 3 | -!!.jpg 4 | !.jpg 5 | -!.jpg 6 | --!.jpg 7 | ---!.jpg 8 | ------!--------.jpg 9 | ----------!.jpg 10 | !.jpg 11 | -!.jpg 12 | !2.jpg 13 | !3.jpg 14 | # .jpg 15 | # .jpg 16 | #!.jpg 17 | #.jpg 18 | -#.jpg 19 | --#.jpg 20 | ---#.jpg 21 | -----------#.jpg 22 | #4.jpg 23 | $.jpg 24 | $5.jpg 25 | %.jpg 26 | %6.jpg 27 | &.jpg 28 | &7.jpg 29 | (.jpg 30 | -(.jpg 31 | (.jpg 32 | -(.jpg 33 | (8.jpg 34 | (9.jpg 35 | (a.jpg 36 | (a.jpg 37 | ).jpg 38 | ).jpg 39 | )10.jpg 40 | )11.jpg 41 | ,.jpg 42 | ,.jpg 43 | ,12.jpg 44 | ,13.jpg 45 | 、 (2).jpg 46 | 、.jpg 47 | 、14.jpg 48 | ..jpg 49 | .1.jpg 50 | .15.jpg 51 | -.jpg 52 | --.jpg 53 | ---.jpg 54 | -----------.jpg 55 | .jpg.jpg 56 | 。.jpg 57 | 。16.jpg 58 | :.jpg 59 | ;.jpg 60 | ;.jpg 61 | ;17.jpg 62 | ?.jpg 63 | ?18.jpg 64 | @.jpg 65 | @19.jpg 66 | [.jpg 67 | [20.jpg 68 | [a.jpg 69 | [b.jpg 70 | \.jpg 71 | ].jpg 72 | ]21.jpg 73 | ^.jpg 74 | ^22.jpg 75 | `.jpg 76 | `23.jpg 77 | {.jpg 78 | {.jpg 79 | {24.jpg 80 | {25.jpg 81 | {a.jpg 82 | {a.jpg 83 | |.jpg 84 | }.jpg 85 | }.jpg 86 | }26.jpg 87 | }27.jpg 88 | ~.jpg 89 | ~28.jpg 90 | ‘.jpg 91 | ’.jpg 92 | “.jpg 93 | “29.jpg 94 | ”.jpg 95 | ”30.jpg 96 | 《.jpg 97 | 》.jpg 98 | ¥.jpg 99 | ¥31.jpg 100 | 【.jpg 101 | 【32.jpg 102 | 【a.jpg 103 | 】.jpg 104 | 】33.jpg 105 | +!.jpg 106 | +.jpg 107 | ++!.jpg 108 | +34.jpg 109 | =.jpg 110 | =35.jpg 111 | ×.jpg 112 | ….jpg 113 | ……37.jpg 114 | …36.jpg 115 | 000000.jpg 116 | 00000.jpg 117 | 0000.jpg 118 | -0000.jpg 119 | 000.jpg 120 | 00.jpg 121 | -00.jpg 122 | ---0 123 | -0.0.jpg 124 | 0.jpg 125 | -0.jpg 126 | --0-.jpg 127 | ---0.jpg 128 | 0-0-.jpg 129 | 0--0.jpg 130 | -0-0.jpg 131 | -0--0.jpg 132 | 0001.jpg 133 | 001.jpg 134 | 1 1.jpg 135 | 1 1 .jpg 136 | ---1(.jpg 137 | ---1(.jpg 138 | -1).jpg 139 | 1.jpg 140 | -1.jpg 141 | --1.jpg 142 | --1-.jpg 143 | ---1.jpg 144 | 1.jpg 145 | --1.jpg 146 | -1.jpg 147 | -1[.jpg 148 | 1-1.jpg 149 | 1-1-.jpg 150 | 1--1.jpg 151 | 1---1.jpg 152 | 1----1.jpg 153 | -1-1.jpg 154 | 1-1-1-.jpg 155 | 1----11.jpg 156 | 2.jpg 157 | -2.jpg 158 | ---2.jpg 159 | -----2.jpg 160 | --3.jpg 161 | 5.jpg 162 | -5.jpg 163 | --6.jpg 164 | 11.jpg 165 | -11.jpg 166 | 111.jpg 167 | 1111.jpg 168 | a&7.jpg 169 | a-&7.jpg 170 | a&8.jpg 171 | a(.jpg 172 | a(.jpg 173 | a(1).jpg 174 | a(2).jpg 175 | a(3).jpg 176 | a(-3).jpg 177 | a(3).jpg 178 | a(3).jpg 179 | a(-3).jpg 180 | a(-3).jpg 181 | a(4).jpg 182 | a(-4).jpg 183 | a(11).jpg 184 | a).jpg 185 | a).jpg 186 | a.jpg 187 | a[.jpg 188 | a[2].jpg 189 | a[4].jpg 190 | a[11] - 副本.jpg 191 | a[11].jpg 192 | a-[20.jpg 193 | a].jpg 194 | a{.jpg 195 | a{.jpg 196 | a{2}.jpg 197 | a{3}.jpg 198 | a{3}.jpg 199 | a{4}.jpg 200 | a{-4}.jpg 201 | a{----4}.jpg 202 | a{4}.jpg 203 | a{5}.jpg 204 | a{11}.jpg 205 | a}.jpg 206 | a}.jpg 207 | --a------‘.jpg 208 | -a------’.jpg 209 | --a’.jpg 210 | a【.jpg 211 | a【4】.jpg 212 | a】.jpg 213 | a-1.jpg 214 | a-2(1).jpg 215 | a-2(1).jpg 216 | a-2.(1).jpg 217 | a-2.jpg 218 | a4.jpg 219 | aa------.jpg 220 | --aa---bb.jpg 221 | ---aa--bb.jpg 222 | ---aa--bb-----.jpg 223 | abc.jpg 224 | abcd.jpg 225 | a国.jpg 226 | a人.jpg 227 | a中.jpg 228 | b---.jpg 229 | --b.jpg 230 | --b--.jpg 231 | ---b.jpg 232 | b--6.jpg 233 | file.jpg 234 | file2.jpg 235 | file03.jpg 236 | file3.jpg -------------------------------------------------------------------------------- /src/com/obroom/filenamesort/test/测试列表2.txt: -------------------------------------------------------------------------------- 1 | !.txt 2 | !.txt 3 | ".txt 4 | #.txt 5 | #.txt 6 | $.txt 7 | $.txt 8 | %.txt 9 | %.txt 10 | (.txt 11 | (.txt 12 | ).txt 13 | ).txt 14 | *.txt 15 | ,.txt 16 | ,.txt 17 | 、.txt 18 | ..txt 19 | -..txt 20 | ..txt 21 | .txt 22 | -.txt 23 | ---.txt 24 | ---------.txt 25 | -----.txt 26 | 。.txt 27 | /.txt 28 | :.txt 29 | ;.txt 30 | ;.txt 31 | ?.txt 32 | @.txt 33 | @.txt 34 | [.txt 35 | [.txt 36 | \.txt 37 | ].txt 38 | ].txt 39 | ^.txt 40 | ^.txt 41 | `.txt 42 | `.txt 43 | {.txt 44 | {.txt 45 | |.txt 46 | }.txt 47 | }.txt 48 | ~.txt 49 | ~.txt 50 | ‘.txt 51 | ’.txt 52 | “.txt 53 | ”.txt 54 | 《.txt 55 | 》.txt 56 | ¥.txt 57 | 【.txt 58 | 】.txt 59 | +.txt 60 | +.txt 61 | <.txt 62 | =.txt 63 | =.txt 64 | >.txt 65 | ×.txt 66 | ….txt 67 | -a.txt 68 | aa.txt 69 | ab.txt -------------------------------------------------------------------------------- /src/com/obroom/filenamesort/util/AsciiUtil.java: -------------------------------------------------------------------------------- 1 | package com.obroom.filenamesort.util; 2 | 3 | /** 4 | * 全半角转换工具类 5 | * 代码复制于:https://segmentfault.com/a/1190000010841143 6 | * 名称做了一些调整 7 | */ 8 | public class AsciiUtil { 9 | public static final char SBC_SPACE = 12288; //全角空格 10 | public static final char DBC_SPACE = 32; //半角空格 11 | public static final char ASCII_END = 126;//ascii结束 12 | public static final char UNICODE_START = 65281;//unicode开始 13 | public static final char UNICODE_END = 65374;//unicode结束 14 | public static final char DBC_SBC_STEP = 65248; // 全角半角转换间隔 15 | 16 | /** 17 | * 判断是否全角字符 18 | * 19 | * @param c 20 | * @return 21 | */ 22 | public static boolean isFullChar(char c) { 23 | if (c == SBC_SPACE) { 24 | return true; 25 | } 26 | if (c >= UNICODE_START && c <= UNICODE_END) { 27 | return true; 28 | } 29 | return false; 30 | } 31 | 32 | /** 33 | * 判断是否半角字符 34 | * 35 | * @param c 36 | * @return 37 | */ 38 | public static boolean isHalfChar(char c) { 39 | if (c == DBC_SPACE) { 40 | return true; 41 | } 42 | if (c <= ASCII_END) { 43 | return true; 44 | } 45 | return false; 46 | } 47 | 48 | /** 49 | * 字符全角转半角 50 | * 51 | * @param src 52 | * @return 53 | */ 54 | public static char full2half(char src) { 55 | if (src == SBC_SPACE) { 56 | return DBC_SPACE; 57 | } 58 | if (src >= UNICODE_START && src <= UNICODE_END) { 59 | return (char) (src - DBC_SBC_STEP); 60 | } 61 | return src; 62 | } 63 | 64 | /** 65 | * 字符串全角转半角 66 | * 67 | * @param src 68 | * @return 69 | */ 70 | public static String full2half(String src) { 71 | if (src == null) { 72 | return null; 73 | } 74 | char[] c = src.toCharArray(); 75 | for (int i = 0; i < c.length; i++) { 76 | c[i] = full2half(c[i]); 77 | } 78 | return new String(c); 79 | } 80 | } -------------------------------------------------------------------------------- /src/com/obroom/filenamesort/util/FileNameComparator.java: -------------------------------------------------------------------------------- 1 | package com.obroom.filenamesort.util; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Comparator; 5 | import java.util.HashMap; 6 | import java.util.Iterator; 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.regex.Matcher; 10 | import java.util.regex.Pattern; 11 | 12 | /** 13 | * 文件名称采用windows资源管理器的排序算法排列 14 | *

15 | * 参考了网络上别人研究的文章,但没发现可以完全解决Windows文件名的排序 16 | * 后面自己对规则做了大量研究,特别是特殊字符的排序处理 17 | * 目前只把中英文(全半角)所有键盘能直接打出来的特殊字符做了处理 18 | * 未对其他语言做测试 19 | * 中英文的文件名目前测试结果完全一致 20 | * 21 | * @author ob 22 | * @since 20180202 23 | */ 24 | public class FileNameComparator implements Comparator { 25 | private static final Pattern DIGIT_PATTERN = Pattern.compile("^\\d*\\d$");//数字匹配 26 | private static final Pattern LETTER_PATTERN = Pattern.compile("^\\w");//字母匹配 27 | private static final Pattern SPLIT_PATTERN = Pattern.compile("-{0,}\\d{1,}|-{0,}\\D{1}"); 28 | private static final String SPECIAL_STRING = " !\"#$%()*,、.-。/:;?@[\\]^`{|}~‘’“”《》¥【】+<=>×…";//特殊字符(全半角)正则匹配 29 | private static Map sortMap = new HashMap<>();//特殊字符(中英文)内部排序号 30 | 31 | static { 32 | String[] specials = SPECIAL_STRING.split(""); 33 | for (int i = 0, j = 10; i < specials.length; ++i, j += 10) { 34 | sortMap.put(specials[i], j); 35 | } 36 | sortMap.put("\\d", 1000); 37 | } 38 | 39 | /** 40 | * 字符串排序 41 | * 42 | * @param str1 43 | * @param str2 44 | * @return 45 | */ 46 | @Override 47 | public int compare(String str1, String str2) { 48 | int result = 0; 49 | //先把字符串全角(如果有)转成半角 50 | String str3 = AsciiUtil.full2half(str1); 51 | String str4 = AsciiUtil.full2half(str2); 52 | //按正则切割字符 53 | Iterator iter1 = splitString(str3).iterator(); 54 | Iterator iter2 = splitString(str4).iterator(); 55 | //进入比对 56 | while (true) { 57 | if (!iter1.hasNext() && !iter2.hasNext()) { 58 | //全半角字符比对 59 | Iterator iter3 = splitString(str1.replace("-", "")).iterator(); 60 | Iterator iter4 = splitString(str2.replace("-", "")).iterator(); 61 | while (true) { 62 | if (!iter3.hasNext() && !iter4.hasNext()) { 63 | if (str1.contains("-") && str2.contains("-")) { 64 | return -1; 65 | } else if (str1.contains("-") && str2.contains("-")) { 66 | return 1; 67 | } else { 68 | //减号位数和位置比对 69 | Iterator iter5 = splitString(str3).iterator(); 70 | Iterator iter6 = splitString(str4).iterator(); 71 | while (true) { 72 | if (!iter5.hasNext() && !iter6.hasNext()) { 73 | return result; 74 | } 75 | if (!iter5.hasNext() && iter6.hasNext()) { 76 | return -1; 77 | } 78 | if (iter5.hasNext() && !iter6.hasNext()) { 79 | return 1; 80 | } 81 | String str9 = iter5.next(); 82 | String str10 = iter6.next(); 83 | if (str9.contains("-") && !str10.contains("-")) { 84 | return 1; 85 | } else if (!str9.contains("-") && str10.contains("-")) { 86 | return -1; 87 | } else if (str9.contains("-") && str10.contains("-")) { 88 | result = str9.length() - str10.length(); 89 | if (result != 0) { 90 | return result; 91 | } 92 | } 93 | } 94 | } 95 | } 96 | if (!iter3.hasNext() && iter4.hasNext()) { 97 | return -1; 98 | } 99 | if (iter3.hasNext() && !iter4.hasNext()) { 100 | return 1; 101 | } 102 | String str7 = iter3.next().replace("-", "").replace("-", ""); 103 | String str8 = iter4.next().replace("-", "").replace("-", ""); 104 | if (str7.length() == str8.length() && str7.length() == 1) { 105 | char c1 = str7.charAt(0); 106 | char c2 = str8.charAt(0); 107 | if (AsciiUtil.full2half(c1) == AsciiUtil.full2half(c2)) { 108 | if (AsciiUtil.isHalfChar(c1) && AsciiUtil.isFullChar(c2)) { 109 | return -1; 110 | } else if (AsciiUtil.isFullChar(c1) && AsciiUtil.isHalfChar(c2)) { 111 | return 1; 112 | } 113 | } 114 | } 115 | } 116 | } 117 | if (!iter1.hasNext() && iter2.hasNext()) { 118 | return -1; 119 | } 120 | if (iter1.hasNext() && !iter2.hasNext()) { 121 | return 1; 122 | } 123 | String str5 = iter1.next().replace("-", ""); 124 | String str6 = iter2.next().replace("-", ""); 125 | try { 126 | //如果都是数字,按大小比对 127 | result = Long.compare(Long.valueOf(str5), Long.valueOf(str6)); 128 | //如果数值相等,按位数大小比对 129 | if (result == 0) { 130 | if (str3.contains(str5) && !str4.contains(str6)) { 131 | result = -1; 132 | } else if (!str3.contains(str5) && str4.contains(str6)) { 133 | result = -1; 134 | } else { 135 | result = str6.length() - str5.length(); 136 | } 137 | } 138 | } catch (NumberFormatException ex) { 139 | //非数字比对,判断是否特殊字符,采用特殊字符排序,否则采用常规字符排序 140 | boolean isDigit1 = DIGIT_PATTERN.matcher(str5).find(); 141 | boolean isDigit2 = DIGIT_PATTERN.matcher(str6).find(); 142 | if (isDigit1 && sortMap.containsKey(str6)) { 143 | result = 1; 144 | } else if (isDigit2 && sortMap.containsKey(str5)) { 145 | result = -1; 146 | } else if (sortMap.containsKey(str5) && sortMap.containsKey(str6)) { 147 | result = sortMap.get(str5) - sortMap.get(str6); 148 | } else { 149 | //字母比对 150 | boolean isLetter1 = LETTER_PATTERN.matcher(str5).find(); 151 | boolean isLetter2 = LETTER_PATTERN.matcher(str6).find(); 152 | if (isLetter1 && sortMap.containsKey(str6)) { 153 | result = 1; 154 | } else if (isLetter2 && sortMap.containsKey(str5)) { 155 | result = -1; 156 | } else { 157 | //如果是中文汉字,采用拼音顺序比对 158 | if (isChinese(str5) && isChinese(str6)) { 159 | result = str6.compareToIgnoreCase(str5); 160 | } else { 161 | result = str5.compareToIgnoreCase(str6); 162 | } 163 | } 164 | } 165 | } 166 | if (result != 0) { 167 | return result; 168 | } 169 | } 170 | } 171 | 172 | /** 173 | * 字符串按特殊字符正则切割 174 | * 175 | * @param str 176 | * @return 177 | */ 178 | public static List splitString(String str) { 179 | Matcher matcher = SPLIT_PATTERN.matcher(str); 180 | List list = new ArrayList<>(); 181 | int pos = 0; 182 | while (matcher.find()) { 183 | list.add(matcher.group()); 184 | pos = matcher.end(); 185 | } 186 | list.add(str.substring(pos)); 187 | return list; 188 | } 189 | 190 | /** 191 | * 判断字符串是否包含中文字符 192 | * 193 | * @param str 194 | * @return 195 | */ 196 | public static boolean isChinese(String str) { 197 | if (str == null) { 198 | return false; 199 | } 200 | Pattern pattern = Pattern.compile("[\\u4E00-\\u9FBF]+"); 201 | return pattern.matcher(str.trim()).find(); 202 | } 203 | } --------------------------------------------------------------------------------