Merge pull request #2262 from nextcloud/naturalSorting

Add natural sorting
This commit is contained in:
Andy Scherzinger 2018-03-15 12:23:20 +01:00 committed by GitHub
commit aa1f5f8166
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 160 additions and 57 deletions

View file

@ -28,6 +28,7 @@ import android.content.Context;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.v4.content.FileProvider;
import com.owncloud.android.R;
@ -36,7 +37,6 @@ import com.owncloud.android.lib.common.utils.Log_OC;
import com.owncloud.android.utils.MimeType;
import java.io.File;
import java.util.Locale;
import third_parties.daveKoeller.AlphanumComparator;
@ -269,6 +269,18 @@ public class OCFile implements Parcelable, Comparable<OCFile> {
return mMimeType != null && mMimeType.equals(MimeType.DIRECTORY);
}
/**
* Sets mimetype to folder and returns this file
* Only for testing
*
* @return OCFile this file
*/
public OCFile setFolder() {
setMimetype(MimeType.DIRECTORY);
return this;
}
/**
* Use this to check if this file is available locally
*
@ -618,9 +630,9 @@ public class OCFile implements Parcelable, Comparable<OCFile> {
}
@Override
public int compareTo(OCFile another) {
public int compareTo(@NonNull OCFile another) {
if (isFolder() && another.isFolder()) {
return getRemotePath().toLowerCase(Locale.ROOT).compareTo(another.getRemotePath().toLowerCase(Locale.ROOT));
return new AlphanumComparator().compare(this, another);
} else if (isFolder()) {
return -1;
} else if (another.isFolder()) {

View file

@ -27,9 +27,9 @@ package third_parties.daveKoeller;
import com.owncloud.android.datamodel.OCFile;
import java.io.File;
import java.io.Serializable;
import java.text.Collator;
import java.util.Comparator;
import java.util.Locale;
/*
* This is an updated version with enhancements made by Daniel Migowski, Andre Bogus, and David Koelle
@ -47,7 +47,7 @@ import java.util.Locale;
* https://github.com/nextcloud/server/blob/9a4253ef7c34f9dc71a6a9f7828a10df769f0c32/tests/lib/NaturalSortTest.php
* by Tobias Kaminsky
*/
public class AlphanumComparator<T> implements Comparator<T> {
public class AlphanumComparator<T> implements Comparator<T>, Serializable {
private boolean isDigit(char ch) {
return ch >= 48 && ch <= 57;
}
@ -87,15 +87,15 @@ public class AlphanumComparator<T> implements Comparator<T> {
}
public int compare(OCFile o1, OCFile o2) {
String s1 = o1.getRemotePath().toLowerCase(Locale.ROOT);
String s2 = o2.getRemotePath().toLowerCase(Locale.ROOT);
String s1 = o1.getFileName();
String s2 = o2.getFileName();
return compare(s1, s2);
}
public int compare(File f1, File f2) {
String s1 = f1.getPath().toLowerCase(Locale.ROOT);
String s2 = f2.getPath().toLowerCase(Locale.ROOT);
String s1 = f1.getPath();
String s2 = f2.getPath();
return compare(s1, s2);
}
@ -120,17 +120,48 @@ public class AlphanumComparator<T> implements Comparator<T> {
// If both chunks contain numeric characters, sort them numerically
int result = 0;
if (isDigit(thisChunk.charAt(0)) && isDigit(thatChunk.charAt(0))) {
// Simple chunk comparison by length.
int thisChunkLength = thisChunk.length();
result = thisChunkLength - thatChunk.length();
// If equal, the first different number counts
if (result == 0) {
for (int i = 0; i < thisChunkLength; i++) {
result = thisChunk.charAt(i) - thatChunk.charAt(i);
if (result != 0) {
return result;
// extract digits
int thisChunkZeroCount = 0;
boolean zero = true;
int c = 0;
while (c < (thisChunk.length()) && isDigit(thisChunk.charAt(c))) {
if (zero) {
if (Character.getNumericValue(thisChunk.charAt(c)) == 0) {
thisChunkZeroCount++;
} else {
zero = false;
}
}
c++;
}
int thisChunkValue = Integer.parseInt(thisChunk.substring(0, c));
int thatChunkZeroCount = 0;
c = 0;
zero = true;
while (c < (thatChunk.length()) && isDigit(thatChunk.charAt(c))) {
if (zero) {
if (Character.getNumericValue(thatChunk.charAt(c)) == 0) {
thatChunkZeroCount++;
} else {
zero = false;
}
}
c++;
}
int thatChunkValue = Integer.parseInt(thatChunk.substring(0, c));
result = Integer.compare(thisChunkValue, thatChunkValue);
if (result == 0) {
// value is equal, compare leading zeros
result = Integer.compare(thisChunkZeroCount, thatChunkZeroCount);
if (result != 0) {
return result;
}
} else {
return result;
}
} else if (isSpecialChar(thisChunk.charAt(0)) && isSpecialChar(thatChunk.charAt(0))) {
for (int i = 0; i < thisChunk.length(); i++) {

View file

@ -1,9 +1,13 @@
package com.owncloud.android.utils;
import com.owncloud.android.datamodel.OCFile;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import third_parties.daveKoeller.AlphanumComparator;
@ -42,71 +46,127 @@ public class TestSorting {
@Test
public void testSpecialChars() {
String[] sortedArray = {"[Test] Folder", "01 - January", "11 - November", "Ôle",
"Test 1", "Test 01", "Test 04", "Üüü",
"z.[Test], z. Test"};
String[] unsortedArray = {"11 - November", "Test 04", "Test 01", "Ôle", "Üüü", "01 - Januar", "[Test] Folder",
"z.[Test]", "z. Test"};
String[] sortedArray = {"[Test] Folder", "01 - Januar", "11 - November", "Ôle", "Test 01", "Test 04", "Üüü",
"z. Test", "z.[Test]"};
assertTrue(sortAndTest(unsortedArray, sortedArray));
assertTrue(sortAndTest(Arrays.asList(sortedArray)));
}
@Test
public void testDifferentCasing() {
String[] unsortedArray = {"aaa", "bbb", "BBB", "AAA"};
String[] sortedArray = {"aaa", "AAA", "bbb", "BBB"};
assertTrue(sortAndTest(unsortedArray, sortedArray));
assertTrue(sortAndTest(Arrays.asList(sortedArray)));
}
@Test
public void testNumbers() {
String[] unsortedArray = {"124.txt", "abc1", "123.txt", "abc", "abc2", "def (2).txt", "ghi 10.txt", "abc12",
"def.txt", "def (1).txt", "ghi 2.txt", "def (10).txt", "abc10", "def (12).txt", "z", "ghi.txt", "za",
"ghi 1.txt", "ghi 12.txt", "zz", "15.txt", "15b.txt"};
public void testLeadingZeros() {
String[] sortedArray = {"T 0 abc", "T 00 abc", "T 000 abc", "T 1 abc", "T 01 abc",
"T 001 abc", "T 2 abc", "T 02 abc", "T 3 abc", "T 03 abc"};
String[] sortedArray = {"15.txt", "15b.txt", "123.txt", "124.txt", "abc", "abc1", "abc2", "abc10", "abc12",
"def.txt", "def (1).txt", "def (2).txt", "def (10).txt", "def (12).txt", "ghi.txt", "ghi 1.txt",
"ghi 2.txt", "ghi 10.txt", "ghi 12.txt", "z", "za", "zz"};
assertTrue(sortAndTest(unsortedArray, sortedArray));
assertTrue(sortAndTest(Arrays.asList(sortedArray)));
}
@Test
public void testChineseCharacters() {
String[] unsortedArray = {"十.txt", "一.txt", "二.txt", "十 2.txt", "三.txt", "四.txt", "abc.txt", "五.txt",
"七.txt", "八.txt", "九.txt", "六.txt", "十一.txt", "波.txt", "破.txt", "莫.txt", "啊.txt", "123.txt"};
public void testTrailingDigits() {
String[] unsortedArray = {"Zeros 2", "Zeros", "T 2", "T", "T 01", "T 003", "A"};
String[] sortedArray = {"A", "T", "T 01", "T 2", "T 003", "Zeros", "Zeros 2"};
String[] sortedArray = {"123.txt", "abc.txt", "一.txt", "七.txt", "三.txt", "九.txt", "二.txt", "五.txt",
"八.txt", "六.txt", "十.txt", "十 2.txt", "十一.txt", "啊.txt", "四.txt", "波.txt", "破.txt", "莫.txt"};
assertTrue(sortAndTest(unsortedArray, sortedArray));
assertTrue(sortAndTest(Arrays.asList(sortedArray)));
}
@Test
public void testWithUmlauts() {
String[] unsortedArray = {"öh.txt", "Äh.txt", "oh.txt", "Üh 2.txt", "Üh.txt", "ah.txt", "Öh.txt", "uh.txt",
"üh.txt", "äh.txt"};
String[] sortedArray = {"ah.txt", "äh.txt", "Äh.txt", "oh.txt", "öh.txt", "Öh.txt", "uh.txt", "üh.txt",
"Üh.txt", "Üh 2.txt"};
public void testOCFilesWithFolderFirst() {
List<OCFile> sortedArray = new ArrayList<>();
sortedArray.add(new OCFile("/ah.txt").setFolder());
sortedArray.add(new OCFile("/Äh.txt").setFolder());
sortedArray.add(new OCFile("/oh.txt").setFolder());
sortedArray.add(new OCFile("/öh.txt").setFolder());
sortedArray.add(new OCFile("/üh.txt").setFolder());
sortedArray.add(new OCFile("/Üh.txt").setFolder());
sortedArray.add(new OCFile("/äh.txt"));
sortedArray.add(new OCFile("/Öh.txt"));
sortedArray.add(new OCFile("/uh.txt"));
sortedArray.add(new OCFile("/Üh 2.txt"));
assertTrue(sortAndTest(unsortedArray, sortedArray));
assertTrue(sortAndTest(sortedArray));
}
private boolean sortAndTest(String[] unsortedArray, String[] sortedArray) {
List<String> unsortedList = Arrays.asList(unsortedArray);
List<String> sortedList = Arrays.asList(sortedArray);
/**
* uses OCFile.compareTo() instead of custom comparator
*/
@Test
public void testOCFiles() {
List<OCFile> sortedArray = new ArrayList<>();
sortedArray.add(new OCFile("/ah.txt").setFolder());
sortedArray.add(new OCFile("/Äh.txt").setFolder());
sortedArray.add(new OCFile("/oh.txt").setFolder());
sortedArray.add(new OCFile("/öh.txt").setFolder());
sortedArray.add(new OCFile("/üh.txt").setFolder());
sortedArray.add(new OCFile("/Üh.txt").setFolder());
sortedArray.add(new OCFile("/äh.txt"));
sortedArray.add(new OCFile("/Öh.txt"));
sortedArray.add(new OCFile("/uh.txt"));
sortedArray.add(new OCFile("/Üh 2.txt"));
List unsortedList = shuffle(sortedArray);
Collections.sort(unsortedList);
Collections.sort(unsortedList, new AlphanumComparator());
assertTrue(test(sortedArray, unsortedList));
}
for (int i = 0; i < sortedList.size(); i++) {
if (sortedList.get(i).compareTo(unsortedList.get(i)) != 0) {
private List<Comparable> shuffle(List<? extends Comparable> files) {
List<Comparable> shuffled = new ArrayList<>();
shuffled.addAll(files);
System.out.println(" target: " + sortedList.toString());
System.out.println(" actual: " + unsortedList.toString());
Collections.shuffle(shuffled);
return shuffled;
}
private boolean sortAndTest(List<? extends Comparable> sortedList) {
return test(sortedList, sort(sortedList));
}
private List<Comparable> sort(List<? extends Comparable> sortedList) {
List unsortedList = shuffle(sortedList);
if (sortedList.get(0) instanceof OCFile) {
Collections.sort(unsortedList, (Comparator<OCFile>) (o1, o2) -> {
if (o1.isFolder() && o2.isFolder()) {
return new AlphanumComparator().compare(o1, o2);
} else if (o1.isFolder()) {
return -1;
} else if (o2.isFolder()) {
return 1;
}
return new AlphanumComparator().compare(o1, o2);
});
} else {
Collections.sort(unsortedList, new AlphanumComparator<>());
}
return unsortedList;
}
private boolean test(List<? extends Comparable> target, List<? extends Comparable> actual) {
for (int i = 0; i < target.size(); i++) {
int compare;
if (target.get(i) instanceof OCFile) {
String sortedName = ((OCFile) target.get(i)).getFileName();
String unsortedName = ((OCFile) actual.get(i)).getFileName();
compare = sortedName.compareTo(unsortedName);
} else {
compare = target.get(i).compareTo(actual.get(i));
}
if (compare != 0) {
System.out.println(" target: \n" + target.toString());
System.out.println(" actual: \n" + actual.toString());
return false;
}