From b1a30e4bc342ef397a3de79eb2756cde42a6951b Mon Sep 17 00:00:00 2001 From: Reinhard Pointner Date: Wed, 28 Sep 2016 13:19:20 +0800 Subject: [PATCH] List files in human order (Original Files area only) @see https://www.filebot.net/forums/viewtopic.php?f=10&t=4174 @see http://stackoverflow.com/questions/12640280/looking-for-a-combination-of-alphabetical-and-natural-order-aka-user-sane-sort --- .../filebot/ui/rename/EpisodeListMatcher.java | 4 +- .../rename/FilesListTransferablePolicy.java | 18 ++- .../sfv/ChecksumTableTransferablePolicy.java | 4 +- .../net/filebot/util/AlphanumComparator.java | 105 ++++++++++++++++++ source/net/filebot/util/FileUtilities.java | 60 +++------- 5 files changed, 129 insertions(+), 62 deletions(-) create mode 100644 source/net/filebot/util/AlphanumComparator.java diff --git a/source/net/filebot/ui/rename/EpisodeListMatcher.java b/source/net/filebot/ui/rename/EpisodeListMatcher.java index a481f264..66fd2c83 100644 --- a/source/net/filebot/ui/rename/EpisodeListMatcher.java +++ b/source/net/filebot/ui/rename/EpisodeListMatcher.java @@ -297,7 +297,7 @@ class EpisodeListMatcher implements AutoCompleteMatcher { } protected String getQueryInputMessage(String header, String message, Collection files) throws Exception { - List selection = files.stream().sorted(comparing(File::length).reversed()).limit(5).collect(toList()); + List selection = files.stream().sorted(comparing(File::length).reversed()).limit(5).sorted(HUMAN_ORDER).collect(toList()); if (selection.isEmpty()) { return ""; } @@ -309,7 +309,7 @@ class EpisodeListMatcher implements AutoCompleteMatcher { } TextColorizer colorizer = new TextColorizer("• ", "
"); - for (File file : sortByUniquePath(selection)) { + for (File file : selection) { File path = getStructurePathTail(file); if (path == null) { path = getRelativePathTail(file, 3); diff --git a/source/net/filebot/ui/rename/FilesListTransferablePolicy.java b/source/net/filebot/ui/rename/FilesListTransferablePolicy.java index d7cacfb5..2d573c7f 100644 --- a/source/net/filebot/ui/rename/FilesListTransferablePolicy.java +++ b/source/net/filebot/ui/rename/FilesListTransferablePolicy.java @@ -5,14 +5,15 @@ import static java.util.stream.Collectors.*; import static net.filebot.Logging.*; import static net.filebot.MediaTypes.*; import static net.filebot.util.FileUtilities.*; +import static net.filebot.util.RegularExpressions.*; import java.awt.datatransfer.Transferable; import java.io.File; import java.util.Collection; -import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.TreeSet; import java.util.logging.Level; import net.filebot.media.MediaDetection; @@ -50,26 +51,21 @@ class FilesListTransferablePolicy extends BackgroundFileTransferablePolicy @Override protected void load(List files, TransferAction action) { - Set fileset = new LinkedHashSet(); + Set fileset = new TreeSet(CASE_INSENSITIVE_ORDER); // load files recursively by default load(files, action != TransferAction.LINK, fileset); - // use fast file to minimize system calls like length(), isDirectory(), isFile(), ... - publish(fileset.stream().map(FastFile::new).toArray(File[]::new)); + // use fast file to minimize system calls like length(), isDirectory(), isFile(), ... and list files in human order + publish(fileset.stream().sorted(HUMAN_ORDER).map(FastFile::new).toArray(File[]::new)); } private void load(List files, boolean recursive, Collection sink) { for (File f : files) { - // ignore hidden files - if (f.isHidden()) { - continue; - } - // load file paths from text files if (recursive && LIST_FILES.accept(f)) { try { - String[] lines = readTextFile(f).split("\\R"); + String[] lines = NEWLINE.split(readTextFile(f)); List paths = stream(lines).filter(s -> s.length() > 0).map(path -> { try { File file = new File(path); @@ -96,7 +92,7 @@ class FilesListTransferablePolicy extends BackgroundFileTransferablePolicy // load folders recursively else if (f.isDirectory()) { - load(sortByUniquePath(getChildren(f)), true, sink); // FORCE NATURAL FILE ORDER + load(getChildren(f, NOT_HIDDEN), true, sink); // FORCE NATURAL FILE ORDER } } } diff --git a/source/net/filebot/ui/sfv/ChecksumTableTransferablePolicy.java b/source/net/filebot/ui/sfv/ChecksumTableTransferablePolicy.java index 7a3aacb6..9a5dfb58 100644 --- a/source/net/filebot/ui/sfv/ChecksumTableTransferablePolicy.java +++ b/source/net/filebot/ui/sfv/ChecksumTableTransferablePolicy.java @@ -96,7 +96,7 @@ class ChecksumTableTransferablePolicy extends BackgroundFileTransferablePolicy { + + protected Collator collator; + + public AlphanumComparator(Collator collator) { + this.collator = collator; + } + + public AlphanumComparator(Locale locale) { + this.collator = Collator.getInstance(locale); + this.collator.setDecomposition(Collator.FULL_DECOMPOSITION); + this.collator.setStrength(Collator.PRIMARY); + } + + protected boolean isDigit(String s, int i) { + return Character.isDigit(s.charAt(i)); + } + + protected int getNumericValue(String s, int i) { + return Character.getNumericValue(s.charAt(i)); + } + + protected String getChunk(String s, int start) { + int index = start; + int length = s.length(); + boolean mode = isDigit(s, index++); + + while (index < length) { + if (mode != isDigit(s, index)) { + break; + } + + ++index; + } + + return s.substring(start, index); + } + + public int compare(String s1, String s2) { + int length1 = s1.length(); + int length2 = s2.length(); + int index1 = 0; + int index2 = 0; + int result = 0; + + while (result == 0 && index1 < length1 && index2 < length2) { + String chunk1 = getChunk(s1, index1); + index1 += chunk1.length(); + + String chunk2 = getChunk(s2, index2); + index2 += chunk2.length(); + + if (isDigit(chunk1, 0) && isDigit(chunk2, 0)) { + int chunkLength1 = chunk1.length(); + int chunkLength2 = chunk2.length(); + + // count and skip leading zeros + int zeroIndex1 = 0; + while (zeroIndex1 < chunkLength1 && getNumericValue(chunk1, zeroIndex1) == 0) { + ++zeroIndex1; + } + + // count and skip leading zeros + int zeroIndex2 = 0; + while (zeroIndex2 < chunkLength2 && getNumericValue(chunk2, zeroIndex2) == 0) { + ++zeroIndex2; + } + + // the longer run of non-zero digits is greater + result = (chunkLength1 - zeroIndex1) - (chunkLength2 - zeroIndex2); + + // if the length is the same, the first differing digit decides + // which one is deemed greater. + int numberIndex1 = zeroIndex1; + int numberIndex2 = zeroIndex2; + + while (result == 0 && numberIndex1 < chunkLength1 && numberIndex2 < chunkLength2) { + result = getNumericValue(chunk1, numberIndex1++) - getNumericValue(chunk2, numberIndex2++); + } + + // if still no difference, the longer zeros-prefix is greater + if (result == 0) { + result = numberIndex1 - numberIndex2; + } + } else { + result = collator.compare(chunk1, chunk2); + } + } + + // if there was no difference at all, let the longer one be the greater one + if (result == 0) { + result = length1 - length2; + } + + // limit result to (-1, 0, or 1) + return Integer.signum(result); + } + +} \ No newline at end of file diff --git a/source/net/filebot/util/FileUtilities.java b/source/net/filebot/util/FileUtilities.java index 3e0337cb..d124fffa 100644 --- a/source/net/filebot/util/FileUtilities.java +++ b/source/net/filebot/util/FileUtilities.java @@ -3,6 +3,7 @@ package net.filebot.util; import static java.nio.charset.StandardCharsets.*; import static java.util.Arrays.*; import static java.util.Collections.*; +import static java.util.Comparator.*; import static java.util.stream.Collectors.*; import static net.filebot.Logging.*; import static net.filebot.util.RegularExpressions.*; @@ -39,6 +40,7 @@ import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.SortedMap; import java.util.TreeMap; @@ -363,7 +365,7 @@ public final class FileUtilities { public static List sortByUniquePath(Collection files) { // sort by unique lower-case paths - TreeSet sortedSet = new TreeSet(CASE_INSENSITIVE_PATH); + TreeSet sortedSet = new TreeSet(CASE_INSENSITIVE_ORDER); sortedSet.addAll(files); return new ArrayList(sortedSet); @@ -385,7 +387,7 @@ public final class FileUtilities { } public static FileFilter not(FileFilter filter) { - return new NotFileFilter(filter); + return f -> !filter.accept(f); } public static List listPath(File file) { @@ -436,14 +438,14 @@ public final class FileUtilities { return getChildren(folder, filter, null); } - public static List getChildren(File folder, FileFilter filter, Comparator sorter) { + public static List getChildren(File folder, FileFilter filter, Comparator order) { File[] files = filter == null ? folder.listFiles() : folder.listFiles(filter); // children array may be null if folder permissions do not allow listing of files if (files == null) { files = new File[0]; - } else if (sorter != null) { - sort(files, sorter); + } else if (order != null) { + sort(files, order); } return asList(files); @@ -667,21 +669,11 @@ public final class FileUtilities { return String.format("%,d bytes", size); } - public static final FileFilter FOLDERS = new FileFilter() { + public static final FileFilter FOLDERS = File::isDirectory; - @Override - public boolean accept(File file) { - return file.isDirectory(); - } - }; + public static final FileFilter FILES = File::isFile; - public static final FileFilter FILES = new FileFilter() { - - @Override - public boolean accept(File file) { - return file.isFile(); - } - }; + public static final FileFilter NOT_HIDDEN = not(File::isHidden); public static final FileFilter TEMPORARY = new FileFilter() { @@ -689,15 +681,7 @@ public final class FileUtilities { @Override public boolean accept(File file) { - return file.getAbsolutePath().startsWith(tmpdir); - } - }; - - public static final FileFilter NOT_HIDDEN = new FileFilter() { - - @Override - public boolean accept(File file) { - return !file.isHidden(); + return file.getPath().startsWith(tmpdir); } }; @@ -801,27 +785,9 @@ public final class FileUtilities { } } - public static class NotFileFilter implements FileFilter { + public static final Comparator CASE_INSENSITIVE_ORDER = comparing(File::getPath, String.CASE_INSENSITIVE_ORDER); - public FileFilter filter; - - public NotFileFilter(FileFilter filter) { - this.filter = filter; - } - - @Override - public boolean accept(File file) { - return !filter.accept(file); - } - } - - public static final Comparator CASE_INSENSITIVE_PATH = new Comparator() { - - @Override - public int compare(File o1, File o2) { - return o1.getPath().compareToIgnoreCase(o2.getPath()); - } - }; + public static final Comparator HUMAN_ORDER = comparing(File::getPath, new AlphanumComparator(Locale.ENGLISH)); /** * Dummy constructor to prevent instantiation.