mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
extract naturalCompare() into separate class
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright 2000-2017 JetBrains s.r.o.
|
||||
*
|
||||
* 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.intellij.openapi.util.text;
|
||||
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
/**
|
||||
* Implementation of <a href="http://www.codinghorror.com/blog/2007/12/sorting-for-humans-natural-sort-order.html"/>
|
||||
* "Sorting for Humans: Natural Sort Order"</a>
|
||||
* @author Bas Leijdekkers
|
||||
*/
|
||||
public class NaturalComparator implements Comparator<String> {
|
||||
|
||||
@Override
|
||||
public int compare(String s1, String s2) {
|
||||
//noinspection StringEquality
|
||||
if (s1 == s2) return 0;
|
||||
if (s1 == null) return -1;
|
||||
if (s2 == null) return +1;
|
||||
return naturalCompare(s1, s2, s1.length(), s2.length(), true);
|
||||
}
|
||||
|
||||
@Contract(pure = true)
|
||||
private static int naturalCompare(@NotNull String s1, @NotNull String s2, int length1, int length2, boolean ignoreCase) {
|
||||
int i = 0;
|
||||
int j = 0;
|
||||
for (; i < length1 && j < length2; i++, j++) {
|
||||
final char ch1 = s1.charAt(i);
|
||||
final char ch2 = s2.charAt(j);
|
||||
if ((StringUtil.isDecimalDigit(ch1) || ch1 == ' ') && (StringUtil.isDecimalDigit(ch2) || ch2 == ' ')) {
|
||||
final int start1 = skipChar(s1, skipChar(s1, i, length1, ' '), length1, '0');
|
||||
final int start2 = skipChar(s2, skipChar(s2, j, length2, ' '), length2, '0');
|
||||
|
||||
final int end1 = skipDigits(s1, start1, length1);
|
||||
final int end2 = skipDigits(s2, start2, length2);
|
||||
|
||||
// numbers with more digits are always greater than shorter numbers
|
||||
final int lengthDiff = (end1 - start1) - (end2 - start2);
|
||||
if (lengthDiff != 0) return lengthDiff;
|
||||
|
||||
// compare numbers with equal digit count
|
||||
final int numberDiff = compareCharRange(s1, s2, start1, start2, end1);
|
||||
if (numberDiff != 0) return numberDiff;
|
||||
|
||||
// compare number length including leading spaces and zeroes
|
||||
final int fullLengthDiff = (end1 - i) - (end2 - j);
|
||||
if (fullLengthDiff != 0) return fullLengthDiff;
|
||||
|
||||
// numbers are same, compare leading spaces and zeroes
|
||||
final int leadingDiff = compareCharRange(s1, s2, i, j, start1);
|
||||
if (leadingDiff != 0) return leadingDiff;
|
||||
|
||||
i = end1 - 1;
|
||||
j = end2 - 1;
|
||||
}
|
||||
else {
|
||||
final int diff = compareChars(ch1, ch2, ignoreCase);
|
||||
if (diff != 0) return diff;
|
||||
}
|
||||
}
|
||||
// After the loop the end of one of the strings might not have been reached, if the other
|
||||
// string ends with a number and the strings are equal until the end of that number. When
|
||||
// there are more characters in the string, then it is greater.
|
||||
if (i < length1) return +1;
|
||||
if (j < length2) return -1;
|
||||
if (length1 != length2) return length1 - length2;
|
||||
|
||||
// do case sensitive compare if case insensitive strings are equal
|
||||
return ignoreCase ? naturalCompare(s1, s2, length1, length2, false) : 0;
|
||||
}
|
||||
|
||||
private static int compareCharRange(@NotNull String s1, @NotNull String s2, int offset1, int offset2, int end1) {
|
||||
for (int i = offset1, j = offset2; i < end1; i++, j++) {
|
||||
final int diff = s1.charAt(i) - s2.charAt(j);
|
||||
if (diff != 0) return diff;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int compareChars(char ch1, char ch2, boolean ignoreCase) {
|
||||
// transitivity fix, otherwise can fail when comparing strings with characters between ' ' and '0' (e.g. '#')
|
||||
if (ch1 == ' ' && ch2 > ' ' && ch2 < '0') return +1;
|
||||
if (ch2 == ' ' && ch1 > ' ' && ch1 < '0') return -1;
|
||||
return StringUtil.compare(ch1, ch2, ignoreCase);
|
||||
}
|
||||
|
||||
private static int skipDigits(String s, int start, int end) {
|
||||
while (start < end && StringUtil.isDecimalDigit(s.charAt(start))) start++;
|
||||
return start;
|
||||
}
|
||||
|
||||
private static int skipChar(String s, int start, int end, char c) {
|
||||
while (start < end && s.charAt(start) == c) start++;
|
||||
return start;
|
||||
}
|
||||
}
|
||||
@@ -2715,116 +2715,11 @@ public class StringUtil extends StringUtilRt {
|
||||
return res;
|
||||
}
|
||||
|
||||
public static final Comparator<String> NATURAL_COMPARATOR = new Comparator<String>() {
|
||||
@Override
|
||||
public int compare(String o1, String o2) {
|
||||
return naturalCompare(o1, o2);
|
||||
}
|
||||
};
|
||||
public static final Comparator<String> NATURAL_COMPARATOR = new NaturalComparator();
|
||||
|
||||
/**
|
||||
* Implementation of <a href="http://www.codinghorror.com/blog/2007/12/sorting-for-humans-natural-sort-order.html"/>
|
||||
* "Sorting for Humans: Natural Sort Order"</a>
|
||||
*/
|
||||
@Contract(pure = true)
|
||||
public static int naturalCompare(@Nullable String string1, @Nullable String string2) {
|
||||
return naturalCompare(string1, string2, false);
|
||||
}
|
||||
|
||||
@Contract(pure = true)
|
||||
private static int naturalCompare(@Nullable String string1, @Nullable String string2, boolean caseSensitive) {
|
||||
//noinspection StringEquality
|
||||
if (string1 == string2) {
|
||||
return 0;
|
||||
}
|
||||
if (string1 == null) {
|
||||
return -1;
|
||||
}
|
||||
if (string2 == null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
final int string1Length = string1.length();
|
||||
final int string2Length = string2.length();
|
||||
int i = 0;
|
||||
int j = 0;
|
||||
for (; i < string1Length && j < string2Length; i++, j++) {
|
||||
char ch1 = string1.charAt(i);
|
||||
char ch2 = string2.charAt(j);
|
||||
if ((isDecimalDigit(ch1) || ch1 == ' ') && (isDecimalDigit(ch2) || ch2 == ' ')) {
|
||||
final int offset1 = i;
|
||||
while (ch1 == ' ' || ch1 == '0') { // skip leading spaces and zeros
|
||||
i++;
|
||||
if (i >= string1Length) break;
|
||||
ch1 = string1.charAt(i);
|
||||
}
|
||||
final int offset2 = j;
|
||||
while (ch2 == ' ' || ch2 == '0') { // skip leading spaces and zeros
|
||||
j++;
|
||||
if (j >= string2Length) break;
|
||||
ch2 = string2.charAt(j);
|
||||
}
|
||||
int adjustedOffset1 = i;
|
||||
int adjustedOffset2 = j;
|
||||
// find end index of number
|
||||
while (i < string1Length && isDecimalDigit(string1.charAt(i))) i++;
|
||||
while (j < string2Length && isDecimalDigit(string2.charAt(j))) j++;
|
||||
final int lengthDiff = (i - adjustedOffset1) - (j - adjustedOffset2);
|
||||
if (lengthDiff != 0) {
|
||||
// numbers with more digits are always greater than shorter numbers
|
||||
return lengthDiff;
|
||||
}
|
||||
for (; adjustedOffset1 < i; adjustedOffset1++, adjustedOffset2++) {
|
||||
// compare numbers with equal digit count
|
||||
final int diff = string1.charAt(adjustedOffset1) - string2.charAt(adjustedOffset2);
|
||||
if (diff != 0) {
|
||||
return diff;
|
||||
}
|
||||
}
|
||||
final int realLengthDiff = (i - offset1) - (j - offset2);
|
||||
if (realLengthDiff != 0) {
|
||||
// compare number length including leading spaces and zeroes
|
||||
return realLengthDiff;
|
||||
}
|
||||
i--;
|
||||
j--;
|
||||
}
|
||||
else {
|
||||
// transitivity fix, can otherwise fail when comparing strings with characters between ' ' and '0' (e.g. '#')
|
||||
if (ch1 == ' ') ch1 = '0';
|
||||
if (ch2 == ' ') ch2 = '0';
|
||||
|
||||
if (caseSensitive) {
|
||||
return ch1 - ch2;
|
||||
}
|
||||
else {
|
||||
// similar logic to charsMatch() below
|
||||
if (ch1 != ch2) {
|
||||
final int diff1 = StringUtilRt.toUpperCase(ch1) - StringUtilRt.toUpperCase(ch2);
|
||||
if (diff1 != 0) {
|
||||
final int diff2 = StringUtilRt.toLowerCase(ch1) - StringUtilRt.toLowerCase(ch2);
|
||||
if (diff2 != 0) {
|
||||
return diff2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// After the loop the end of one of the strings might not have been reached, if the other
|
||||
// string ends with a number and the strings are equal until the end of that number. When
|
||||
// there are more characters in the string, then it is greater.
|
||||
if (i < string1Length) {
|
||||
return 1;
|
||||
}
|
||||
if (j < string2Length) {
|
||||
return -1;
|
||||
}
|
||||
if (!caseSensitive && string1Length == string2Length) {
|
||||
// do case sensitive compare if case insensitive strings are equal
|
||||
return naturalCompare(string1, string2, true);
|
||||
}
|
||||
return string1Length - string2Length;
|
||||
return NATURAL_COMPARATOR.compare(string1, string2);
|
||||
}
|
||||
|
||||
@Contract(pure = true)
|
||||
|
||||
@@ -171,38 +171,46 @@ public class StringUtilTest {
|
||||
assertTrue(StringUtil.naturalCompare(s1, s2) < 0);
|
||||
assertTrue(StringUtil.naturalCompare(s2, s3) < 0);
|
||||
assertTrue("non-transitive", StringUtil.naturalCompare(s1, s3) < 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNaturalCompareStability() {
|
||||
assertTrue(StringUtil.naturalCompare("01a1", "1a01") != StringUtil.naturalCompare("1a01", "01a1"));
|
||||
assertTrue(StringUtil.naturalCompare("#01A", "# 1A") != StringUtil.naturalCompare("# 1A", "#01A"));
|
||||
assertTrue(StringUtil.naturalCompare("aA", "aa") != StringUtil.naturalCompare("aa", "aA"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNaturalCompare() {
|
||||
final Comparator<String> c = StringUtil::naturalCompare;
|
||||
|
||||
final List<String> numbers = Arrays.asList("1a000001", "000001a1", "001a0001", "0001A001" , "00001a01", "01a00001");
|
||||
numbers.sort(c);
|
||||
numbers.sort(StringUtil.NATURAL_COMPARATOR);
|
||||
assertEquals(Arrays.asList("1a000001", "01a00001", "001a0001", "0001A001" , "00001a01", "000001a1"), numbers);
|
||||
|
||||
final List<String> test = Arrays.asList("test011", "test10", "test10a", "test010");
|
||||
test.sort(c);
|
||||
test.sort(StringUtil.NATURAL_COMPARATOR);
|
||||
assertEquals(Arrays.asList("test10", "test10a", "test010", "test011"), test);
|
||||
|
||||
final List<String> strings = Arrays.asList("Test99", "tes0", "test0", "testing", "test", "test99", "test011", "test1",
|
||||
"test 3", "test2", "test10a", "test10", "1.2.10.5", "1.2.9.1");
|
||||
strings.sort(c);
|
||||
strings.sort(StringUtil.NATURAL_COMPARATOR);
|
||||
assertEquals(Arrays.asList("1.2.9.1", "1.2.10.5", "tes0", "test", "test0", "test1", "test2", "test 3", "test10", "test10a",
|
||||
"test011", "Test99", "test99", "testing"), strings);
|
||||
|
||||
final List<String> strings2 = Arrays.asList("t1", "t001", "T2", "T002", "T1", "t2");
|
||||
strings2.sort(c);
|
||||
strings2.sort(StringUtil.NATURAL_COMPARATOR);
|
||||
assertEquals(Arrays.asList("T1", "t1", "t001", "T2", "t2", "T002"), strings2);
|
||||
assertEquals(1 ,StringUtil.naturalCompare("7403515080361171695", "07403515080361171694"));
|
||||
assertEquals(-14, StringUtil.naturalCompare("_firstField", "myField1"));
|
||||
//idea-80853
|
||||
final List<String> strings3 =
|
||||
Arrays.asList("C148A_InsomniaCure", "C148B_Escape", "C148C_TersePrincess", "C148D_BagOfMice", "C148E_Porcelain");
|
||||
strings3.sort(c);
|
||||
strings3.sort(StringUtil.NATURAL_COMPARATOR);
|
||||
assertEquals(Arrays.asList("C148A_InsomniaCure", "C148B_Escape", "C148C_TersePrincess", "C148D_BagOfMice", "C148E_Porcelain"), strings3);
|
||||
|
||||
final List<String> l = Arrays.asList("a0002", "a0 2", "a001");
|
||||
l.sort(StringUtil.NATURAL_COMPARATOR);
|
||||
assertEquals(Arrays.asList("a0 2", "a001", "a0002"), l);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user