Index: packages/collection/lib/src/comparators.dart |
diff --git a/packages/collection/lib/src/comparators.dart b/packages/collection/lib/src/comparators.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..05615ba1b9f5f48453533e35cf09d8fcd9d5f78e |
--- /dev/null |
+++ b/packages/collection/lib/src/comparators.dart |
@@ -0,0 +1,399 @@ |
+// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file |
+// for details. All rights reserved. Use of this source code is governed by a |
+// BSD-style license that can be found in the LICENSE file. |
+ |
+library dart.pkg.collection.comparators; |
+ |
+// Character constants. |
+const int _zero = 0x30; |
+const int _upperCaseA = 0x41; |
+const int _upperCaseZ = 0x5a; |
+const int _lowerCaseA = 0x61; |
+const int _lowerCaseZ = 0x7a; |
+const int _asciiCaseBit = 0x20; |
+ |
+/// Checks if strings [a] and [b] differ only on the case of ASCII letters. |
+/// |
+/// Strings are equal if they have the same length, and the characters at |
+/// each index are the same, or they are ASCII letters where one is upper-case |
+/// and the other is the lower-case version of the same letter. |
+/// |
+/// The comparison does not ignore the case of non-ASCII letters, so |
+/// an upper-case ae-ligature (Æ) is different from |
+/// a lower case ae-ligature (æ). |
+/// |
+/// Ignoring non-ASCII letters is not generally a good idea, but it makes sense |
+/// for situations where the strings are known to be ASCII. Examples could |
+/// be Dart identifiers, base-64 or hex encoded strings, GUIDs or similar |
+/// strings with a known structure. |
+bool equalsIgnoreAsciiCase(String a, String b) { |
+ if (a.length != b.length) return false; |
+ for (int i = 0; i < a.length; i++) { |
+ var aChar = a.codeUnitAt(i); |
+ var bChar = b.codeUnitAt(i); |
+ if (aChar == bChar) continue; |
+ // Quick-check for whether this may be different cases of the same letter. |
+ if (aChar ^ bChar != _asciiCaseBit) return false; |
+ // If it's possible, then check if either character is actually an ASCII |
+ // letter. |
+ int aCharUpperCase = aChar | _asciiCaseBit; |
+ if (_upperCaseA <= aCharUpperCase && aCharUpperCase <= _upperCaseZ) { |
+ continue; |
+ } |
+ return false; |
+ } |
+ return true; |
+} |
+ |
+ |
+/// Hash code for a string which is compatible with [equalsIgnoreAsciiCase]. |
+/// |
+/// The hash code is unaffected by changing the case of ASCII letters, but |
+/// the case of non-ASCII letters do affect the result. |
+int hashIgnoreAsciiCase(String string) { |
+ // Jenkins hash code ( http://en.wikipedia.org/wiki/Jenkins_hash_function). |
+ // adapted to smi values. |
+ // Same hash used by dart2js for strings, modified to ignore ASCII letter |
+ // case. |
+ int hash = 0; |
+ for (int i = 0; i < string.length; i++) { |
+ int char = string.codeUnitAt(i); |
+ // Convert lower-case ASCII letters to upper case.upper |
+ // This ensures that strings that differ only in case will have the |
+ // same hash code. |
+ if (_lowerCaseA <= char && char <= _lowerCaseZ) char -= _asciiCaseBit; |
+ hash = 0x1fffffff & (hash + char); |
+ hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); |
+ hash >>= 6; |
+ } |
+ hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); |
+ hash >>= 11; |
+ return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); |
+} |
+ |
+ |
+/// Compares [a] and [b] lexically, converting ASCII letters to upper case. |
+/// |
+/// Comparison treats all lower-case ASCII letters as upper-case letters, |
+/// but does no case conversion for non-ASCII letters. |
+/// |
+/// If two strings differ only on the case of ASCII letters, the one with the |
+/// capital letter at the first difference will compare as less than the other |
+/// string. This tie-breaking ensures that the comparison is a total ordering |
+/// on strings and is compatible with equality. |
+/// |
+/// Ignoring non-ASCII letters is not generally a good idea, but it makes sense |
+/// for situations where the strings are known to be ASCII. Examples could |
+/// be Dart identifiers, base-64 or hex encoded strings, GUIDs or similar |
+/// strings with a known structure. |
+int compareAsciiUpperCase(String a, String b) { |
+ int defaultResult = 0; // Returned if no difference found. |
+ for (int i = 0; i < a.length; i++) { |
+ if (i >= b.length) return 1; |
+ var aChar = a.codeUnitAt(i); |
+ var bChar = b.codeUnitAt(i); |
+ if (aChar == bChar) continue; |
+ // Upper-case if letters. |
+ int aUpperCase = aChar; |
+ int bUpperCase = bChar; |
+ if (_lowerCaseA <= aChar && aChar <= _lowerCaseZ) { |
+ aUpperCase -= _asciiCaseBit; |
+ } |
+ if (_lowerCaseA <= bChar && bChar <= _lowerCaseZ) { |
+ bUpperCase -= _asciiCaseBit; |
+ } |
+ if (aUpperCase != bUpperCase) return (aUpperCase - bUpperCase).sign; |
+ if (defaultResult == 0) defaultResult = (aChar - bChar); |
+ } |
+ if (b.length > a.length) return -1; |
+ return defaultResult.sign; |
+} |
+ |
+ |
+/// Compares [a] and [b] lexically, converting ASCII letters to lower case. |
+/// |
+/// Comparison treats all upper-case ASCII letters as lower-case letters, |
+/// but does no case conversion for non-ASCII letters. |
+/// |
+/// If two strings differ only on the case of ASCII letters, the one with the |
+/// capital letter at the first difference will compare as less than the other |
+/// string. This tie-breaking ensures that the comparison is a total ordering |
+/// on strings. |
+/// |
+/// Ignoring non-ASCII letters is not generally a good idea, but it makes sense |
+/// for situations where the strings are known to be ASCII. Examples could |
+/// be Dart identifiers, base-64 or hex encoded strings, GUIDs or similar |
+/// strings with a known structure. |
+int compareAsciiLowerCase(String a, String b) { |
+ int defaultResult = 0; |
+ for (int i = 0; i < a.length; i++) { |
+ if (i >= b.length) return 1; |
+ var aChar = a.codeUnitAt(i); |
+ var bChar = b.codeUnitAt(i); |
+ if (aChar == bChar) continue; |
+ int aLowerCase = aChar; |
+ int bLowerCase = bChar; |
+ // Upper case if ASCII letters. |
+ if (_upperCaseA <= bChar && bChar <= _upperCaseZ) { |
+ bLowerCase += _asciiCaseBit; |
+ } |
+ if (_upperCaseA <= aChar && aChar <= _upperCaseZ) { |
+ aLowerCase += _asciiCaseBit; |
+ } |
+ if (aLowerCase != bLowerCase) return (aLowerCase - bLowerCase).sign; |
+ if (defaultResult == 0) defaultResult = aChar - bChar; |
+ } |
+ if (b.length > a.length) return -1; |
+ return defaultResult.sign; |
+} |
+ |
+/// Compares strings [a] and [b] according to [natural sort ordering]. |
+/// |
+/// A natural sort ordering is a lexical ordering where embedded |
+/// numerals (digit sequences) are treated as a single unit and ordered by |
+/// numerical value. |
+/// This means that `"a10b"` will be ordered after `"a7b"` in natural |
+/// ordering, where lexical ordering would put the `1` before the `7`, ignoring |
+/// that the `1` is part of a larger number. |
+/// |
+/// Example: |
+/// The following strings are in the order they would be sorted by using this |
+/// comparison function: |
+/// |
+/// "a", "a0", "a0b", "a1", "a01", "a9", "a10", "a100", "a100b", "aa" |
+/// |
+/// [natural sort ordering]: https://en.wikipedia.org/wiki/Natural_sort_order |
+int compareNatural(String a, String b) { |
+ for (int i = 0; i < a.length; i++) { |
+ if (i >= b.length) return 1; |
+ var aChar = a.codeUnitAt(i); |
+ var bChar = b.codeUnitAt(i); |
+ if (aChar != bChar) { |
+ return _compareNaturally(a, b, i, aChar, bChar); |
+ } |
+ } |
+ if (b.length > a.length) return -1; |
+ return 0; |
+} |
+ |
+/// Compares strings [a] and [b] according to lower-case |
+/// [natural sort ordering]. |
+/// |
+/// ASCII letters are converted to lower case before being compared, like |
+/// for [compareAsciiLowerCase], then the result is compared like for |
+/// [compareNatural]. |
+/// |
+/// If two strings differ only on the case of ASCII letters, the one with the |
+/// capital letter at the first difference will compare as less than the other |
+/// string. This tie-breaking ensures that the comparison is a total ordering |
+/// on strings. |
+/// |
+/// [natural sort ordering]: https://en.wikipedia.org/wiki/Natural_sort_order |
+int compareAsciiLowerCaseNatural(String a, String b) { |
+ int defaultResult = 0; // Returned if no difference found. |
+ for (int i = 0; i < a.length; i++) { |
+ if (i >= b.length) return 1; |
+ var aChar = a.codeUnitAt(i); |
+ var bChar = b.codeUnitAt(i); |
+ if (aChar == bChar) continue; |
+ int aLowerCase = aChar; |
+ int bLowerCase = bChar; |
+ if (_upperCaseA <= aChar && aChar <= _upperCaseZ) { |
+ aLowerCase += _asciiCaseBit; |
+ } |
+ if (_upperCaseA <= bChar && bChar <= _upperCaseZ) { |
+ bLowerCase += _asciiCaseBit; |
+ } |
+ if (aLowerCase != bLowerCase) { |
+ return _compareNaturally(a, b, i, aLowerCase, bLowerCase); |
+ } |
+ if (defaultResult == 0) defaultResult = aChar - bChar; |
+ } |
+ if (b.length > a.length) return -1; |
+ return defaultResult.sign; |
+} |
+ |
+/// Compares strings [a] and [b] according to upper-case |
+/// [natural sort ordering]. |
+/// |
+/// ASCII letters are converted to upper case before being compared, like |
+/// for [compareAsciiUpperCase], then the result is compared like for |
+/// [compareNatural]. |
+/// |
+/// If two strings differ only on the case of ASCII letters, the one with the |
+/// capital letter at the first difference will compare as less than the other |
+/// string. This tie-breaking ensures that the comparison is a total ordering |
+/// on strings |
+/// |
+/// [natural sort ordering]: https://en.wikipedia.org/wiki/Natural_sort_order |
+int compareAsciiUpperCaseNatural(String a, String b) { |
+ int defaultResult = 0; |
+ for (int i = 0; i < a.length; i++) { |
+ if (i >= b.length) return 1; |
+ var aChar = a.codeUnitAt(i); |
+ var bChar = b.codeUnitAt(i); |
+ if (aChar == bChar) continue; |
+ int aUpperCase = aChar; |
+ int bUpperCase = bChar; |
+ if (_lowerCaseA <= aChar && aChar <= _lowerCaseZ) { |
+ aUpperCase -= _asciiCaseBit; |
+ } |
+ if (_lowerCaseA <= bChar && bChar <= _lowerCaseZ) { |
+ bUpperCase -= _asciiCaseBit; |
+ } |
+ if (aUpperCase != bUpperCase) { |
+ return _compareNaturally(a, b, i, aUpperCase, bUpperCase); |
+ } |
+ if (defaultResult == 0) defaultResult = aChar - bChar; |
+ } |
+ if (b.length > a.length) return -1; |
+ return defaultResult.sign; |
+} |
+ |
+/// Check for numbers overlapping the current mismatched characters. |
+/// |
+/// If both [aChar] and [bChar] are digits, use numerical comparison. |
+/// Check if the previous characters is a non-zero number, and if not, |
+/// skip - but count - leading zeros before comparing numbers. |
+/// |
+/// If one is a digit and the other isn't, check if the previous character |
+/// is a digit, and if so, the the one with the digit is the greater number. |
+/// |
+/// Otherwise just returns the difference between [aChar] and [bChar]. |
+int _compareNaturally( |
+ String a, String b, int index, int aChar, int bChar) { |
+ assert(aChar != bChar); |
+ var aIsDigit = _isDigit(aChar); |
+ var bIsDigit = _isDigit(bChar); |
+ if (aIsDigit) { |
+ if (bIsDigit) { |
+ return _compareNumerically(a, b, aChar, bChar, index); |
+ } else if (index > 0 && _isDigit(a.codeUnitAt(index - 1))) { |
+ // aChar is the continuation of a longer number. |
+ return 1; |
+ } |
+ } else if (bIsDigit && index > 0 && _isDigit(b.codeUnitAt(index - 1))) { |
+ // bChar is the continuation of a longer number. |
+ return -1; |
+ } |
+ // Characters are both non-digits, or not continuation of earlier number. |
+ return (aChar - bChar).sign; |
+} |
+ |
+/// Compare numbers overlapping [aChar] and [bChar] numerically. |
+/// |
+/// If the numbers have the same numerical value, but one has more leading |
+/// zeros, the longer number is considered greater than the shorter one. |
+/// |
+/// This ensures a total ordering on strings compatible with equality. |
+int _compareNumerically(String a, String b, int aChar, int bChar, int index) { |
+ // Both are digits. Find the first significant different digit, then find |
+ // the length of the numbers. |
+ if (_isNonZeroNumberSuffix(a, index)) { |
+ // Part of a longer number, differs at this index, just count the length. |
+ int result = _compareDigitCount(a, b, index, index); |
+ if (result != 0) return result; |
+ // If same length, the current character is the most significant differing |
+ // digit. |
+ return (aChar - bChar).sign; |
+ } |
+ // Not part of larger (non-zero) number, so skip leading zeros before |
+ // comparing numbers. |
+ int aIndex = index; |
+ int bIndex = index; |
+ if (aChar == _zero) { |
+ do { |
+ aIndex++; |
+ if (aIndex == a.length) return -1; // number in a is zero, b is not. |
+ aChar = a.codeUnitAt(aIndex); |
+ } while (aChar == _zero); |
+ if (!_isDigit(aChar)) return -1; |
+ } else if (bChar == _zero) { |
+ do { |
+ bIndex++; |
+ if (bIndex == b.length) return 1; // number in b is zero, a is not. |
+ bChar = b.codeUnitAt(bIndex); |
+ } while (bChar == _zero); |
+ if (!_isDigit(bChar)) return 1; |
+ } |
+ if (aChar != bChar) { |
+ int result = _compareDigitCount(a, b, aIndex, bIndex); |
+ if (result != 0) return result; |
+ return (aChar - bChar).sign; |
+ } |
+ // Same leading digit, one had more leading zeros. |
+ // Compare digits until reaching a difference. |
+ while (true) { |
+ var aIsDigit = false; |
+ var bIsDigit = false; |
+ aChar = 0; |
+ bChar = 0; |
+ if (++aIndex < a.length) { |
+ aChar = a.codeUnitAt(aIndex); |
+ aIsDigit = _isDigit(aChar); |
+ } |
+ if (++bIndex < b.length) { |
+ bChar = b.codeUnitAt(bIndex); |
+ bIsDigit = _isDigit(bChar); |
+ } |
+ if (aIsDigit) { |
+ if (bIsDigit) { |
+ if (aChar == bChar) continue; |
+ // First different digit found. |
+ break; |
+ } |
+ // bChar is non-digit, so a has longer number. |
+ return 1; |
+ } else if (bIsDigit) { |
+ return -1; // b has longer number. |
+ } else { |
+ // Neither is digit, so numbers had same numerical value. |
+ // Fall back on number of leading zeros |
+ // (reflected by difference in indices). |
+ return (aIndex - bIndex).sign; |
+ } |
+ } |
+ // At first differing digits. |
+ int result = _compareDigitCount(a, b, aIndex, bIndex); |
+ if (result != 0) return result; |
+ return (aChar - bChar).sign; |
+} |
+ |
+/// Checks which of [a] and [b] has the longest sequence of digits. |
+/// |
+/// Starts counting from `i + 1` and `j + 1` (assumes that `a[i]` and `b[j]` are |
+/// both already known to be digits). |
+int _compareDigitCount(String a, String b, int i, int j) { |
+ while (++i < a.length) { |
+ bool aIsDigit = _isDigit(a.codeUnitAt(i)); |
+ if (++j == b.length) return aIsDigit ? 1 : 0; |
+ bool bIsDigit = _isDigit(b.codeUnitAt(j)); |
+ if (aIsDigit) { |
+ if (bIsDigit) continue; |
+ return 1; |
+ } else if (bIsDigit) { |
+ return -1; |
+ } else { |
+ return 0; |
+ } |
+ } |
+ if (++j < b.length && _isDigit(b.codeUnitAt(j))) { |
+ return -1; |
+ } |
+ return 0; |
+} |
+ |
+bool _isDigit(int charCode) => (charCode ^ _zero) <= 9; |
+ |
+/// Check if the digit at [index] is continuing a non-zero number. |
+/// |
+/// If there is no non-zero digits before, then leading zeros at [index] |
+/// are also ignored when comparing numerically. If there is a non-zero digit |
+/// before, then zeros at [index] are significant. |
+bool _isNonZeroNumberSuffix(String string, int index) { |
+ while (--index >= 0) { |
+ int char = string.codeUnitAt(index); |
+ if (char != _zero) return _isDigit(char); |
+ } |
+ return false; |
+} |