From 9ce2dc1699363d09961fe1afbc5ab7fc4f4a3ff9 Mon Sep 17 00:00:00 2001 From: Bas Leijdekkers Date: Tue, 9 May 2017 15:36:43 +0200 Subject: [PATCH] extract naturalCompare() into separate class --- .../openapi/util/text/NaturalComparator.java | 112 ++++++++++++++++++ .../openapi/util/text/StringUtil.java | 109 +---------------- .../intellij/util/text/StringUtilTest.java | 20 +++- 3 files changed, 128 insertions(+), 113 deletions(-) create mode 100644 platform/util/src/com/intellij/openapi/util/text/NaturalComparator.java diff --git a/platform/util/src/com/intellij/openapi/util/text/NaturalComparator.java b/platform/util/src/com/intellij/openapi/util/text/NaturalComparator.java new file mode 100644 index 000000000000..0e4885c367db --- /dev/null +++ b/platform/util/src/com/intellij/openapi/util/text/NaturalComparator.java @@ -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 + * "Sorting for Humans: Natural Sort Order" + * @author Bas Leijdekkers + */ +public class NaturalComparator implements Comparator { + + @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; + } +} diff --git a/platform/util/src/com/intellij/openapi/util/text/StringUtil.java b/platform/util/src/com/intellij/openapi/util/text/StringUtil.java index db7f1dfe8f46..02b90710df74 100644 --- a/platform/util/src/com/intellij/openapi/util/text/StringUtil.java +++ b/platform/util/src/com/intellij/openapi/util/text/StringUtil.java @@ -2715,116 +2715,11 @@ public class StringUtil extends StringUtilRt { return res; } - public static final Comparator NATURAL_COMPARATOR = new Comparator() { - @Override - public int compare(String o1, String o2) { - return naturalCompare(o1, o2); - } - }; + public static final Comparator NATURAL_COMPARATOR = new NaturalComparator(); - /** - * Implementation of - * "Sorting for Humans: Natural Sort Order" - */ @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) diff --git a/platform/util/testSrc/com/intellij/util/text/StringUtilTest.java b/platform/util/testSrc/com/intellij/util/text/StringUtilTest.java index a676de3541b1..4f4003ec6c18 100644 --- a/platform/util/testSrc/com/intellij/util/text/StringUtilTest.java +++ b/platform/util/testSrc/com/intellij/util/text/StringUtilTest.java @@ -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 c = StringUtil::naturalCompare; final List 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 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 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 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 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 l = Arrays.asList("a0002", "a0 2", "a001"); + l.sort(StringUtil.NATURAL_COMPARATOR); + assertEquals(Arrays.asList("a0 2", "a001", "a0002"), l); } @Test