diff --git a/app/src/main/java/com/hippo/util/NaturalComparator.java b/app/src/main/java/com/hippo/util/NaturalComparator.java new file mode 100644 index 000000000..6e126f5a5 --- /dev/null +++ b/app/src/main/java/com/hippo/util/NaturalComparator.java @@ -0,0 +1,114 @@ +/* + * Copyright 2018 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.util; + +import java.util.Comparator; + +/** + * Implements natural sort order. + */ +public class NaturalComparator implements Comparator { + + @Override + public int compare(String o1, String o2) { + if (o1 == null && o2 == null) { + return 0; + } + if (o1 == null) { + return -1; + } + if (o2 == null) { + return 1; + } + + int index1 = 0; + int index2 = 0; + while (true) { + String data1 = nextSlice(o1, index1); + String data2 = nextSlice(o2, index2); + + if (data1 == null && data2 == null) { + return 0; + } + if (data1 == null) { + return -1; + } + if (data2 == null) { + return 1; + } + + index1 += data1.length(); + index2 += data2.length(); + + int result; + if (isDigit(data1) && isDigit(data2)) { + result = Long.compare(Long.valueOf(data1), Long.valueOf(data2)); + if (result == 0) { + result = -Integer.compare(data1.length(), data2.length()); + } + } else { + result = data1.compareToIgnoreCase(data2); + } + + if (result != 0) { + return result; + } + } + } + + private static boolean isDigit(String str) { + // Just check the first char + char ch = str.charAt(0); + return ch >= '0' && ch <= '9'; + } + + private static String nextSlice(String str, int index) { + int length = str.length(); + if (index == length) { + return null; + } + + char ch = str.charAt(index); + if (ch == '.' || ch == ' ') { + return str.substring(index, index + 1); + } else if (ch >= '0' && ch <= '9') { + return str.substring(index, nextNumberBound(str, index + 1)); + } else { + return str.substring(index, nextOtherBound(str, index + 1)); + } + } + + private static int nextNumberBound(String str, int index) { + for (int length = str.length(); index < length; index++) { + char ch = str.charAt(index); + if (ch < '0' || ch > '9') { + break; + } + } + return index; + } + + private static int nextOtherBound(String str, int index) { + for (int length = str.length(); index < length; index++) { + char ch = str.charAt(index); + if (ch == '.' || ch == ' ' || (ch >= '0' && ch <= '9')) { + break; + } + } + return index; + } +} diff --git a/app/src/main/java/com/hippo/util/PathNaturalComparator.java b/app/src/main/java/com/hippo/util/PathNaturalComparator.java new file mode 100644 index 000000000..62d15a39f --- /dev/null +++ b/app/src/main/java/com/hippo/util/PathNaturalComparator.java @@ -0,0 +1,104 @@ +package com.hippo.util; + +import java.util.Comparator; + +/** + * Compares each path segment in natural sort order. + * {@code \} and {@code /} are the same. Duplicate {@code \} and {@code /} are ignored. + * Leading {@code \} and {@code /} are ignored, so there is no difference between + * relative path and absolute path. + * {@code /./} and {@code /../} are treated as normal path. + */ +public class PathNaturalComparator implements Comparator { + + private static final boolean TYPE_SEPARATOR = false; + private static final boolean TYPE_NORMAL = true; + + private Comparator naturalComparator = new NaturalComparator(); + + @Override + public int compare(String o1, String o2) { + if (o1 == null && o2 == null) { + return 0; + } + if (o1 == null) { + return -1; + } + if (o2 == null) { + return 1; + } + + int index1 = 0; + int index2 = 0; + + while (true) { + String data1; + String data2; + + for (;;) { + int newIndex1 = nextSegmentStart(o1, index1); + if (newIndex1 == index1) { + data1 = null; + break; + } + if (getType(o1.charAt(newIndex1 - 1)) == TYPE_NORMAL) { + data1 = o1.substring(index1, newIndex1); + index1 = newIndex1; + break; + } + index1 = newIndex1; + } + + for (;;) { + int newIndex2 = nextSegmentStart(o2, index2); + if (newIndex2 == index2) { + data2 = null; + break; + } + if (getType(o2.charAt(newIndex2 - 1)) == TYPE_NORMAL) { + data2 = o2.substring(index2, newIndex2); + index2 = newIndex2; + break; + } + index2 = newIndex2; + } + + if (data1 == null && data2 == null) { + return 0; + } + if (data1 == null) { + return -1; + } + if (data2 == null) { + return 1; + } + + int result = naturalComparator.compare(data1, data2); + if (result != 0) { + return result; + } + } + } + + private static int nextSegmentStart(String str, int index) { + if (index >= str.length()) { + return index; + } + + boolean type = getType(str.charAt(index)); + int i = index + 1; + while (i < str.length() && type == getType(str.charAt(i))) { + i += 1; + } + + return i; + } + + private static boolean getType(char c) { + if (c == '/' || c == '\\') { + return TYPE_SEPARATOR; + } else { + return TYPE_NORMAL; + } + } +} diff --git a/app/src/test/java/com/hippo/util/NaturalComparatorTest.java b/app/src/test/java/com/hippo/util/NaturalComparatorTest.java new file mode 100644 index 000000000..4137ec2e8 --- /dev/null +++ b/app/src/test/java/com/hippo/util/NaturalComparatorTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 2018 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.util; + +import static org.junit.Assert.assertTrue; + +import org.junit.Assert; +import org.junit.Test; + +public class NaturalComparatorTest { + + @Test + public void testOrder() { + assertOrder("1", "2"); + assertOrder("2", "11"); + assertOrder("2", "00011"); + + assertOrder("a2", "a11"); + assertOrder("a2a", "a11a"); + } + + @Test + public void testEquals() { + assertEquals("2", "2"); + assertEquals("11", "11"); + assertEquals("a11", "a11"); + assertEquals("a2", "a2"); + assertEquals("a2a", "a2a"); + } + + @Test + public void testEmpty() { + assertOrder("", "1"); + assertEquals("", ""); + } + + @Test + public void testNull() { + assertEquals(null, null); + assertOrder(null, "1"); + } + + private void assertOrder(String s1, String s2) { + NaturalComparator comparator = new NaturalComparator(); + assertTrue(comparator.compare(s1, s2) < 0); + assertTrue(comparator.compare(s2, s1) > 0); + } + + private void assertEquals(String s1, String s2) { + NaturalComparator comparator = new NaturalComparator(); + Assert.assertEquals(0, comparator.compare(s1, s2)); + Assert.assertEquals(0, comparator.compare(s2, s1)); + } +} diff --git a/app/src/test/java/com/hippo/util/PathNaturalComparatorTest.java b/app/src/test/java/com/hippo/util/PathNaturalComparatorTest.java new file mode 100644 index 000000000..0596b1906 --- /dev/null +++ b/app/src/test/java/com/hippo/util/PathNaturalComparatorTest.java @@ -0,0 +1,65 @@ +/* + * Copyright 2018 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.util; + +import static org.junit.Assert.assertTrue; + +import org.junit.Assert; +import org.junit.Test; + +public class PathNaturalComparatorTest { + + @Test + public void testOrder() { + assertOrder("ab/d", "abcd"); + assertOrder("ab\\d", "abcd"); + + assertOrder("ab/d2", "ab/d11"); + } + + @Test + public void testEquals() { + assertEquals("/abc", "abc"); + assertEquals("/abc", "\\abc"); + assertEquals("abc", "\\abc"); + assertEquals("abc///", "abc"); + } + + @Test + public void testEmpty() { + assertOrder("", "1"); + assertEquals("", ""); + } + + @Test + public void testNull() { + assertEquals(null, null); + assertOrder(null, "1"); + } + + private void assertOrder(String s1, String s2) { + PathNaturalComparator comparator = new PathNaturalComparator(); + assertTrue(comparator.compare(s1, s2) < 0); + assertTrue(comparator.compare(s2, s1) > 0); + } + + private void assertEquals(String s1, String s2) { + PathNaturalComparator comparator = new PathNaturalComparator(); + Assert.assertEquals(0, comparator.compare(s1, s2)); + Assert.assertEquals(0, comparator.compare(s2, s1)); + } +}