From 87a8d0356524f51555a58c528170592a2ccf19bf Mon Sep 17 00:00:00 2001 From: Pranav Senthilnathan Date: Wed, 13 Nov 2024 12:23:06 -0800 Subject: [PATCH 1/8] Numerical ordering for string compare/equals/hashcode --- .../System/Globalization/CompareInfo.Nls.cs | 2 + .../Globalization/CompareInfo.WebAssembly.cs | 6 +- .../src/System/Globalization/CompareInfo.cs | 3 +- .../System/Globalization/CompareOptions.cs | 1 + .../src/System/StringComparer.cs | 4 +- .../System.Runtime/ref/System.Runtime.cs | 1 + .../CompareInfo/CompareInfoTests.Compare.cs | 124 +++++++++++++- .../CompareInfo/CompareInfoTests.HashCode.cs | 7 + .../CompareInfo/CompareInfoTests.IndexOf.cs | 11 ++ .../CompareInfo/CompareInfoTests.IsPrefix.cs | 1 + .../CompareInfo/CompareInfoTests.IsSuffix.cs | 1 + .../CompareInfoTests.LastIndexOf.cs | 11 ++ .../CompareInfo/CompareInfoTests.SortKey.cs | 41 +++++ .../Invariant/InvariantMode.cs | 41 +++++ .../System/StringComparerTests.cs | 10 ++ .../System/StringGetHashCodeTests.cs | 1 + .../hybrid-globalization/collations.ts | 157 +++++++----------- .../pal_collation.c | 19 ++- 18 files changed, 331 insertions(+), 110 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Nls.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Nls.cs index bb24e9242fa0e..41d8f465a6113 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Nls.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Nls.cs @@ -583,6 +583,7 @@ private static unsafe bool NlsIsSortable(ReadOnlySpan text) private const int NORM_IGNOREWIDTH = 0x00020000; // Does not differentiate between a single-byte character and the same character as a double-byte character. private const int NORM_LINGUISTIC_CASING = 0x08000000; // use linguistic rules for casing private const int SORT_STRINGSORT = 0x00001000; // Treats punctuation the same as symbols. + private const int SORT_DIGITSASNUMBERS = 0x00000008; // Treat digits as numbers during sorting, for example, sort "2" before "10". private static int GetNativeCompareFlags(CompareOptions options) { @@ -595,6 +596,7 @@ private static int GetNativeCompareFlags(CompareOptions options) if ((options & CompareOptions.IgnoreSymbols) != 0) { nativeCompareFlags |= NORM_IGNORESYMBOLS; } if ((options & CompareOptions.IgnoreWidth) != 0) { nativeCompareFlags |= NORM_IGNOREWIDTH; } if ((options & CompareOptions.StringSort) != 0) { nativeCompareFlags |= SORT_STRINGSORT; } + if ((options & CompareOptions.NumericOrdering) != 0) { nativeCompareFlags |= SORT_DIGITSASNUMBERS; } // TODO: Can we try for GetNativeCompareFlags to never // take Ordinal or OrdinalIgnoreCase. This value is not part of Win32, we just handle it special diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.WebAssembly.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.WebAssembly.cs index 8ea2223764d8d..34cf8afee31e9 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.WebAssembly.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.WebAssembly.cs @@ -175,11 +175,9 @@ private ReadOnlySpan SanitizeForInvariantHash(ReadOnlySpan source, C } private static bool IndexingOptionsNotSupported(CompareOptions options) => - (options & CompareOptions.IgnoreSymbols) == CompareOptions.IgnoreSymbols; + (options & (CompareOptions.IgnoreSymbols | CompareOptions.NumericOrdering)) != 0; - private static bool CompareOptionsNotSupported(CompareOptions options) => - (options & CompareOptions.IgnoreWidth) == CompareOptions.IgnoreWidth || - ((options & CompareOptions.IgnoreNonSpace) == CompareOptions.IgnoreNonSpace && (options & CompareOptions.IgnoreKanaType) != CompareOptions.IgnoreKanaType); + private static bool CompareOptionsNotSupported(CompareOptions options) => (options & CompareOptions.IgnoreWidth) != 0; private static string GetPNSE(CompareOptions options) => SR.Format(SR.PlatformNotSupported_HybridGlobalizationWithCompareOptions, options); diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.cs index f60a8fa16558c..dd0b021d3bb05 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.cs @@ -26,7 +26,8 @@ public sealed partial class CompareInfo : IDeserializationCallback // Mask used to check if Compare() / GetHashCode(string) / GetSortKey has the right flags. private const CompareOptions ValidCompareMaskOffFlags = ~(CompareOptions.IgnoreCase | CompareOptions.IgnoreSymbols | CompareOptions.IgnoreNonSpace | - CompareOptions.IgnoreWidth | CompareOptions.IgnoreKanaType | CompareOptions.StringSort); + CompareOptions.IgnoreWidth | CompareOptions.IgnoreKanaType | CompareOptions.StringSort | + CompareOptions.NumericOrdering); // Cache the invariant CompareInfo internal static readonly CompareInfo Invariant = CultureInfo.InvariantCulture.CompareInfo; diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareOptions.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareOptions.cs index b80f7430c90b7..ba12f58c5a057 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareOptions.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareOptions.cs @@ -12,6 +12,7 @@ public enum CompareOptions IgnoreSymbols = 0x00000004, IgnoreKanaType = 0x00000008, IgnoreWidth = 0x00000010, + NumericOrdering = 0x00000020, OrdinalIgnoreCase = 0x10000000, // This flag can not be used with other flags. StringSort = 0x20000000, Ordinal = 0x40000000, // This flag can not be used with other flags. diff --git a/src/libraries/System.Private.CoreLib/src/System/StringComparer.cs b/src/libraries/System.Private.CoreLib/src/System/StringComparer.cs index 583454a33cf72..8398dcfa79b3e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/StringComparer.cs +++ b/src/libraries/System.Private.CoreLib/src/System/StringComparer.cs @@ -217,7 +217,9 @@ public sealed class CultureAwareComparer : StringComparer, IAlternateEqualityCom internal static readonly CultureAwareComparer InvariantCaseSensitiveInstance = new CultureAwareComparer(CompareInfo.Invariant, CompareOptions.None); internal static readonly CultureAwareComparer InvariantIgnoreCaseInstance = new CultureAwareComparer(CompareInfo.Invariant, CompareOptions.IgnoreCase); - private const CompareOptions ValidCompareMaskOffFlags = ~(CompareOptions.IgnoreCase | CompareOptions.IgnoreSymbols | CompareOptions.IgnoreNonSpace | CompareOptions.IgnoreWidth | CompareOptions.IgnoreKanaType | CompareOptions.StringSort); + private const CompareOptions ValidCompareMaskOffFlags = + ~(CompareOptions.IgnoreCase | CompareOptions.IgnoreSymbols | CompareOptions.IgnoreNonSpace | CompareOptions.IgnoreKanaType | + CompareOptions.IgnoreWidth | CompareOptions.NumericOrdering | CompareOptions.StringSort); private readonly CompareInfo _compareInfo; // Do not rename (binary serialization) private readonly CompareOptions _options; diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 1d354dd846c30..0d0f6787840cc 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -9168,6 +9168,7 @@ public enum CompareOptions IgnoreSymbols = 4, IgnoreKanaType = 8, IgnoreWidth = 16, + NumericOrdering = 32, OrdinalIgnoreCase = 268435456, StringSort = 536870912, Ordinal = 1073741824, diff --git a/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.Compare.cs b/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.Compare.cs index 484023f579f6a..7f06f2fdd25f7 100644 --- a/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.Compare.cs +++ b/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.Compare.cs @@ -3,6 +3,8 @@ using System.Buffers; using System.Collections.Generic; +using System.Linq; +using System.Text; using Xunit; namespace System.Globalization.Tests @@ -41,6 +43,47 @@ public static IEnumerable Compare_Kana_TestData() public static IEnumerable Compare_TestData() { + #region Numeric ordering + var isNls = PlatformDetection.IsNlsGlobalization; + + yield return new object[] { s_invariantCompare, "1234567890", "1234567890", CompareOptions.NumericOrdering, 0 }; + yield return new object[] { s_invariantCompare, "1234567890", "1234567890", CompareOptions.NumericOrdering, 0 }; + + // Leading zero + yield return new object[] { s_invariantCompare, "02", "1", CompareOptions.NumericOrdering, 1 }; + yield return new object[] { s_invariantCompare, "a02", "a1", CompareOptions.NumericOrdering, 1 }; + yield return new object[] { s_invariantCompare, "02a", "1a", CompareOptions.NumericOrdering, 1 }; + + // NLS treats equivalent numbers differing by leading zeros as unequal + yield return new object[] { s_invariantCompare, "01", "1", CompareOptions.NumericOrdering, isNls ? -1 : 0 }; + yield return new object[] { s_invariantCompare, "a01", "a1", CompareOptions.NumericOrdering, isNls ? -1 : 0 }; + yield return new object[] { s_invariantCompare, "01a", "1a", CompareOptions.NumericOrdering, isNls ? -1 : 0 }; + + // But they are closer in sort order than unequal numbers: 1 < 02 < 2 < 03 + // Unlike non-numerical sort which bookends them: 02 < 03 < 1 < 2 + yield return new object[] { s_invariantCompare, "1", "02", CompareOptions.NumericOrdering, -1 }; + yield return new object[] { s_invariantCompare, "02", "2", CompareOptions.NumericOrdering, isNls ? -1 : 0 }; + yield return new object[] { s_invariantCompare, "2", "03", CompareOptions.NumericOrdering, -1 }; + + // 2 < 10 + yield return new object[] { s_invariantCompare, "2", "10", CompareOptions.NumericOrdering, -1 }; + yield return new object[] { s_invariantCompare, "a2", "a10", CompareOptions.NumericOrdering, -1 }; + yield return new object[] { s_invariantCompare, "2a", "10a", CompareOptions.NumericOrdering, -1 }; + + // With casing + yield return new object[] { s_invariantCompare, "1A02", "1a02", CompareOptions.NumericOrdering | CompareOptions.IgnoreCase, 0 }; + yield return new object[] { s_invariantCompare, "A1", "a2", CompareOptions.NumericOrdering, -1 }; // Numerical differences have higher precedence + yield return new object[] { s_invariantCompare, "A01", "a1", CompareOptions.NumericOrdering, isNls ? -1 : 1 }; // ICU treats 01 == 1 + + // With diacritics + yield return new object[] { s_invariantCompare, "1\u00E102", "1a02", CompareOptions.NumericOrdering | CompareOptions.IgnoreNonSpace, 0 }; + yield return new object[] { s_invariantCompare, "\u00E11", "a2", CompareOptions.NumericOrdering, -1 }; // Numerical differences have higher precedence + yield return new object[] { s_invariantCompare, "\u00E101", "a1", CompareOptions.NumericOrdering, isNls ? -1 : 1 }; // ICU treats 01 == 1 + + // Period is NOT part of the numeric value + yield return new object[] { s_invariantCompare, "0.1", "0.02", CompareOptions.NumericOrdering, -1 }; + #endregion + // PlatformDetection.IsHybridGlobalizationOnBrowser does not support IgnoreKanaType alone, it needs to be e.g. with IgnoreCase CompareOptions validIgnoreKanaTypeOption = PlatformDetection.IsHybridGlobalizationOnBrowser ? CompareOptions.IgnoreKanaType | CompareOptions.IgnoreCase : @@ -105,6 +148,7 @@ public static IEnumerable Compare_TestData() yield return new object[] { s_invariantCompare, "'", "\uFF07", CompareOptions.IgnoreWidth, 0 }; yield return new object[] { s_invariantCompare, "\"", "\uFF02", CompareOptions.IgnoreWidth, 0 }; } + yield return new object[] { s_invariantCompare, "\u3042", "\u30A1", CompareOptions.None, PlatformDetection.IsHybridGlobalizationOnApplePlatform ? 1 : s_expectedHiraganaToKatakanaCompare }; yield return new object[] { s_invariantCompare, "\u3042", "\u30A2", CompareOptions.None, s_expectedHiraganaToKatakanaCompare }; yield return new object[] { s_invariantCompare, "\u3042", "\uFF71", CompareOptions.None, s_expectedHiraganaToKatakanaCompare }; @@ -463,6 +507,14 @@ public void Compare_Invalid() AssertExtensions.Throws("options", () => s_invariantCompare.Compare("Tests", 0, "Tests", 0, CompareOptions.OrdinalIgnoreCase | CompareOptions.IgnoreWidth)); AssertExtensions.Throws("options", () => s_invariantCompare.Compare("Tests", 0, 2, "Tests", 0, 2, CompareOptions.OrdinalIgnoreCase | CompareOptions.IgnoreWidth)); + AssertExtensions.Throws("options", () => s_invariantCompare.Compare("Tests", "Tests", CompareOptions.Ordinal | CompareOptions.NumericOrdering)); + AssertExtensions.Throws("options", () => s_invariantCompare.Compare("Tests", 0, "Tests", 0, CompareOptions.Ordinal | CompareOptions.NumericOrdering)); + AssertExtensions.Throws("options", () => s_invariantCompare.Compare("Tests", 0, 2, "Tests", 0, 2, CompareOptions.Ordinal | CompareOptions.NumericOrdering)); + + AssertExtensions.Throws("options", () => s_invariantCompare.Compare("Tests", "Tests", CompareOptions.OrdinalIgnoreCase | CompareOptions.NumericOrdering)); + AssertExtensions.Throws("options", () => s_invariantCompare.Compare("Tests", 0, "Tests", 0, CompareOptions.OrdinalIgnoreCase | CompareOptions.NumericOrdering)); + AssertExtensions.Throws("options", () => s_invariantCompare.Compare("Tests", 0, 2, "Tests", 0, 2, CompareOptions.OrdinalIgnoreCase | CompareOptions.NumericOrdering)); + // Offset1 < 0 AssertExtensions.Throws("offset1", () => s_invariantCompare.Compare("Test", -1, "Test", 0)); AssertExtensions.Throws("offset1", () => s_invariantCompare.Compare("Test", -1, "Test", 0, CompareOptions.None)); @@ -512,6 +564,70 @@ public void Compare_Invalid() AssertExtensions.Throws("string2", () => s_invariantCompare.Compare("Test", 0, 2, "Test", 2, 3, CompareOptions.None)); } + public static IEnumerable Compare_Numeric_TestData() + { + if (!PlatformDetection.IsNlsGlobalization) + { + // '1' in different languages. Not exhaustive. + Rune[] numberOnes = + [ + new Rune(0x0031), new Rune(0x0661), new Rune(0x06F1), new Rune(0x07C1), new Rune(0x0967), + new Rune(0x09E7), new Rune(0x0A67), new Rune(0x0AE7), new Rune(0x0B67), new Rune(0x0BE7), + new Rune(0x0C67), new Rune(0x0CE7), new Rune(0x0D67), new Rune(0x0DE7), new Rune(0x0E51), + new Rune(0x0ED1), new Rune(0x0F21), new Rune(0x1041), new Rune(0x1091), new Rune(0x17E1), + new Rune(0x1811), new Rune(0x1947), new Rune(0x19D1), new Rune(0x1A81), new Rune(0x1A91), + new Rune(0x1B51), new Rune(0x1BB1), new Rune(0x1C41), new Rune(0x1C51), new Rune(0xA621), + new Rune(0xA8D1), new Rune(0xA901), new Rune(0xA9D1), new Rune(0xA9F1), new Rune(0xAA51), + new Rune(0xABF1), new Rune(0xFF11), new Rune(0x104A1), new Rune(0x10D31), new Rune(0x11067), + new Rune(0x110F1), new Rune(0x11137), new Rune(0x111D1), new Rune(0x112F1), new Rune(0x11451), + new Rune(0x114D1), new Rune(0x11651), new Rune(0x116C1), new Rune(0x11731), new Rune(0x118E1), + new Rune(0x11951), new Rune(0x11C51), + ]; + + StringBuilder sb = new(); + Span buffer = stackalloc char[2]; + foreach (var r in numberOnes) + { + sb.Append(buffer.Slice(0, r.EncodeToUtf16(buffer))); + } + string numberOnesString = sb.ToString(); + + // 111...110 vs 111...111 + yield return new object[] { + s_invariantCompare, + new string('1', numberOnes.Length - 1) + "0", + numberOnesString, + CompareOptions.NumericOrdering, + -1 + }; + + // 111...111 vs 111...111 + yield return new object[] { + s_invariantCompare, + new string('1', numberOnes.Length), + numberOnesString, + CompareOptions.NumericOrdering, + 0 + }; + + // 111...112 vs 111...111 + yield return new object[] { + s_invariantCompare, + new string('1', numberOnes.Length - 1) + "2", + numberOnesString, + CompareOptions.NumericOrdering, + 1 + }; + } + } + + [Theory] + [MemberData(nameof(Compare_Numeric_TestData))] + public void CompareNumericLanguages(CompareInfo compareInfo, string string1, string string2, CompareOptions options, int expected) + { + Compare_Advanced(compareInfo, string1, 0, string1?.Length ?? 0, string2, 0, string2?.Length ?? 0, options, expected); + } + [Fact] public void TestIgnoreKanaAndWidthCases() { @@ -550,6 +666,10 @@ public static IEnumerable Compare_HiraganaAndKatakana_TestData() CompareOptions.None, CompareOptions.IgnoreCase, CompareOptions.IgnoreSymbols, + CompareOptions.IgnoreNonSpace, + CompareOptions.IgnoreNonSpace | CompareOptions.IgnoreCase, + CompareOptions.IgnoreSymbols | CompareOptions.IgnoreNonSpace, + CompareOptions.IgnoreSymbols | CompareOptions.IgnoreNonSpace | CompareOptions.IgnoreCase, CompareOptions.IgnoreSymbols | CompareOptions.IgnoreCase, CompareOptions.IgnoreKanaType | CompareOptions.IgnoreSymbols, CompareOptions.IgnoreKanaType | CompareOptions.IgnoreCase, @@ -600,10 +720,6 @@ public static IEnumerable Compare_HiraganaAndKatakana_TestData() }; CompareOptions[] optionsNegative = PlatformDetection.IsHybridGlobalizationOnBrowser ? new[] { - CompareOptions.IgnoreNonSpace, - CompareOptions.IgnoreNonSpace | CompareOptions.IgnoreCase, - CompareOptions.IgnoreSymbols | CompareOptions.IgnoreNonSpace, - CompareOptions.IgnoreSymbols | CompareOptions.IgnoreNonSpace | CompareOptions.IgnoreCase, CompareOptions.IgnoreWidth, CompareOptions.IgnoreWidth | CompareOptions.IgnoreCase, CompareOptions.IgnoreWidth | CompareOptions.IgnoreNonSpace, diff --git a/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.HashCode.cs b/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.HashCode.cs index aa05f00d4b6e6..49be898000b9b 100644 --- a/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.HashCode.cs +++ b/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.HashCode.cs @@ -66,6 +66,8 @@ void CheckChar(int charCode, CultureInfo culture) new object[] { "abc", CompareOptions.Ordinal, "abc", CompareOptions.Ordinal, true }, new object[] { "abc", CompareOptions.None, "abc", CompareOptions.None, true }, new object[] { "", CompareOptions.None, "\u200c", CompareOptions.None, true }, // see comment at bottom of SortKey_TestData + new object[] { "1", CompareOptions.NumericOrdering, "01", CompareOptions.NumericOrdering, true }, + new object[] { "1", CompareOptions.NumericOrdering, "\u0661", CompareOptions.NumericOrdering, true }, }; [Theory] @@ -99,7 +101,10 @@ public void GetHashCode_Invalid() AssertExtensions.Throws("source", () => CultureInfo.InvariantCulture.CompareInfo.GetHashCode(null, CompareOptions.None)); AssertExtensions.Throws("options", () => CultureInfo.InvariantCulture.CompareInfo.GetHashCode("Test", CompareOptions.OrdinalIgnoreCase | CompareOptions.IgnoreCase)); + AssertExtensions.Throws("options", () => CultureInfo.InvariantCulture.CompareInfo.GetHashCode("Test", CompareOptions.OrdinalIgnoreCase | CompareOptions.IgnoreSymbols)); AssertExtensions.Throws("options", () => CultureInfo.InvariantCulture.CompareInfo.GetHashCode("Test", CompareOptions.Ordinal | CompareOptions.IgnoreSymbols)); + AssertExtensions.Throws("options", () => CultureInfo.InvariantCulture.CompareInfo.GetHashCode("Test", CompareOptions.OrdinalIgnoreCase | CompareOptions.NumericOrdering)); + AssertExtensions.Throws("options", () => CultureInfo.InvariantCulture.CompareInfo.GetHashCode("Test", CompareOptions.Ordinal | CompareOptions.NumericOrdering)); AssertExtensions.Throws("options", () => CultureInfo.InvariantCulture.CompareInfo.GetHashCode("Test", (CompareOptions)(-1))); } @@ -118,6 +123,8 @@ public void GetHashCode_Span_Invalid() { AssertExtensions.Throws("options", () => CultureInfo.InvariantCulture.CompareInfo.GetHashCode("Test".AsSpan(), CompareOptions.OrdinalIgnoreCase | CompareOptions.IgnoreCase)); AssertExtensions.Throws("options", () => CultureInfo.InvariantCulture.CompareInfo.GetHashCode("Test".AsSpan(), CompareOptions.Ordinal | CompareOptions.IgnoreSymbols)); + AssertExtensions.Throws("options", () => CultureInfo.InvariantCulture.CompareInfo.GetHashCode("Test".AsSpan(), CompareOptions.OrdinalIgnoreCase | CompareOptions.NumericOrdering)); + AssertExtensions.Throws("options", () => CultureInfo.InvariantCulture.CompareInfo.GetHashCode("Test".AsSpan(), CompareOptions.Ordinal | CompareOptions.NumericOrdering)); AssertExtensions.Throws("options", () => CultureInfo.InvariantCulture.CompareInfo.GetHashCode("Test".AsSpan(), (CompareOptions)(-1))); } } diff --git a/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.IndexOf.cs b/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.IndexOf.cs index 7c583fb519aa1..2750a648bb80d 100644 --- a/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.IndexOf.cs +++ b/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.IndexOf.cs @@ -344,9 +344,19 @@ public void IndexOf_Invalid() AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's", 'b', 0, CompareOptions.StringSort)); AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's", 'c', 0, 2, CompareOptions.StringSort)); AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's".AsSpan(), "b".AsSpan(), CompareOptions.StringSort)); + + AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's", "Tests", CompareOptions.NumericOrdering)); + AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's", "Tests", 0, CompareOptions.NumericOrdering)); + AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's", "Tests", 0, 2, CompareOptions.NumericOrdering)); + AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's", 'a', CompareOptions.NumericOrdering)); + AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's", 'b', 0, CompareOptions.NumericOrdering)); + AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's", 'c', 0, 2, CompareOptions.NumericOrdering)); + AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's".AsSpan(), "b".AsSpan(), CompareOptions.NumericOrdering)); + if (PlatformDetection.IsHybridGlobalizationOnBrowser || PlatformDetection.IsHybridGlobalizationOnApplePlatform) { Assert.Throws(() => s_invariantCompare.IndexOf("Test's".AsSpan(), "b".AsSpan(), CompareOptions.StringSort, out _)); + Assert.Throws(() => s_invariantCompare.IndexOf("Test's".AsSpan(), "b".AsSpan(), CompareOptions.NumericOrdering, out _)); Assert.Throws(() => s_invariantCompare.IndexOf("Test's".AsSpan(), "b".AsSpan(), CompareOptions.Ordinal | CompareOptions.IgnoreWidth, out _)); Assert.Throws(() => s_invariantCompare.IndexOf("Test's".AsSpan(), "b".AsSpan(), (CompareOptions)(-1), out _)); Assert.Throws(() => s_invariantCompare.IndexOf("Test's".AsSpan(), "b".AsSpan(), (CompareOptions)0x11111111, out _)); @@ -355,6 +365,7 @@ public void IndexOf_Invalid() else { AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's".AsSpan(), "b".AsSpan(), CompareOptions.StringSort, out _)); + AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's".AsSpan(), "b".AsSpan(), CompareOptions.NumericOrdering, out _)); AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's".AsSpan(), "b".AsSpan(), CompareOptions.Ordinal | CompareOptions.IgnoreWidth, out _)); AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's".AsSpan(), "b".AsSpan(), (CompareOptions)(-1), out _)); AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's".AsSpan(), "b".AsSpan(), (CompareOptions)0x11111111, out _)); diff --git a/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.IsPrefix.cs b/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.IsPrefix.cs index f709f1de1348c..7dba521344e1c 100644 --- a/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.IsPrefix.cs +++ b/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.IsPrefix.cs @@ -184,6 +184,7 @@ public void IsPrefix_Invalid() // Options are invalid AssertExtensions.Throws("options", () => s_invariantCompare.IsPrefix("Test's", "Tests", CompareOptions.StringSort)); + AssertExtensions.Throws("options", () => s_invariantCompare.IsPrefix("Test's", "Tests", CompareOptions.NumericOrdering)); AssertExtensions.Throws("options", () => s_invariantCompare.IsPrefix("Test's", "Tests", CompareOptions.Ordinal | CompareOptions.IgnoreWidth)); AssertExtensions.Throws("options", () => s_invariantCompare.IsPrefix("Test's", "Tests", CompareOptions.OrdinalIgnoreCase | CompareOptions.IgnoreWidth)); AssertExtensions.Throws("options", () => s_invariantCompare.IsPrefix("Test's", "Tests", (CompareOptions)(-1))); diff --git a/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.IsSuffix.cs b/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.IsSuffix.cs index 3b2d048582536..e2506d7fdd84b 100644 --- a/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.IsSuffix.cs +++ b/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.IsSuffix.cs @@ -186,6 +186,7 @@ public void IsSuffix_Invalid() // Options are invalid AssertExtensions.Throws("options", () => s_invariantCompare.IsSuffix("Test's", "Tests", CompareOptions.StringSort)); + AssertExtensions.Throws("options", () => s_invariantCompare.IsSuffix("Test's", "Tests", CompareOptions.NumericOrdering)); AssertExtensions.Throws("options", () => s_invariantCompare.IsSuffix("Test's", "Tests", CompareOptions.Ordinal | CompareOptions.IgnoreWidth)); AssertExtensions.Throws("options", () => s_invariantCompare.IsSuffix("Test's", "Tests", CompareOptions.OrdinalIgnoreCase | CompareOptions.IgnoreWidth)); AssertExtensions.Throws("options", () => s_invariantCompare.IsSuffix("Test's", "Tests", (CompareOptions)(-1))); diff --git a/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.LastIndexOf.cs b/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.LastIndexOf.cs index 69cf4c0c3ba4a..380610e4412b2 100644 --- a/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.LastIndexOf.cs +++ b/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.LastIndexOf.cs @@ -348,9 +348,19 @@ public void LastIndexOf_Invalid() AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', 0, CompareOptions.StringSort)); AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', 0, 1, CompareOptions.StringSort)); AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", "a".AsSpan(), CompareOptions.StringSort)); + + AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", "Tests", CompareOptions.NumericOrdering)); + AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", "Tests", 0, CompareOptions.NumericOrdering)); + AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", "Tests", 0, 1, CompareOptions.NumericOrdering)); + AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', CompareOptions.NumericOrdering)); + AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', 0, CompareOptions.NumericOrdering)); + AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', 0, 1, CompareOptions.NumericOrdering)); + AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", "a".AsSpan(), CompareOptions.NumericOrdering)); + if (PlatformDetection.IsHybridGlobalizationOnBrowser || PlatformDetection.IsHybridGlobalizationOnApplePlatform) { Assert.Throws(() => s_invariantCompare.LastIndexOf("Test's", "a".AsSpan(), CompareOptions.StringSort, out int matchLength)); + Assert.Throws(() => s_invariantCompare.LastIndexOf("Test's", "a".AsSpan(), CompareOptions.NumericOrdering, out int matchLength)); Assert.Throws(() => s_invariantCompare.LastIndexOf("Test's", "a".AsSpan(), CompareOptions.Ordinal | CompareOptions.IgnoreWidth, out int matchLength)); Assert.Throws(() => s_invariantCompare.LastIndexOf("Test's", "a".AsSpan(), CompareOptions.OrdinalIgnoreCase | CompareOptions.IgnoreWidth, out int matchLength)); Assert.Throws(() => s_invariantCompare.LastIndexOf("Test's", "a".AsSpan(), (CompareOptions)(-1), out int matchLength)); @@ -359,6 +369,7 @@ public void LastIndexOf_Invalid() else { AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", "a".AsSpan(), CompareOptions.StringSort, out int matchLength)); + AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", "a".AsSpan(), CompareOptions.NumericOrdering, out int matchLength)); AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", "a".AsSpan(), CompareOptions.Ordinal | CompareOptions.IgnoreWidth, out int matchLength)); AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", "a".AsSpan(), CompareOptions.OrdinalIgnoreCase | CompareOptions.IgnoreWidth, out int matchLength)); AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", "a".AsSpan(), (CompareOptions)(-1), out int matchLength)); diff --git a/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.SortKey.cs b/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.SortKey.cs index faf49a2a8831f..723a86f14a315 100644 --- a/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.SortKey.cs +++ b/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.SortKey.cs @@ -21,6 +21,47 @@ public class CompareInfoSortKeyTests : CompareInfoTestsBase PlatformDetection.IsNlsGlobalization && CompareStringEx("", NORM_LINGUISTIC_CASING, "", 0, "\u200C", 1, IntPtr.Zero, IntPtr.Zero, 0) != 2; public static IEnumerable SortKey_TestData() { + #region Numeric ordering + var isNls = PlatformDetection.IsNlsGlobalization; + + yield return new object[] { s_invariantCompare, "1234567890", "1234567890", CompareOptions.NumericOrdering, 0 }; + yield return new object[] { s_invariantCompare, "1234567890", "1234567890", CompareOptions.NumericOrdering, 0 }; + + // Leading zero + yield return new object[] { s_invariantCompare, "02", "1", CompareOptions.NumericOrdering, 1 }; + yield return new object[] { s_invariantCompare, "a02", "a1", CompareOptions.NumericOrdering, 1 }; + yield return new object[] { s_invariantCompare, "02a", "1a", CompareOptions.NumericOrdering, 1 }; + + // NLS treats equivalent numbers differing by leading zeros as unequal + yield return new object[] { s_invariantCompare, "01", "1", CompareOptions.NumericOrdering, isNls ? -1 : 0 }; + yield return new object[] { s_invariantCompare, "a01", "a1", CompareOptions.NumericOrdering, isNls ? -1 : 0 }; + yield return new object[] { s_invariantCompare, "01a", "1a", CompareOptions.NumericOrdering, isNls ? -1 : 0 }; + + // But they are closer in sort order than unequal numbers: 1 < 02 < 2 < 03 + // Unlike non-numerical sort which bookends them: 02 < 03 < 1 < 2 + yield return new object[] { s_invariantCompare, "1", "02", CompareOptions.NumericOrdering, -1 }; + yield return new object[] { s_invariantCompare, "02", "2", CompareOptions.NumericOrdering, isNls ? -1 : 0 }; + yield return new object[] { s_invariantCompare, "2", "03", CompareOptions.NumericOrdering, -1 }; + + // 2 < 10 + yield return new object[] { s_invariantCompare, "2", "10", CompareOptions.NumericOrdering, -1 }; + yield return new object[] { s_invariantCompare, "a2", "a10", CompareOptions.NumericOrdering, -1 }; + yield return new object[] { s_invariantCompare, "2a", "10a", CompareOptions.NumericOrdering, -1 }; + + // With casing + yield return new object[] { s_invariantCompare, "1A02", "1a02", CompareOptions.NumericOrdering | CompareOptions.IgnoreCase, 0 }; + yield return new object[] { s_invariantCompare, "A1", "a2", CompareOptions.NumericOrdering, -1 }; // Numerical differences have higher precedence + yield return new object[] { s_invariantCompare, "A01", "a1", CompareOptions.NumericOrdering, isNls ? -1 : 1 }; // ICU treats 01 == 1 + + // With diacritics + yield return new object[] { s_invariantCompare, "1\u00E102", "1a02", CompareOptions.NumericOrdering | CompareOptions.IgnoreNonSpace, 0 }; + yield return new object[] { s_invariantCompare, "\u00E11", "a2", CompareOptions.NumericOrdering, -1 }; // Numerical differences have higher precedence + yield return new object[] { s_invariantCompare, "\u00E101", "a1", CompareOptions.NumericOrdering, isNls ? -1 : 1 }; // ICU treats 01 == 1 + + // Period is NOT part of the numeric value + yield return new object[] { s_invariantCompare, "0.1", "0.02", CompareOptions.NumericOrdering, -1 }; + #endregion + CompareOptions ignoreKanaIgnoreWidthIgnoreCase = CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth | CompareOptions.IgnoreCase; yield return new object[] { s_invariantCompare, "\u3042", "\u30A2", ignoreKanaIgnoreWidthIgnoreCase, 0 }; yield return new object[] { s_invariantCompare, "\u3042", "\uFF71", ignoreKanaIgnoreWidthIgnoreCase, 0 }; diff --git a/src/libraries/System.Runtime/tests/System.Globalization.Tests/Invariant/InvariantMode.cs b/src/libraries/System.Runtime/tests/System.Globalization.Tests/Invariant/InvariantMode.cs index c5733728fe664..61e03d7e7febe 100644 --- a/src/libraries/System.Runtime/tests/System.Globalization.Tests/Invariant/InvariantMode.cs +++ b/src/libraries/System.Runtime/tests/System.Globalization.Tests/Invariant/InvariantMode.cs @@ -312,6 +312,47 @@ public static IEnumerable IsSuffix_TestData() public static IEnumerable Compare_TestData() { + #region Numeric ordering + var isNls = PlatformDetection.IsNlsGlobalization; + + yield return new object[] { "1234567890", "1234567890", CompareOptions.NumericOrdering, 0 }; + yield return new object[] { "1234567890", "1234567890", CompareOptions.NumericOrdering, 0 }; + + // Leading zero + yield return new object[] { "02", "1", CompareOptions.NumericOrdering, 1 }; + yield return new object[] { "a02", "a1", CompareOptions.NumericOrdering, 1 }; + yield return new object[] { "02a", "1a", CompareOptions.NumericOrdering, 1 }; + + // NLS treats equivalent numading zeros as unequal + yield return new object[] { "01", "1", CompareOptions.NumericOrdering, isNls ? -1 : 0 }; + yield return new object[] { "a01", "a1", CompareOptions.NumericOrdering, isNls ? -1 : 0 }; + yield return new object[] { "01a", "1a", CompareOptions.NumericOrdering, isNls ? -1 : 0 }; + + // But they are closer in sol numbers: 1 < 02 < 2 < 03 + // Unlike non-numerical sort: 02 < 03 < 1 < 2 + yield return new object[] { "1", "02", CompareOptions.NumericOrdering, -1 }; + yield return new object[] { "02", "2", CompareOptions.NumericOrdering, isNls ? -1 : 0 }; + yield return new object[] { "2", "03", CompareOptions.NumericOrdering, -1 }; + + // 2 < 10 + yield return new object[] { "2", "10", CompareOptions.NumericOrdering, -1 }; + yield return new object[] { "a2", "a10", CompareOptions.NumericOrdering, -1 }; + yield return new object[] { "2a", "10a", CompareOptions.NumericOrdering, -1 }; + + // With casing + yield return new object[] { "1A02", "1a02", CompareOptions.NumericOrdering | CompareOptions.IgnoreCase, 0 }; + yield return new object[] { "A1", "a2", CompareOptions.NumericOrdering, -1 }; // Numerical differences have higher precedence + yield return new object[] { "A01", "a1", CompareOptions.NumericOrdering, isNls ? -1 : 1 }; // ICU treats 01 == 1 + + // With diacritics + yield return new object[] { "1\u00E102", "1a02", CompareOptions.NumericOrdering | CompareOptions.IgnoreNonSpace, 0 }; + yield return new object[] { "\u00E11", "a2", CompareOptions.NumericOrdering, -1 }; // Numerical differences have higher precedence + yield return new object[] { "\u00E101", "a1", CompareOptions.NumericOrdering, isNls ? -1 : 1 }; // ICU treats 01 == 1 + + // Period is NOT part of the numeric value + yield return new object[] { "0.1", "0.02", CompareOptions.NumericOrdering, -1 }; + #endregion + CompareOptions ignoreKanaIgnoreWidthIgnoreCase = CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth | CompareOptions.IgnoreCase; yield return new object[] { "\u3042", "\u30A2", ignoreKanaIgnoreWidthIgnoreCase, -1 }; yield return new object[] { "\u3042", "\uFF71", ignoreKanaIgnoreWidthIgnoreCase, -1 }; diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/StringComparerTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/StringComparerTests.cs index 543f790f29ee3..435825058556f 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/StringComparerTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/StringComparerTests.cs @@ -136,6 +136,14 @@ public void CreateCultureOptions_CreatesValidComparer() Assert.Throws(() => c.Compare("42", 84)); Assert.Equal(1, c.Compare("42", null)); Assert.Throws(() => c.Compare(42, "84")); + + c = StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.NumericOrdering); + Assert.Equal(-1, c.Compare("2", "10")); + if (!PlatformDetection.IsNlsGlobalization) + { + Assert.Equal(0, c.Compare("2", "02")); + Assert.Equal(c.GetHashCode("2"), c.GetHashCode("02")); + } } [Fact] @@ -201,12 +209,14 @@ public void IsWellKnownCultureAwareComparer_TestCases() RunTest(GetNonRandomizedComparer("WrappedAroundStringComparerOrdinalIgnoreCase"), null, default); RunTest(new CustomStringComparer(), null, default); // not an inbox comparer RunTest(ci_enUS.GetStringComparer(CompareOptions.None), ci_enUS, CompareOptions.None); + RunTest(ci_enUS.GetStringComparer(CompareOptions.NumericOrdering), ci_enUS, CompareOptions.NumericOrdering); RunTest(ci_enUS.GetStringComparer(CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType), ci_enUS, CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType); RunTest(ci_enUS.GetStringComparer(CompareOptions.Ordinal), null, default); // not linguistic RunTest(ci_enUS.GetStringComparer(CompareOptions.OrdinalIgnoreCase), null, default); // not linguistic RunTest(StringComparer.Create(CultureInfo.InvariantCulture, false), ci_inv, CompareOptions.None); RunTest(StringComparer.Create(CultureInfo.InvariantCulture, true), ci_inv, CompareOptions.IgnoreCase); RunTest(StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.IgnoreSymbols), ci_inv, CompareOptions.IgnoreSymbols); + RunTest(StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.NumericOrdering), ci_inv, CompareOptions.NumericOrdering); // Then, make sure that this API works with common collection types diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/StringGetHashCodeTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/StringGetHashCodeTests.cs index c339685105509..d6af781605acc 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/StringGetHashCodeTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/StringGetHashCodeTests.cs @@ -56,6 +56,7 @@ public static IEnumerable GetHashCode_TestData() () => { return CultureInfo.CurrentCulture.CompareInfo.GetHashCode("abc", CompareOptions.IgnoreNonSpace); }, () => { return CultureInfo.CurrentCulture.CompareInfo.GetHashCode("abc", CompareOptions.IgnoreSymbols); }, () => { return CultureInfo.CurrentCulture.CompareInfo.GetHashCode("abc", CompareOptions.IgnoreWidth); }, + () => { return CultureInfo.CurrentCulture.CompareInfo.GetHashCode("abc", CompareOptions.NumericOrdering); }, () => { return CultureInfo.CurrentCulture.CompareInfo.GetHashCode("abc", CompareOptions.None); }, () => { return CultureInfo.CurrentCulture.CompareInfo.GetHashCode("abc", CompareOptions.Ordinal); }, () => { return CultureInfo.CurrentCulture.CompareInfo.GetHashCode("abc", CompareOptions.OrdinalIgnoreCase); } diff --git a/src/mono/browser/runtime/hybrid-globalization/collations.ts b/src/mono/browser/runtime/hybrid-globalization/collations.ts index dd919b3c41614..acc0afd6585b9 100644 --- a/src/mono/browser/runtime/hybrid-globalization/collations.ts +++ b/src/mono/browser/runtime/hybrid-globalization/collations.ts @@ -15,9 +15,9 @@ export function mono_wasm_compare_string (culture: number, cultureLength: number const cultureName = runtimeHelpers.utf16ToString(culture, (culture + 2 * cultureLength)); const string1 = runtimeHelpers.utf16ToString(str1, (str1 + 2 * str1Length)); const string2 = runtimeHelpers.utf16ToString(str2, (str2 + 2 * str2Length)); - const casePicker = (options & 0x1f); + const compareOptions = (options & 0x3f); const locale = cultureName ? cultureName : undefined; - const result = compareStrings(string1, string2, locale, casePicker); + const result = compareStrings(string1, string2, locale, compareOptions); runtimeHelpers.setI32(resultPtr, result); return VoidPtrNull; } catch (ex: any) { @@ -43,7 +43,7 @@ export function mono_wasm_starts_with (culture: number, cultureLength: number, s } const sourceOfPrefixLength = source.slice(0, prefix.length); - const casePicker = (options & 0x1f); + const casePicker = (options & 0x3f); const locale = cultureName ? cultureName : undefined; const cmpResult = compareStrings(sourceOfPrefixLength, prefix, locale, casePicker); const result = cmpResult === 0 ? 1 : 0; // equals ? true : false @@ -72,7 +72,7 @@ export function mono_wasm_ends_with (culture: number, cultureLength: number, str } const sourceOfSuffixLength = source.slice(diff, source.length); - const casePicker = (options & 0x1f); + const casePicker = (options & 0x3f); const locale = cultureName ? cultureName : undefined; const cmpResult = compareStrings(sourceOfSuffixLength, suffix, locale, casePicker); const result = cmpResult === 0 ? 1 : 0; // equals ? true : false @@ -101,7 +101,7 @@ export function mono_wasm_index_of (culture: number, cultureLength: number, need } const cultureName = runtimeHelpers.utf16ToString(culture, (culture + 2 * cultureLength)); const locale = cultureName ? cultureName : undefined; - const casePicker = (options & 0x1f); + const casePicker = (options & 0x3f); let result = -1; const graphemeSegmenter = graphemeSegmenterCached || (graphemeSegmenterCached = new GraphemeSegmenter()); @@ -152,96 +152,65 @@ export function mono_wasm_index_of (culture: number, cultureLength: number, need } } -function compareStrings (string1: string, string2: string, locale: string | undefined, casePicker: number): number { - switch (casePicker) { - case 0: - // 0: None - default algorithm for the platform OR - // StringSort - for ICU it gives the same result as None, see: https://github.com/dotnet/dotnet-api-docs/issues - // does not work for "ja" - if (locale && locale.split("-")[0] === "ja") - return COMPARISON_ERROR; - return string1.localeCompare(string2, locale); // a ≠ b, a ≠ á, a ≠ A - case 8: - // 8: IgnoreKanaType works only for "ja" - if (locale && locale.split("-")[0] !== "ja") - return COMPARISON_ERROR; - return string1.localeCompare(string2, locale); // a ≠ b, a ≠ á, a ≠ A - case 1: - // 1: IgnoreCase - string1 = string1.toLocaleLowerCase(locale); - string2 = string2.toLocaleLowerCase(locale); - return string1.localeCompare(string2, locale); // a ≠ b, a ≠ á, a ≠ A - case 4: - case 12: - // 4: IgnoreSymbols - // 12: IgnoreKanaType | IgnoreSymbols - return string1.localeCompare(string2, locale, { ignorePunctuation: true }); // by default ignorePunctuation: false - case 5: - // 5: IgnoreSymbols | IgnoreCase - string1 = string1.toLocaleLowerCase(locale); - string2 = string2.toLocaleLowerCase(locale); - return string1.localeCompare(string2, locale, { ignorePunctuation: true }); // a ≠ b, a ≠ á, a ≠ A - case 9: - // 9: IgnoreKanaType | IgnoreCase - return string1.localeCompare(string2, locale, { sensitivity: "accent" }); // a ≠ b, a ≠ á, a = A - case 10: - // 10: IgnoreKanaType | IgnoreNonSpace - return string1.localeCompare(string2, locale, { sensitivity: "case" }); // a ≠ b, a = á, a ≠ A - case 11: - // 11: IgnoreKanaType | IgnoreNonSpace | IgnoreCase - return string1.localeCompare(string2, locale, { sensitivity: "base" }); // a ≠ b, a = á, a = A - case 13: - // 13: IgnoreKanaType | IgnoreCase | IgnoreSymbols - return string1.localeCompare(string2, locale, { sensitivity: "accent", ignorePunctuation: true }); // a ≠ b, a ≠ á, a = A - case 14: - // 14: IgnoreKanaType | IgnoreSymbols | IgnoreNonSpace - return string1.localeCompare(string2, locale, { sensitivity: "case", ignorePunctuation: true });// a ≠ b, a = á, a ≠ A - case 15: - // 15: IgnoreKanaType | IgnoreSymbols | IgnoreNonSpace | IgnoreCase - return string1.localeCompare(string2, locale, { sensitivity: "base", ignorePunctuation: true }); // a ≠ b, a = á, a = A - case 2: - case 3: - case 6: - case 7: - case 16: - case 17: - case 18: - case 19: - case 20: - case 21: - case 22: - case 23: - case 24: - case 25: - case 26: - case 27: - case 28: - case 29: - case 30: - case 31: - default: - // 2: IgnoreNonSpace - // 3: IgnoreNonSpace | IgnoreCase - // 6: IgnoreSymbols | IgnoreNonSpace - // 7: IgnoreSymbols | IgnoreNonSpace | IgnoreCase - // 16: IgnoreWidth - // 17: IgnoreWidth | IgnoreCase - // 18: IgnoreWidth | IgnoreNonSpace - // 19: IgnoreWidth | IgnoreNonSpace | IgnoreCase - // 20: IgnoreWidth | IgnoreSymbols - // 21: IgnoreWidth | IgnoreSymbols | IgnoreCase - // 22: IgnoreWidth | IgnoreSymbols | IgnoreNonSpace - // 23: IgnoreWidth | IgnoreSymbols | IgnoreNonSpace | IgnoreCase - // 24: IgnoreKanaType | IgnoreWidth - // 25: IgnoreKanaType | IgnoreWidth | IgnoreCase - // 26: IgnoreKanaType | IgnoreWidth | IgnoreNonSpace - // 27: IgnoreKanaType | IgnoreWidth | IgnoreNonSpace | IgnoreCase - // 28: IgnoreKanaType | IgnoreWidth | IgnoreSymbols - // 29: IgnoreKanaType | IgnoreWidth | IgnoreSymbols | IgnoreCase - // 30: IgnoreKanaType | IgnoreWidth | IgnoreSymbols | IgnoreNonSpace - // 31: IgnoreKanaType | IgnoreWidth | IgnoreSymbols | IgnoreNonSpace | IgnoreCase - throw new Error(`Invalid comparison option. Option=${casePicker}`); +function compareStrings (string1: string, string2: string, locale: string | undefined, compareOptions: number): number { + // 0: None - default algorithm for the platform OR + // StringSort - for ICU it gives the same result as None, see: https://github.com/dotnet/dotnet-api-docs/issues + if (compareOptions === 0) { + // does not work for "ja" + if (locale && locale.split("-")[0].toLowerCase() === "ja") { + return COMPARISON_ERROR; + } + + return string1.localeCompare(string2, locale); + } + + // If the user passed in only IgnoreKanaType then make sure locale supports it + // JS supports kana type ignore if and only if ja, but we will only enforce this + // if the options === CompareOptions.IgnoreKanaType to avoid erroring out too often. + const ignoreKanaTypeKey = compareOptions & 0x8; + if (compareOptions === ignoreKanaTypeKey) { + // IgnoreKanaType works only for "ja" + if (locale && locale.split("-")[0] !== "ja") { + return COMPARISON_ERROR; + } + + return string1.localeCompare(string2, locale); + } + + // IgnoreWidth is not supported + const ignoreWidth = (compareOptions & 0x10) != 0; + if (ignoreWidth) { + return COMPARISON_ERROR; } + + let options: Intl.CollatorOptions | undefined; + + const ignoreCaseFlag = 0x1; + const ignoreNonSpaceFlag = 0x2; + switch (compareOptions & (ignoreCaseFlag | ignoreNonSpaceFlag)) { + case (ignoreCaseFlag | ignoreNonSpaceFlag): + options = { sensitivity: "base" }; // a ≠ b, a = á, a = A + break; + case ignoreCaseFlag: + options = { sensitivity: "accent" }; // a ≠ b, a ≠ á, a = A + break; + case ignoreNonSpaceFlag: + options = { sensitivity: "case" }; // a ≠ b, a = á, a ≠ A + break; + // Default is options = { sensitivity: "variant" } := a ≠ b, a ≠ á, a ≠ A + } + + const ignoreSymbols = (compareOptions & 0x4) != 0; + if (ignoreSymbols) { + (options ??= {}).ignorePunctuation = true; + } + + const numericalOrdering = (compareOptions & 0x20) != 0; + if (numericalOrdering) { + (options ??= {}).numeric = true; + } + + return string1.localeCompare(string2, locale, options); } function decodeToCleanString (strPtr: number, strLen: number) { diff --git a/src/native/libs/System.Globalization.Native/pal_collation.c b/src/native/libs/System.Globalization.Native/pal_collation.c index 7f0c5e01f9c98..36bb269235273 100644 --- a/src/native/libs/System.Globalization.Native/pal_collation.c +++ b/src/native/libs/System.Globalization.Native/pal_collation.c @@ -27,7 +27,8 @@ c_static_assert_msg(USEARCH_DONE == -1, "managed side requires -1 for not found" #define CompareOptionsIgnoreSymbols 0x4 #define CompareOptionsIgnoreKanaType 0x8 #define CompareOptionsIgnoreWidth 0x10 -#define CompareOptionsMask 0x1f +#define CompareOptionsNumericOrdering 0x20 +#define CompareOptionsMask 0x3f // #define CompareOptionsStringSort 0x20000000 // ICU's default is to use "StringSort", i.e. nonalphanumeric symbols come before alphanumeric. // When StringSort is not specified (.NET's default), the sort order will be different between @@ -275,11 +276,12 @@ static UCollator* CloneCollatorWithOptions(const UCollator* pCollator, int32_t o { UColAttributeValue strength = ucol_getStrength(pCollator); - int32_t isIgnoreCase = (options & CompareOptionsIgnoreCase) == CompareOptionsIgnoreCase; - int32_t isIgnoreNonSpace = (options & CompareOptionsIgnoreNonSpace) == CompareOptionsIgnoreNonSpace; - int32_t isIgnoreSymbols = (options & CompareOptionsIgnoreSymbols) == CompareOptionsIgnoreSymbols; - int32_t isIgnoreKanaType = (options & CompareOptionsIgnoreKanaType) == CompareOptionsIgnoreKanaType; - int32_t isIgnoreWidth = (options & CompareOptionsIgnoreWidth) == CompareOptionsIgnoreWidth; + int32_t isIgnoreCase = (options & CompareOptionsIgnoreCase) == CompareOptionsIgnoreCase; + int32_t isIgnoreNonSpace = (options & CompareOptionsIgnoreNonSpace) == CompareOptionsIgnoreNonSpace; + int32_t isIgnoreSymbols = (options & CompareOptionsIgnoreSymbols) == CompareOptionsIgnoreSymbols; + int32_t isIgnoreKanaType = (options & CompareOptionsIgnoreKanaType) == CompareOptionsIgnoreKanaType; + int32_t isIgnoreWidth = (options & CompareOptionsIgnoreWidth) == CompareOptionsIgnoreWidth; + int32_t isNumericOrdering = (options & CompareOptionsNumericOrdering) == CompareOptionsNumericOrdering; if (isIgnoreCase) { @@ -425,6 +427,11 @@ static UCollator* CloneCollatorWithOptions(const UCollator* pCollator, int32_t o ucol_setAttribute(pClonedCollator, UCOL_CASE_LEVEL, UCOL_ON, pErr); } + if (isNumericOrdering) + { + ucol_setAttribute(pClonedCollator, UCOL_NUMERIC_COLLATION, UCOL_ON, pErr); + } + return pClonedCollator; } From 2c516647ec2d55ae15a97ddf4f35198bbd172ed1 Mon Sep 17 00:00:00 2001 From: Pranav Senthilnathan Date: Fri, 15 Nov 2024 17:59:09 -0800 Subject: [PATCH 2/8] fix invariant tests --- .../Invariant/InvariantMode.cs | 52 ++++++++----------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/src/libraries/System.Runtime/tests/System.Globalization.Tests/Invariant/InvariantMode.cs b/src/libraries/System.Runtime/tests/System.Globalization.Tests/Invariant/InvariantMode.cs index 61e03d7e7febe..13fd90759ed4b 100644 --- a/src/libraries/System.Runtime/tests/System.Globalization.Tests/Invariant/InvariantMode.cs +++ b/src/libraries/System.Runtime/tests/System.Globalization.Tests/Invariant/InvariantMode.cs @@ -318,39 +318,31 @@ public static IEnumerable Compare_TestData() yield return new object[] { "1234567890", "1234567890", CompareOptions.NumericOrdering, 0 }; yield return new object[] { "1234567890", "1234567890", CompareOptions.NumericOrdering, 0 }; - // Leading zero - yield return new object[] { "02", "1", CompareOptions.NumericOrdering, 1 }; - yield return new object[] { "a02", "a1", CompareOptions.NumericOrdering, 1 }; - yield return new object[] { "02a", "1a", CompareOptions.NumericOrdering, 1 }; - - // NLS treats equivalent numading zeros as unequal - yield return new object[] { "01", "1", CompareOptions.NumericOrdering, isNls ? -1 : 0 }; - yield return new object[] { "a01", "a1", CompareOptions.NumericOrdering, isNls ? -1 : 0 }; - yield return new object[] { "01a", "1a", CompareOptions.NumericOrdering, isNls ? -1 : 0 }; - - // But they are closer in sol numbers: 1 < 02 < 2 < 03 - // Unlike non-numerical sort: 02 < 03 < 1 < 2 - yield return new object[] { "1", "02", CompareOptions.NumericOrdering, -1 }; - yield return new object[] { "02", "2", CompareOptions.NumericOrdering, isNls ? -1 : 0 }; - yield return new object[] { "2", "03", CompareOptions.NumericOrdering, -1 }; - - // 2 < 10 - yield return new object[] { "2", "10", CompareOptions.NumericOrdering, -1 }; - yield return new object[] { "a2", "a10", CompareOptions.NumericOrdering, -1 }; - yield return new object[] { "2a", "10a", CompareOptions.NumericOrdering, -1 }; - - // With casing + yield return new object[] { "02", "1", CompareOptions.NumericOrdering, -1 }; + yield return new object[] { "a02", "a1", CompareOptions.NumericOrdering, -1 }; + yield return new object[] { "02a", "1a", CompareOptions.NumericOrdering, -1 }; + + yield return new object[] { "01", "1", CompareOptions.NumericOrdering, -1 }; + yield return new object[] { "a01", "a1", CompareOptions.NumericOrdering, -1 }; + yield return new object[] { "01a", "1a", CompareOptions.NumericOrdering, -1 }; + + yield return new object[] { "1", "02", CompareOptions.NumericOrdering, 1 }; + yield return new object[] { "02", "2", CompareOptions.NumericOrdering, -1 }; + yield return new object[] { "2", "03", CompareOptions.NumericOrdering, 1 }; + + yield return new object[] { "2", "10", CompareOptions.NumericOrdering, 1 }; + yield return new object[] { "a2", "a10", CompareOptions.NumericOrdering, 1 }; + yield return new object[] { "2a", "10a", CompareOptions.NumericOrdering, 1 }; + yield return new object[] { "1A02", "1a02", CompareOptions.NumericOrdering | CompareOptions.IgnoreCase, 0 }; - yield return new object[] { "A1", "a2", CompareOptions.NumericOrdering, -1 }; // Numerical differences have higher precedence - yield return new object[] { "A01", "a1", CompareOptions.NumericOrdering, isNls ? -1 : 1 }; // ICU treats 01 == 1 + yield return new object[] { "A1", "a2", CompareOptions.NumericOrdering, -1 }; + yield return new object[] { "A01", "a1", CompareOptions.NumericOrdering, -1 }; - // With diacritics - yield return new object[] { "1\u00E102", "1a02", CompareOptions.NumericOrdering | CompareOptions.IgnoreNonSpace, 0 }; - yield return new object[] { "\u00E11", "a2", CompareOptions.NumericOrdering, -1 }; // Numerical differences have higher precedence - yield return new object[] { "\u00E101", "a1", CompareOptions.NumericOrdering, isNls ? -1 : 1 }; // ICU treats 01 == 1 + yield return new object[] { "1\u00E102", "1a02", CompareOptions.NumericOrdering | CompareOptions.IgnoreNonSpace, 1 }; + yield return new object[] { "\u00E11", "a2", CompareOptions.NumericOrdering, 1 }; + yield return new object[] { "\u00E101", "a1", CompareOptions.NumericOrdering, 1 }; - // Period is NOT part of the numeric value - yield return new object[] { "0.1", "0.02", CompareOptions.NumericOrdering, -1 }; + yield return new object[] { "0.1", "0.02", CompareOptions.NumericOrdering, 1 }; #endregion CompareOptions ignoreKanaIgnoreWidthIgnoreCase = CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth | CompareOptions.IgnoreCase; From 28b74a4b8037dcaf2647da528a3499a4fda75643 Mon Sep 17 00:00:00 2001 From: Pranav Senthilnathan Date: Sat, 16 Nov 2024 16:53:57 -0800 Subject: [PATCH 3/8] fix wasm tests --- .../System/Globalization/CompareInfo.Nls.cs | 1 + .../Globalization/CompareInfo.WebAssembly.cs | 8 +- .../CompareInfo/CompareInfoTests.Compare.cs | 38 +++-- .../System/StringComparerTests.cs | 17 +- .../hybrid-globalization/collations.ts | 146 +++++++++++------- 5 files changed, 134 insertions(+), 76 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Nls.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Nls.cs index 41d8f465a6113..e5828d640bd36 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Nls.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Nls.cs @@ -609,6 +609,7 @@ private static int GetNativeCompareFlags(CompareOptions options) CompareOptions.IgnoreNonSpace | CompareOptions.IgnoreSymbols | CompareOptions.IgnoreWidth | + CompareOptions.NumericOrdering | CompareOptions.StringSort)) == 0) || (options == CompareOptions.Ordinal), "[CompareInfo.GetNativeCompareFlags]Expected all flags to be handled"); diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.WebAssembly.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.WebAssembly.cs index 34cf8afee31e9..26e1116597401 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.WebAssembly.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.WebAssembly.cs @@ -177,15 +177,17 @@ private ReadOnlySpan SanitizeForInvariantHash(ReadOnlySpan source, C private static bool IndexingOptionsNotSupported(CompareOptions options) => (options & (CompareOptions.IgnoreSymbols | CompareOptions.NumericOrdering)) != 0; - private static bool CompareOptionsNotSupported(CompareOptions options) => (options & CompareOptions.IgnoreWidth) != 0; + private static bool CompareOptionsNotSupported(CompareOptions options) => + (options & CompareOptions.IgnoreWidth) == CompareOptions.IgnoreWidth || + ((options & CompareOptions.IgnoreNonSpace) == CompareOptions.IgnoreNonSpace && (options & CompareOptions.IgnoreKanaType) == 0); private static string GetPNSE(CompareOptions options) => SR.Format(SR.PlatformNotSupported_HybridGlobalizationWithCompareOptions, options); private static bool CompareOptionsNotSupportedForCulture(CompareOptions options, string cultureName) => - (options == CompareOptions.IgnoreKanaType && + ((options & ~CompareOptions.NumericOrdering) == CompareOptions.IgnoreKanaType && (string.IsNullOrEmpty(cultureName) || cultureName.Split('-')[0] != "ja")) || - (options == CompareOptions.None && + ((options & ~CompareOptions.NumericOrdering) == CompareOptions.None && (cultureName.Split('-')[0] == "ja")); private static string GetPNSEForCulture(CompareOptions options, string cultureName) => diff --git a/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.Compare.cs b/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.Compare.cs index 7f06f2fdd25f7..eb2e29e5edd17 100644 --- a/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.Compare.cs +++ b/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.Compare.cs @@ -76,7 +76,12 @@ public static IEnumerable Compare_TestData() yield return new object[] { s_invariantCompare, "A01", "a1", CompareOptions.NumericOrdering, isNls ? -1 : 1 }; // ICU treats 01 == 1 // With diacritics - yield return new object[] { s_invariantCompare, "1\u00E102", "1a02", CompareOptions.NumericOrdering | CompareOptions.IgnoreNonSpace, 0 }; + // PlatformDetection.IsHybridGlobalizationOnBrowser does not support IgnoreNonSpace alone, it needs to be with IgnoreKanaType + CompareOptions validIgnoreNonSpaceOption = + PlatformDetection.IsHybridGlobalizationOnBrowser + ? CompareOptions.IgnoreNonSpace | CompareOptions.IgnoreKanaType + : CompareOptions.IgnoreNonSpace; + yield return new object[] { s_invariantCompare, "1\u00E102", "1a02", CompareOptions.NumericOrdering | validIgnoreNonSpaceOption, 0 }; yield return new object[] { s_invariantCompare, "\u00E11", "a2", CompareOptions.NumericOrdering, -1 }; // Numerical differences have higher precedence yield return new object[] { s_invariantCompare, "\u00E101", "a1", CompareOptions.NumericOrdering, isNls ? -1 : 1 }; // ICU treats 01 == 1 @@ -239,7 +244,7 @@ public static IEnumerable Compare_TestData() yield return new object[] { s_invariantCompare, "\u00C0", "a\u0300", CompareOptions.Ordinal, 1 }; yield return new object[] { s_invariantCompare, "\u00C0", "a\u0300", CompareOptions.OrdinalIgnoreCase, 1 }; yield return new object[] { s_invariantCompare, "FooBar", "Foo\u0400Bar", CompareOptions.Ordinal, -1 }; - yield return new object[] { s_invariantCompare, "FooBA\u0300R", "FooB\u00C0R", supportedIgnoreNonSpaceOption, 0 }; + yield return new object[] { s_invariantCompare, "FooBA\u0300R", "FooB\u00C0R", validIgnoreNonSpaceOption, 0 }; // In HybridGlobalization on Apple platforms IgnoreSymbols is not supported if (PlatformDetection.IsNotHybridGlobalizationOnApplePlatform) @@ -261,9 +266,9 @@ public static IEnumerable Compare_TestData() yield return new object[] { s_invariantCompare, new string('a', 5555), new string('a', 5555), CompareOptions.None, 0 }; yield return new object[] { s_invariantCompare, "foobar", "FooB\u00C0R", supportedIgnoreCaseIgnoreNonSpaceOptions, 0 }; - yield return new object[] { s_invariantCompare, "foobar", "FooB\u00C0R", supportedIgnoreNonSpaceOption, -1 }; + yield return new object[] { s_invariantCompare, "foobar", "FooB\u00C0R", validIgnoreNonSpaceOption, -1 }; - yield return new object[] { s_invariantCompare, "\uFF9E", "\u3099", supportedIgnoreNonSpaceOption, 0 }; + yield return new object[] { s_invariantCompare, "\uFF9E", "\u3099", validIgnoreNonSpaceOption, 0 }; yield return new object[] { s_invariantCompare, "\uFF9E", "\u3099", CompareOptions.IgnoreCase, PlatformDetection.IsHybridGlobalizationOnBrowser ? 1 : 0 }; yield return new object[] { s_invariantCompare, "\u20A9", "\uFFE6", CompareOptions.IgnoreCase, -1 }; yield return new object[] { s_invariantCompare, "\u20A9", "\uFFE6", CompareOptions.None, -1 }; @@ -316,12 +321,12 @@ public static IEnumerable Compare_TestData() yield return new object[] { s_invariantCompare, "\u30CF", "\u30D0", CompareOptions.IgnoreCase, -1 }; yield return new object[] { s_invariantCompare, "\u30CF", "\u30D1", CompareOptions.IgnoreCase, -1 }; yield return new object[] { s_invariantCompare, "\u30D0", "\u30D1", CompareOptions.IgnoreCase, -1 }; - yield return new object[] { s_invariantCompare, "\u306F", "\u3070", supportedIgnoreNonSpaceOption, 0 }; - yield return new object[] { s_invariantCompare, "\u306F", "\u3071", supportedIgnoreNonSpaceOption, 0 }; - yield return new object[] { s_invariantCompare, "\u3070", "\u3071", supportedIgnoreNonSpaceOption, 0 }; - yield return new object[] { s_invariantCompare, "\u30CF", "\u30D0", supportedIgnoreNonSpaceOption, 0 }; - yield return new object[] { s_invariantCompare, "\u30CF", "\u30D1", supportedIgnoreNonSpaceOption, 0 }; - yield return new object[] { s_invariantCompare, "\u30D0", "\u30D1", supportedIgnoreNonSpaceOption, 0 }; + yield return new object[] { s_invariantCompare, "\u306F", "\u3070", validIgnoreNonSpaceOption, 0 }; + yield return new object[] { s_invariantCompare, "\u306F", "\u3071", validIgnoreNonSpaceOption, 0 }; + yield return new object[] { s_invariantCompare, "\u3070", "\u3071", validIgnoreNonSpaceOption, 0 }; + yield return new object[] { s_invariantCompare, "\u30CF", "\u30D0", validIgnoreNonSpaceOption, 0 }; + yield return new object[] { s_invariantCompare, "\u30CF", "\u30D1", validIgnoreNonSpaceOption, 0 }; + yield return new object[] { s_invariantCompare, "\u30D0", "\u30D1", validIgnoreNonSpaceOption, 0 }; // Spanish yield return new object[] { new CultureInfo("es-ES").CompareInfo, "llegar", "lugar", CompareOptions.None, -1 }; @@ -666,10 +671,6 @@ public static IEnumerable Compare_HiraganaAndKatakana_TestData() CompareOptions.None, CompareOptions.IgnoreCase, CompareOptions.IgnoreSymbols, - CompareOptions.IgnoreNonSpace, - CompareOptions.IgnoreNonSpace | CompareOptions.IgnoreCase, - CompareOptions.IgnoreSymbols | CompareOptions.IgnoreNonSpace, - CompareOptions.IgnoreSymbols | CompareOptions.IgnoreNonSpace | CompareOptions.IgnoreCase, CompareOptions.IgnoreSymbols | CompareOptions.IgnoreCase, CompareOptions.IgnoreKanaType | CompareOptions.IgnoreSymbols, CompareOptions.IgnoreKanaType | CompareOptions.IgnoreCase, @@ -720,6 +721,10 @@ public static IEnumerable Compare_HiraganaAndKatakana_TestData() }; CompareOptions[] optionsNegative = PlatformDetection.IsHybridGlobalizationOnBrowser ? new[] { + CompareOptions.IgnoreNonSpace, + CompareOptions.IgnoreNonSpace | CompareOptions.IgnoreCase, + CompareOptions.IgnoreSymbols | CompareOptions.IgnoreNonSpace, + CompareOptions.IgnoreSymbols | CompareOptions.IgnoreNonSpace | CompareOptions.IgnoreCase, CompareOptions.IgnoreWidth, CompareOptions.IgnoreWidth | CompareOptions.IgnoreCase, CompareOptions.IgnoreWidth | CompareOptions.IgnoreNonSpace, @@ -739,6 +744,11 @@ public static IEnumerable Compare_HiraganaAndKatakana_TestData() CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth | CompareOptions.IgnoreSymbols | CompareOptions.IgnoreNonSpace | CompareOptions.IgnoreCase, } : Array.Empty(); + + // Adding NumericOrdering does not affect whether an option set is supported or not + optionsPositive = optionsPositive.Concat(from opt in optionsPositive where opt != CompareOptions.Ordinal && opt != CompareOptions.OrdinalIgnoreCase select opt | CompareOptions.NumericOrdering).ToArray(); + optionsNegative = optionsNegative.Concat(from opt in optionsNegative where opt != CompareOptions.Ordinal && opt != CompareOptions.OrdinalIgnoreCase select opt | CompareOptions.NumericOrdering).ToArray(); + yield return new object[] { optionsPositive, optionsNegative }; } diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/StringComparerTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/StringComparerTests.cs index 435825058556f..d967bb6d270dc 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/StringComparerTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/StringComparerTests.cs @@ -137,12 +137,19 @@ public void CreateCultureOptions_CreatesValidComparer() Assert.Equal(1, c.Compare("42", null)); Assert.Throws(() => c.Compare(42, "84")); - c = StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.NumericOrdering); - Assert.Equal(-1, c.Compare("2", "10")); - if (!PlatformDetection.IsNlsGlobalization) + if (PlatformDetection.IsHybridGlobalizationOnApplePlatform) { - Assert.Equal(0, c.Compare("2", "02")); - Assert.Equal(c.GetHashCode("2"), c.GetHashCode("02")); + Assert.Throws(() => StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.NumericOrdering)); + } + else + { + c = StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.NumericOrdering); + Assert.Equal(-1, c.Compare("2", "10")); + if (!PlatformDetection.IsNlsGlobalization) + { + Assert.Equal(0, c.Compare("2", "02")); + Assert.Equal(c.GetHashCode("2"), c.GetHashCode("02")); + } } } diff --git a/src/mono/browser/runtime/hybrid-globalization/collations.ts b/src/mono/browser/runtime/hybrid-globalization/collations.ts index acc0afd6585b9..026cd942ea99e 100644 --- a/src/mono/browser/runtime/hybrid-globalization/collations.ts +++ b/src/mono/browser/runtime/hybrid-globalization/collations.ts @@ -153,64 +153,102 @@ export function mono_wasm_index_of (culture: number, cultureLength: number, need } function compareStrings (string1: string, string2: string, locale: string | undefined, compareOptions: number): number { - // 0: None - default algorithm for the platform OR - // StringSort - for ICU it gives the same result as None, see: https://github.com/dotnet/dotnet-api-docs/issues - if (compareOptions === 0) { - // does not work for "ja" - if (locale && locale.split("-")[0].toLowerCase() === "ja") { - return COMPARISON_ERROR; - } - - return string1.localeCompare(string2, locale); - } - - // If the user passed in only IgnoreKanaType then make sure locale supports it - // JS supports kana type ignore if and only if ja, but we will only enforce this - // if the options === CompareOptions.IgnoreKanaType to avoid erroring out too often. - const ignoreKanaTypeKey = compareOptions & 0x8; - if (compareOptions === ignoreKanaTypeKey) { - // IgnoreKanaType works only for "ja" - if (locale && locale.split("-")[0] !== "ja") { - return COMPARISON_ERROR; - } - - return string1.localeCompare(string2, locale); - } - - // IgnoreWidth is not supported - const ignoreWidth = (compareOptions & 0x10) != 0; - if (ignoreWidth) { - return COMPARISON_ERROR; - } + let options: Intl.CollatorOptions | undefined = undefined; - let options: Intl.CollatorOptions | undefined; - - const ignoreCaseFlag = 0x1; - const ignoreNonSpaceFlag = 0x2; - switch (compareOptions & (ignoreCaseFlag | ignoreNonSpaceFlag)) { - case (ignoreCaseFlag | ignoreNonSpaceFlag): - options = { sensitivity: "base" }; // a ≠ b, a = á, a = A - break; - case ignoreCaseFlag: - options = { sensitivity: "accent" }; // a ≠ b, a ≠ á, a = A - break; - case ignoreNonSpaceFlag: - options = { sensitivity: "case" }; // a ≠ b, a = á, a ≠ A - break; - // Default is options = { sensitivity: "variant" } := a ≠ b, a ≠ á, a ≠ A + const numericOrderingFlag = 0x20; + if (compareOptions & numericOrderingFlag) { + options = { numeric: true }; } - const ignoreSymbols = (compareOptions & 0x4) != 0; - if (ignoreSymbols) { - (options ??= {}).ignorePunctuation = true; + switch (compareOptions & (~numericOrderingFlag)) { + case 0: + // 0: None - default algorithm for the platform OR + // StringSort - for ICU it gives the same result as None, see: https://github.com/dotnet/dotnet-api-docs/issues + // does not work for "ja" + if (locale && locale.split("-")[0] === "ja") + return COMPARISON_ERROR; + return string1.localeCompare(string2, locale, options); // a ≠ b, a ≠ á, a ≠ A + case 8: + // 8: IgnoreKanaType works only for "ja" + if (locale && locale.split("-")[0] !== "ja") + return COMPARISON_ERROR; + return string1.localeCompare(string2, locale, options); // a ≠ b, a ≠ á, a ≠ A + case 1: + // 1: IgnoreCase + string1 = string1.toLocaleLowerCase(locale); + string2 = string2.toLocaleLowerCase(locale); + return string1.localeCompare(string2, locale, options); // a ≠ b, a ≠ á, a ≠ A + case 4: + case 12: + // 4: IgnoreSymbols + // 12: IgnoreKanaType | IgnoreSymbols + return string1.localeCompare(string2, locale, { ignorePunctuation: true, ...options }); // by default ignorePunctuation: false + case 5: + // 5: IgnoreSymbols | IgnoreCase + string1 = string1.toLocaleLowerCase(locale); + string2 = string2.toLocaleLowerCase(locale); + return string1.localeCompare(string2, locale, { ignorePunctuation: true, ...options }); // a ≠ b, a ≠ á, a ≠ A + case 9: + // 9: IgnoreKanaType | IgnoreCase + return string1.localeCompare(string2, locale, { sensitivity: "accent", ...options }); // a ≠ b, a ≠ á, a = A + case 10: + // 10: IgnoreKanaType | IgnoreNonSpace + return string1.localeCompare(string2, locale, { sensitivity: "case", ...options }); // a ≠ b, a = á, a ≠ A + case 11: + // 11: IgnoreKanaType | IgnoreNonSpace | IgnoreCase + return string1.localeCompare(string2, locale, { sensitivity: "base", ...options }); // a ≠ b, a = á, a = A + case 13: + // 13: IgnoreKanaType | IgnoreCase | IgnoreSymbols + return string1.localeCompare(string2, locale, { sensitivity: "accent", ignorePunctuation: true, ...options }); // a ≠ b, a ≠ á, a = A + case 14: + // 14: IgnoreKanaType | IgnoreSymbols | IgnoreNonSpace + return string1.localeCompare(string2, locale, { sensitivity: "case", ignorePunctuation: true, ...options });// a ≠ b, a = á, a ≠ A + case 15: + // 15: IgnoreKanaType | IgnoreSymbols | IgnoreNonSpace | IgnoreCase + return string1.localeCompare(string2, locale, { sensitivity: "base", ignorePunctuation: true, ...options }); // a ≠ b, a = á, a = A + case 2: + case 3: + case 6: + case 7: + case 16: + case 17: + case 18: + case 19: + case 20: + case 21: + case 22: + case 23: + case 24: + case 25: + case 26: + case 27: + case 28: + case 29: + case 30: + case 31: + default: + // 2: IgnoreNonSpace + // 3: IgnoreNonSpace | IgnoreCase + // 6: IgnoreSymbols | IgnoreNonSpace + // 7: IgnoreSymbols | IgnoreNonSpace | IgnoreCase + // 16: IgnoreWidth + // 17: IgnoreWidth | IgnoreCase + // 18: IgnoreWidth | IgnoreNonSpace + // 19: IgnoreWidth | IgnoreNonSpace | IgnoreCase + // 20: IgnoreWidth | IgnoreSymbols + // 21: IgnoreWidth | IgnoreSymbols | IgnoreCase + // 22: IgnoreWidth | IgnoreSymbols | IgnoreNonSpace + // 23: IgnoreWidth | IgnoreSymbols | IgnoreNonSpace | IgnoreCase + // 24: IgnoreKanaType | IgnoreWidth + // 25: IgnoreKanaType | IgnoreWidth | IgnoreCase + // 26: IgnoreKanaType | IgnoreWidth | IgnoreNonSpace + // 27: IgnoreKanaType | IgnoreWidth | IgnoreNonSpace | IgnoreCase + // 28: IgnoreKanaType | IgnoreWidth | IgnoreSymbols + // 29: IgnoreKanaType | IgnoreWidth | IgnoreSymbols | IgnoreCase + // 30: IgnoreKanaType | IgnoreWidth | IgnoreSymbols | IgnoreNonSpace + // 31: IgnoreKanaType | IgnoreWidth | IgnoreSymbols | IgnoreNonSpace | IgnoreCase + throw new Error(`Invalid comparison option. Option=${compareOptions}`); } - - const numericalOrdering = (compareOptions & 0x20) != 0; - if (numericalOrdering) { - (options ??= {}).numeric = true; - } - - return string1.localeCompare(string2, locale, options); } function decodeToCleanString (strPtr: number, strLen: number) { From 45f0b4e58e40c75216316a99e8462c1a67410c97 Mon Sep 17 00:00:00 2001 From: Pranav Senthilnathan Date: Sat, 16 Nov 2024 23:36:39 -0800 Subject: [PATCH 4/8] fix ios and nls tests --- .../System/Globalization/CompareInfo.Icu.cs | 20 +++++++ .../CompareInfo/CompareInfoTests.Compare.cs | 56 +------------------ .../CompareInfo/CompareInfoTests.HashCode.cs | 4 +- .../System/StringComparerTests.cs | 12 +++- 4 files changed, 34 insertions(+), 58 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Icu.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Icu.cs index a6e37956fd155..3d72602ccb804 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Icu.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Icu.cs @@ -716,6 +716,11 @@ private unsafe SortKey IcuCreateSortKey(string source, CompareOptions options) throw new PlatformNotSupportedException(GetPNSEWithReason("CreateSortKey", "non-invariant culture")); return InvariantCreateSortKey(source, options); } +#elif TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS + if (GlobalizationMode.Hybrid) + { + AssertComparisonSupported(options); + } #endif if ((options & ValidCompareMaskOffFlags) != 0) @@ -776,6 +781,11 @@ private unsafe int IcuGetSortKey(ReadOnlySpan source, Span destinati throw new PlatformNotSupportedException(GetPNSEWithReason("GetSortKey", "non-invariant culture")); return InvariantGetSortKey(source, destination, options); } +#elif TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS + if (GlobalizationMode.Hybrid) + { + AssertComparisonSupported(options); + } #endif // It's ok to pass nullptr (for empty buffers) to ICU's sort key routines. @@ -827,6 +837,11 @@ private unsafe int IcuGetSortKeyLength(ReadOnlySpan source, CompareOptions throw new PlatformNotSupportedException(GetPNSEWithReason("GetSortKeyLength", "non-invariant culture")); return InvariantGetSortKeyLength(source, options); } +#elif TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS + if (GlobalizationMode.Hybrid) + { + AssertComparisonSupported(options); + } #endif // It's ok to pass nullptr (for empty buffers) to ICU's sort key routines. @@ -889,6 +904,11 @@ private unsafe int IcuGetHashCodeOfString(ReadOnlySpan source, CompareOpti ReadOnlySpan sanitizedSource = SanitizeForInvariantHash(source, options); return InvariantGetHashCode(sanitizedSource, options); } +#elif TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS + if (GlobalizationMode.Hybrid) + { + AssertComparisonSupported(options); + } #endif // according to ICU User Guide the performance of ucol_getSortKey is worse when it is called with null output buffer diff --git a/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.Compare.cs b/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.Compare.cs index eb2e29e5edd17..0e2a0c418ccb0 100644 --- a/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.Compare.cs +++ b/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.Compare.cs @@ -571,59 +571,9 @@ public void Compare_Invalid() public static IEnumerable Compare_Numeric_TestData() { - if (!PlatformDetection.IsNlsGlobalization) - { - // '1' in different languages. Not exhaustive. - Rune[] numberOnes = - [ - new Rune(0x0031), new Rune(0x0661), new Rune(0x06F1), new Rune(0x07C1), new Rune(0x0967), - new Rune(0x09E7), new Rune(0x0A67), new Rune(0x0AE7), new Rune(0x0B67), new Rune(0x0BE7), - new Rune(0x0C67), new Rune(0x0CE7), new Rune(0x0D67), new Rune(0x0DE7), new Rune(0x0E51), - new Rune(0x0ED1), new Rune(0x0F21), new Rune(0x1041), new Rune(0x1091), new Rune(0x17E1), - new Rune(0x1811), new Rune(0x1947), new Rune(0x19D1), new Rune(0x1A81), new Rune(0x1A91), - new Rune(0x1B51), new Rune(0x1BB1), new Rune(0x1C41), new Rune(0x1C51), new Rune(0xA621), - new Rune(0xA8D1), new Rune(0xA901), new Rune(0xA9D1), new Rune(0xA9F1), new Rune(0xAA51), - new Rune(0xABF1), new Rune(0xFF11), new Rune(0x104A1), new Rune(0x10D31), new Rune(0x11067), - new Rune(0x110F1), new Rune(0x11137), new Rune(0x111D1), new Rune(0x112F1), new Rune(0x11451), - new Rune(0x114D1), new Rune(0x11651), new Rune(0x116C1), new Rune(0x11731), new Rune(0x118E1), - new Rune(0x11951), new Rune(0x11C51), - ]; - - StringBuilder sb = new(); - Span buffer = stackalloc char[2]; - foreach (var r in numberOnes) - { - sb.Append(buffer.Slice(0, r.EncodeToUtf16(buffer))); - } - string numberOnesString = sb.ToString(); - - // 111...110 vs 111...111 - yield return new object[] { - s_invariantCompare, - new string('1', numberOnes.Length - 1) + "0", - numberOnesString, - CompareOptions.NumericOrdering, - -1 - }; - - // 111...111 vs 111...111 - yield return new object[] { - s_invariantCompare, - new string('1', numberOnes.Length), - numberOnesString, - CompareOptions.NumericOrdering, - 0 - }; - - // 111...112 vs 111...111 - yield return new object[] { - s_invariantCompare, - new string('1', numberOnes.Length - 1) + "2", - numberOnesString, - CompareOptions.NumericOrdering, - 1 - }; - } + yield return new object[] { s_invariantCompare, "0", "\u0661", CompareOptions.NumericOrdering, -1 }; + yield return new object[] { s_invariantCompare, "1", "\u0661", CompareOptions.NumericOrdering, PlatformDetection.IsNlsGlobalization ? -1 : 0 }; + yield return new object[] { s_invariantCompare, "2", "\u0661", CompareOptions.NumericOrdering, 1 }; } [Theory] diff --git a/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.HashCode.cs b/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.HashCode.cs index 49be898000b9b..70893a4b378d6 100644 --- a/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.HashCode.cs +++ b/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.HashCode.cs @@ -66,8 +66,8 @@ void CheckChar(int charCode, CultureInfo culture) new object[] { "abc", CompareOptions.Ordinal, "abc", CompareOptions.Ordinal, true }, new object[] { "abc", CompareOptions.None, "abc", CompareOptions.None, true }, new object[] { "", CompareOptions.None, "\u200c", CompareOptions.None, true }, // see comment at bottom of SortKey_TestData - new object[] { "1", CompareOptions.NumericOrdering, "01", CompareOptions.NumericOrdering, true }, - new object[] { "1", CompareOptions.NumericOrdering, "\u0661", CompareOptions.NumericOrdering, true }, + new object[] { "1", CompareOptions.NumericOrdering, "01", CompareOptions.NumericOrdering, PlatformDetection.IsNlsGlobalization ? false : true }, + new object[] { "1", CompareOptions.NumericOrdering, "\u0661", CompareOptions.NumericOrdering, PlatformDetection.IsIcuGlobalization ? false : true }, }; [Theory] diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/StringComparerTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/StringComparerTests.cs index d967bb6d270dc..bc12ee10a7b8d 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/StringComparerTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/StringComparerTests.cs @@ -137,15 +137,21 @@ public void CreateCultureOptions_CreatesValidComparer() Assert.Equal(1, c.Compare("42", null)); Assert.Throws(() => c.Compare(42, "84")); + c = StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.NumericOrdering); if (PlatformDetection.IsHybridGlobalizationOnApplePlatform) { - Assert.Throws(() => StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.NumericOrdering)); + Assert.Throws(() => c.Compare("2", "10")); + Assert.Throws(() => c.GetHashCode("42")); } else { - c = StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.NumericOrdering); Assert.Equal(-1, c.Compare("2", "10")); - if (!PlatformDetection.IsNlsGlobalization) + if (PlatformDetection.IsNlsGlobalization) + { + Assert.NotEqual(0, c.Compare("2", "02")); + Assert.NotEqual(c.GetHashCode("2"), c.GetHashCode("02")); + } + else { Assert.Equal(0, c.Compare("2", "02")); Assert.Equal(c.GetHashCode("2"), c.GetHashCode("02")); From 48b3c6ef345c953cdbe6077fd136deb5ca6558b7 Mon Sep 17 00:00:00 2001 From: Pranav Senthilnathan Date: Sun, 17 Nov 2024 01:43:58 -0800 Subject: [PATCH 5/8] flipped condition --- .../CompareInfo/CompareInfoTests.HashCode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.HashCode.cs b/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.HashCode.cs index 70893a4b378d6..a30d37ddd9092 100644 --- a/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.HashCode.cs +++ b/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.HashCode.cs @@ -67,7 +67,7 @@ void CheckChar(int charCode, CultureInfo culture) new object[] { "abc", CompareOptions.None, "abc", CompareOptions.None, true }, new object[] { "", CompareOptions.None, "\u200c", CompareOptions.None, true }, // see comment at bottom of SortKey_TestData new object[] { "1", CompareOptions.NumericOrdering, "01", CompareOptions.NumericOrdering, PlatformDetection.IsNlsGlobalization ? false : true }, - new object[] { "1", CompareOptions.NumericOrdering, "\u0661", CompareOptions.NumericOrdering, PlatformDetection.IsIcuGlobalization ? false : true }, + new object[] { "1", CompareOptions.NumericOrdering, "\u0661", CompareOptions.NumericOrdering, PlatformDetection.IsNlsGlobalization ? false : true }, }; [Theory] From 3d294992e0b41b0239bc18309d0509ecf314ceb0 Mon Sep 17 00:00:00 2001 From: Pranav Senthilnathan Date: Mon, 18 Nov 2024 13:31:19 -0800 Subject: [PATCH 6/8] docs --- .../System/Globalization/CompareOptions.cs | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareOptions.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareOptions.cs index ba12f58c5a057..8dae18c12de81 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareOptions.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareOptions.cs @@ -3,18 +3,73 @@ namespace System.Globalization { + /// + /// Defines the string comparison options to use with . + /// [Flags] public enum CompareOptions { + /// + /// Indicates the default option settings for string comparisons + /// None = 0x00000000, + + /// + /// Indicates that the string comparison must ignore case. + /// IgnoreCase = 0x00000001, + + /// + /// Indicates that the string comparison must ignore nonspacing combining characters, such as diacritics. + /// The Unicode Standard defines combining characters as + /// characters that are combined with base characters to produce a new character. Nonspacing combining characters do not + /// occupy a spacing position by themselves when rendered. + /// IgnoreNonSpace = 0x00000002, + + /// + /// Indicates that the string comparison must ignore symbols, such as white-space characters, punctuation, currency symbols, + /// the percent sign, mathematical symbols, the ampersand, and so on. + /// IgnoreSymbols = 0x00000004, + + /// + /// Indicates that the string comparison must ignore the Kana type. Kana type refers to Japanese hiragana and katakana characters, which represent phonetic sounds in the Japanese language. + /// Hiragana is used for native Japanese expressions and words, while katakana is used for words borrowed from other languages, such as "computer" or "Internet". + /// A phonetic sound can be expressed in both hiragana and katakana. If this value is selected, the hiragana character for one sound is considered equal to the katakana character for the same sound. + /// IgnoreKanaType = 0x00000008, + + /// + /// Indicates that the string comparison must ignore the character width. For example, Japanese katakana characters can be written as full-width or half-width. + /// If this value is selected, the katakana characters written as full-width are considered equal to the same characters written as half-width. + /// IgnoreWidth = 0x00000010, + + /// + /// Indicates that the string comparison must sort sequences of digits (Unicode general category "Nd") based on their numeric value. + /// For example, "2" comes before "10". Non-digit characters such as decimal points, minus or plus signs, etc. + /// are not considered as part of the sequence and will terminate it. + /// NumericOrdering = 0x00000020, + + /// + /// String comparison must ignore case, then perform an ordinal comparison. This technique is equivalent to + /// converting the string to uppercase using the invariant culture and then performing an ordinal comparison on the result. + /// OrdinalIgnoreCase = 0x10000000, // This flag can not be used with other flags. + + /// + /// Indicates that the string comparison must use the string sort algorithm. In a string sort, the hyphen and the apostrophe, + /// as well as other nonalphanumeric symbols, come before alphanumeric characters. + /// StringSort = 0x20000000, + + /// + /// Indicates that the string comparison must use successive Unicode UTF-16 encoded values of the string (code unit by code unit comparison), + /// leading to a fast comparison but one that is culture-insensitive. A string starting with a code unit XXXX16 comes before a string starting with YYYY16, + /// if XXXX16 is less than YYYY16. This value cannot be combined with other values and must be used alone. + /// Ordinal = 0x40000000, // This flag can not be used with other flags. } } From 215c1d661bd7625ef775d8eab4189e4c9bd444a9 Mon Sep 17 00:00:00 2001 From: Pranav Senthilnathan Date: Tue, 19 Nov 2024 16:55:34 -0800 Subject: [PATCH 7/8] Address comments --- .../TestUtilities/System/PlatformDetection.cs | 3 + .../System/Globalization/CompareOptions.cs | 4 +- .../CompareInfo/CompareInfoTests.Compare.cs | 99 ++++++++++--------- .../CompareInfo/CompareInfoTests.HashCode.cs | 22 +++-- .../CompareInfo/CompareInfoTests.SortKey.cs | 79 ++++++++------- .../System/StringComparerTests.cs | 25 +++-- 6 files changed, 131 insertions(+), 101 deletions(-) diff --git a/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs index de1ce353d5549..23d63a5ab0e70 100644 --- a/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs +++ b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs @@ -396,6 +396,9 @@ public static string GetDistroVersionString() public static bool IsNotHybridGlobalization => !IsHybridGlobalization; public static bool IsNotHybridGlobalizationOnApplePlatform => !IsHybridGlobalizationOnApplePlatform; + // This can be removed once numeric comparisons are supported on Apple platforms + public static bool IsNumericComparisonSupported => !IsHybridGlobalizationOnApplePlatform; + // HG on apple platforms implies ICU public static bool IsIcuGlobalization => !IsInvariantGlobalization && (IsHybridGlobalizationOnApplePlatform || ICUVersion > new Version(0, 0, 0, 0)); diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareOptions.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareOptions.cs index 8dae18c12de81..33d9e23d780c4 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareOptions.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareOptions.cs @@ -49,13 +49,15 @@ public enum CompareOptions /// /// Indicates that the string comparison must sort sequences of digits (Unicode general category "Nd") based on their numeric value. /// For example, "2" comes before "10". Non-digit characters such as decimal points, minus or plus signs, etc. - /// are not considered as part of the sequence and will terminate it. + /// are not considered as part of the sequence and will terminate it. This flag is not valid for indexing + /// (such as , , etc.). /// NumericOrdering = 0x00000020, /// /// String comparison must ignore case, then perform an ordinal comparison. This technique is equivalent to /// converting the string to uppercase using the invariant culture and then performing an ordinal comparison on the result. + /// This value cannot be combined with other values and must be used alone. /// OrdinalIgnoreCase = 0x10000000, // This flag can not be used with other flags. diff --git a/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.Compare.cs b/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.Compare.cs index 0e2a0c418ccb0..fd20fa33ffbdc 100644 --- a/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.Compare.cs +++ b/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.Compare.cs @@ -43,50 +43,54 @@ public static IEnumerable Compare_Kana_TestData() public static IEnumerable Compare_TestData() { - #region Numeric ordering - var isNls = PlatformDetection.IsNlsGlobalization; - - yield return new object[] { s_invariantCompare, "1234567890", "1234567890", CompareOptions.NumericOrdering, 0 }; - yield return new object[] { s_invariantCompare, "1234567890", "1234567890", CompareOptions.NumericOrdering, 0 }; - - // Leading zero - yield return new object[] { s_invariantCompare, "02", "1", CompareOptions.NumericOrdering, 1 }; - yield return new object[] { s_invariantCompare, "a02", "a1", CompareOptions.NumericOrdering, 1 }; - yield return new object[] { s_invariantCompare, "02a", "1a", CompareOptions.NumericOrdering, 1 }; - - // NLS treats equivalent numbers differing by leading zeros as unequal - yield return new object[] { s_invariantCompare, "01", "1", CompareOptions.NumericOrdering, isNls ? -1 : 0 }; - yield return new object[] { s_invariantCompare, "a01", "a1", CompareOptions.NumericOrdering, isNls ? -1 : 0 }; - yield return new object[] { s_invariantCompare, "01a", "1a", CompareOptions.NumericOrdering, isNls ? -1 : 0 }; - - // But they are closer in sort order than unequal numbers: 1 < 02 < 2 < 03 - // Unlike non-numerical sort which bookends them: 02 < 03 < 1 < 2 - yield return new object[] { s_invariantCompare, "1", "02", CompareOptions.NumericOrdering, -1 }; - yield return new object[] { s_invariantCompare, "02", "2", CompareOptions.NumericOrdering, isNls ? -1 : 0 }; - yield return new object[] { s_invariantCompare, "2", "03", CompareOptions.NumericOrdering, -1 }; - - // 2 < 10 - yield return new object[] { s_invariantCompare, "2", "10", CompareOptions.NumericOrdering, -1 }; - yield return new object[] { s_invariantCompare, "a2", "a10", CompareOptions.NumericOrdering, -1 }; - yield return new object[] { s_invariantCompare, "2a", "10a", CompareOptions.NumericOrdering, -1 }; - - // With casing - yield return new object[] { s_invariantCompare, "1A02", "1a02", CompareOptions.NumericOrdering | CompareOptions.IgnoreCase, 0 }; - yield return new object[] { s_invariantCompare, "A1", "a2", CompareOptions.NumericOrdering, -1 }; // Numerical differences have higher precedence - yield return new object[] { s_invariantCompare, "A01", "a1", CompareOptions.NumericOrdering, isNls ? -1 : 1 }; // ICU treats 01 == 1 - - // With diacritics // PlatformDetection.IsHybridGlobalizationOnBrowser does not support IgnoreNonSpace alone, it needs to be with IgnoreKanaType CompareOptions validIgnoreNonSpaceOption = PlatformDetection.IsHybridGlobalizationOnBrowser ? CompareOptions.IgnoreNonSpace | CompareOptions.IgnoreKanaType : CompareOptions.IgnoreNonSpace; - yield return new object[] { s_invariantCompare, "1\u00E102", "1a02", CompareOptions.NumericOrdering | validIgnoreNonSpaceOption, 0 }; - yield return new object[] { s_invariantCompare, "\u00E11", "a2", CompareOptions.NumericOrdering, -1 }; // Numerical differences have higher precedence - yield return new object[] { s_invariantCompare, "\u00E101", "a1", CompareOptions.NumericOrdering, isNls ? -1 : 1 }; // ICU treats 01 == 1 - // Period is NOT part of the numeric value - yield return new object[] { s_invariantCompare, "0.1", "0.02", CompareOptions.NumericOrdering, -1 }; + #region Numeric ordering + if (PlatformDetection.IsNumericComparisonSupported) + { + var isNls = PlatformDetection.IsNlsGlobalization; + + yield return new object[] { s_invariantCompare, "1234567890", "1234567890", CompareOptions.NumericOrdering, 0 }; + yield return new object[] { s_invariantCompare, "1234567890", "1234567890", CompareOptions.NumericOrdering, 0 }; + + // Leading zero + yield return new object[] { s_invariantCompare, "02", "1", CompareOptions.NumericOrdering, 1 }; + yield return new object[] { s_invariantCompare, "a02", "a1", CompareOptions.NumericOrdering, 1 }; + yield return new object[] { s_invariantCompare, "02a", "1a", CompareOptions.NumericOrdering, 1 }; + + // NLS treats equivalent numbers differing by leading zeros as unequal + yield return new object[] { s_invariantCompare, "01", "1", CompareOptions.NumericOrdering, isNls ? -1 : 0 }; + yield return new object[] { s_invariantCompare, "a01", "a1", CompareOptions.NumericOrdering, isNls ? -1 : 0 }; + yield return new object[] { s_invariantCompare, "01a", "1a", CompareOptions.NumericOrdering, isNls ? -1 : 0 }; + + // But they are closer in sort order than unequal numbers: 1 < 02 < 2 < 03 + // Unlike non-numerical sort which bookends them: 02 < 03 < 1 < 2 + yield return new object[] { s_invariantCompare, "1", "02", CompareOptions.NumericOrdering, -1 }; + yield return new object[] { s_invariantCompare, "02", "2", CompareOptions.NumericOrdering, isNls ? -1 : 0 }; + yield return new object[] { s_invariantCompare, "2", "03", CompareOptions.NumericOrdering, -1 }; + + // 2 < 10 + yield return new object[] { s_invariantCompare, "2", "10", CompareOptions.NumericOrdering, -1 }; + yield return new object[] { s_invariantCompare, "a2", "a10", CompareOptions.NumericOrdering, -1 }; + yield return new object[] { s_invariantCompare, "2a", "10a", CompareOptions.NumericOrdering, -1 }; + + // With casing + yield return new object[] { s_invariantCompare, "1A02", "1a02", CompareOptions.NumericOrdering | CompareOptions.IgnoreCase, 0 }; + yield return new object[] { s_invariantCompare, "A1", "a2", CompareOptions.NumericOrdering, -1 }; // Numerical differences have higher precedence + yield return new object[] { s_invariantCompare, "A01", "a1", CompareOptions.NumericOrdering, isNls ? -1 : 1 }; // ICU treats 01 == 1 + + // With diacritics + yield return new object[] { s_invariantCompare, "1\u00E102", "1a02", CompareOptions.NumericOrdering | validIgnoreNonSpaceOption, 0 }; + yield return new object[] { s_invariantCompare, "\u00E11", "a2", CompareOptions.NumericOrdering, -1 }; // Numerical differences have higher precedence + yield return new object[] { s_invariantCompare, "\u00E101", "a1", CompareOptions.NumericOrdering, isNls ? -1 : 1 }; // ICU treats 01 == 1 + + // Period is NOT part of the numeric value + yield return new object[] { s_invariantCompare, "0.1", "0.02", CompareOptions.NumericOrdering, -1 }; + } #endregion // PlatformDetection.IsHybridGlobalizationOnBrowser does not support IgnoreKanaType alone, it needs to be e.g. with IgnoreCase @@ -153,7 +157,6 @@ public static IEnumerable Compare_TestData() yield return new object[] { s_invariantCompare, "'", "\uFF07", CompareOptions.IgnoreWidth, 0 }; yield return new object[] { s_invariantCompare, "\"", "\uFF02", CompareOptions.IgnoreWidth, 0 }; } - yield return new object[] { s_invariantCompare, "\u3042", "\u30A1", CompareOptions.None, PlatformDetection.IsHybridGlobalizationOnApplePlatform ? 1 : s_expectedHiraganaToKatakanaCompare }; yield return new object[] { s_invariantCompare, "\u3042", "\u30A2", CompareOptions.None, s_expectedHiraganaToKatakanaCompare }; yield return new object[] { s_invariantCompare, "\u3042", "\uFF71", CompareOptions.None, s_expectedHiraganaToKatakanaCompare }; @@ -571,9 +574,12 @@ public void Compare_Invalid() public static IEnumerable Compare_Numeric_TestData() { - yield return new object[] { s_invariantCompare, "0", "\u0661", CompareOptions.NumericOrdering, -1 }; - yield return new object[] { s_invariantCompare, "1", "\u0661", CompareOptions.NumericOrdering, PlatformDetection.IsNlsGlobalization ? -1 : 0 }; - yield return new object[] { s_invariantCompare, "2", "\u0661", CompareOptions.NumericOrdering, 1 }; + if (PlatformDetection.IsNumericComparisonSupported) + { + yield return new object[] { s_invariantCompare, "0", "\u0661", CompareOptions.NumericOrdering, -1 }; + yield return new object[] { s_invariantCompare, "1", "\u0661", CompareOptions.NumericOrdering, PlatformDetection.IsNlsGlobalization ? -1 : 0 }; + yield return new object[] { s_invariantCompare, "2", "\u0661", CompareOptions.NumericOrdering, 1 }; + } } [Theory] @@ -695,9 +701,12 @@ public static IEnumerable Compare_HiraganaAndKatakana_TestData() } : Array.Empty(); - // Adding NumericOrdering does not affect whether an option set is supported or not - optionsPositive = optionsPositive.Concat(from opt in optionsPositive where opt != CompareOptions.Ordinal && opt != CompareOptions.OrdinalIgnoreCase select opt | CompareOptions.NumericOrdering).ToArray(); - optionsNegative = optionsNegative.Concat(from opt in optionsNegative where opt != CompareOptions.Ordinal && opt != CompareOptions.OrdinalIgnoreCase select opt | CompareOptions.NumericOrdering).ToArray(); + if (PlatformDetection.IsNumericComparisonSupported) + { + // Adding NumericOrdering does not affect whether an option set is supported or not + optionsPositive = optionsPositive.Concat(from opt in optionsPositive where opt != CompareOptions.Ordinal && opt != CompareOptions.OrdinalIgnoreCase select opt | CompareOptions.NumericOrdering).ToArray(); + optionsNegative = optionsNegative.Concat(from opt in optionsNegative where opt != CompareOptions.Ordinal && opt != CompareOptions.OrdinalIgnoreCase select opt | CompareOptions.NumericOrdering).ToArray(); + } yield return new object[] { optionsPositive, optionsNegative }; } diff --git a/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.HashCode.cs b/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.HashCode.cs index a30d37ddd9092..a6fe2712e1ee1 100644 --- a/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.HashCode.cs +++ b/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.HashCode.cs @@ -59,16 +59,20 @@ void CheckChar(int charCode, CultureInfo culture) } } - public static IEnumerable GetHashCodeTestData => new[] + public static IEnumerable GetHashCodeTestData() { - new object[] { "abc", CompareOptions.OrdinalIgnoreCase, "ABC", CompareOptions.OrdinalIgnoreCase, true }, - new object[] { "abc", CompareOptions.Ordinal, "ABC", CompareOptions.Ordinal, false }, - new object[] { "abc", CompareOptions.Ordinal, "abc", CompareOptions.Ordinal, true }, - new object[] { "abc", CompareOptions.None, "abc", CompareOptions.None, true }, - new object[] { "", CompareOptions.None, "\u200c", CompareOptions.None, true }, // see comment at bottom of SortKey_TestData - new object[] { "1", CompareOptions.NumericOrdering, "01", CompareOptions.NumericOrdering, PlatformDetection.IsNlsGlobalization ? false : true }, - new object[] { "1", CompareOptions.NumericOrdering, "\u0661", CompareOptions.NumericOrdering, PlatformDetection.IsNlsGlobalization ? false : true }, - }; + yield return new object[] { "abc", CompareOptions.OrdinalIgnoreCase, "ABC", CompareOptions.OrdinalIgnoreCase, true }; + yield return new object[] { "abc", CompareOptions.Ordinal, "ABC", CompareOptions.Ordinal, false }; + yield return new object[] { "abc", CompareOptions.Ordinal, "abc", CompareOptions.Ordinal, true }; + yield return new object[] { "abc", CompareOptions.None, "abc", CompareOptions.None, true }; + yield return new object[] { "", CompareOptions.None, "\u200c", CompareOptions.None, true }; // see comment at bottom of SortKey_TestData + + if (PlatformDetection.IsNumericComparisonSupported) + { + yield return new object[] { "1", CompareOptions.NumericOrdering, "01", CompareOptions.NumericOrdering, PlatformDetection.IsNlsGlobalization ? false : true }; + yield return new object[] { "1", CompareOptions.NumericOrdering, "\u0661", CompareOptions.NumericOrdering, PlatformDetection.IsNlsGlobalization ? false : true }; + } + } [Theory] [MemberData(nameof(GetHashCodeTestData))] diff --git a/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.SortKey.cs b/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.SortKey.cs index 723a86f14a315..193b55baf47b5 100644 --- a/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.SortKey.cs +++ b/src/libraries/System.Runtime/tests/System.Globalization.Tests/CompareInfo/CompareInfoTests.SortKey.cs @@ -22,44 +22,47 @@ public class CompareInfoSortKeyTests : CompareInfoTestsBase public static IEnumerable SortKey_TestData() { #region Numeric ordering - var isNls = PlatformDetection.IsNlsGlobalization; - - yield return new object[] { s_invariantCompare, "1234567890", "1234567890", CompareOptions.NumericOrdering, 0 }; - yield return new object[] { s_invariantCompare, "1234567890", "1234567890", CompareOptions.NumericOrdering, 0 }; - - // Leading zero - yield return new object[] { s_invariantCompare, "02", "1", CompareOptions.NumericOrdering, 1 }; - yield return new object[] { s_invariantCompare, "a02", "a1", CompareOptions.NumericOrdering, 1 }; - yield return new object[] { s_invariantCompare, "02a", "1a", CompareOptions.NumericOrdering, 1 }; - - // NLS treats equivalent numbers differing by leading zeros as unequal - yield return new object[] { s_invariantCompare, "01", "1", CompareOptions.NumericOrdering, isNls ? -1 : 0 }; - yield return new object[] { s_invariantCompare, "a01", "a1", CompareOptions.NumericOrdering, isNls ? -1 : 0 }; - yield return new object[] { s_invariantCompare, "01a", "1a", CompareOptions.NumericOrdering, isNls ? -1 : 0 }; - - // But they are closer in sort order than unequal numbers: 1 < 02 < 2 < 03 - // Unlike non-numerical sort which bookends them: 02 < 03 < 1 < 2 - yield return new object[] { s_invariantCompare, "1", "02", CompareOptions.NumericOrdering, -1 }; - yield return new object[] { s_invariantCompare, "02", "2", CompareOptions.NumericOrdering, isNls ? -1 : 0 }; - yield return new object[] { s_invariantCompare, "2", "03", CompareOptions.NumericOrdering, -1 }; - - // 2 < 10 - yield return new object[] { s_invariantCompare, "2", "10", CompareOptions.NumericOrdering, -1 }; - yield return new object[] { s_invariantCompare, "a2", "a10", CompareOptions.NumericOrdering, -1 }; - yield return new object[] { s_invariantCompare, "2a", "10a", CompareOptions.NumericOrdering, -1 }; - - // With casing - yield return new object[] { s_invariantCompare, "1A02", "1a02", CompareOptions.NumericOrdering | CompareOptions.IgnoreCase, 0 }; - yield return new object[] { s_invariantCompare, "A1", "a2", CompareOptions.NumericOrdering, -1 }; // Numerical differences have higher precedence - yield return new object[] { s_invariantCompare, "A01", "a1", CompareOptions.NumericOrdering, isNls ? -1 : 1 }; // ICU treats 01 == 1 - - // With diacritics - yield return new object[] { s_invariantCompare, "1\u00E102", "1a02", CompareOptions.NumericOrdering | CompareOptions.IgnoreNonSpace, 0 }; - yield return new object[] { s_invariantCompare, "\u00E11", "a2", CompareOptions.NumericOrdering, -1 }; // Numerical differences have higher precedence - yield return new object[] { s_invariantCompare, "\u00E101", "a1", CompareOptions.NumericOrdering, isNls ? -1 : 1 }; // ICU treats 01 == 1 - - // Period is NOT part of the numeric value - yield return new object[] { s_invariantCompare, "0.1", "0.02", CompareOptions.NumericOrdering, -1 }; + if (PlatformDetection.IsNumericComparisonSupported) + { + var isNls = PlatformDetection.IsNlsGlobalization; + + yield return new object[] { s_invariantCompare, "1234567890", "1234567890", CompareOptions.NumericOrdering, 0 }; + yield return new object[] { s_invariantCompare, "1234567890", "1234567890", CompareOptions.NumericOrdering, 0 }; + + // Leading zero + yield return new object[] { s_invariantCompare, "02", "1", CompareOptions.NumericOrdering, 1 }; + yield return new object[] { s_invariantCompare, "a02", "a1", CompareOptions.NumericOrdering, 1 }; + yield return new object[] { s_invariantCompare, "02a", "1a", CompareOptions.NumericOrdering, 1 }; + + // NLS treats equivalent numbers differing by leading zeros as unequal + yield return new object[] { s_invariantCompare, "01", "1", CompareOptions.NumericOrdering, isNls ? -1 : 0 }; + yield return new object[] { s_invariantCompare, "a01", "a1", CompareOptions.NumericOrdering, isNls ? -1 : 0 }; + yield return new object[] { s_invariantCompare, "01a", "1a", CompareOptions.NumericOrdering, isNls ? -1 : 0 }; + + // But they are closer in sort order than unequal numbers: 1 < 02 < 2 < 03 + // Unlike non-numerical sort which bookends them: 02 < 03 < 1 < 2 + yield return new object[] { s_invariantCompare, "1", "02", CompareOptions.NumericOrdering, -1 }; + yield return new object[] { s_invariantCompare, "02", "2", CompareOptions.NumericOrdering, isNls ? -1 : 0 }; + yield return new object[] { s_invariantCompare, "2", "03", CompareOptions.NumericOrdering, -1 }; + + // 2 < 10 + yield return new object[] { s_invariantCompare, "2", "10", CompareOptions.NumericOrdering, -1 }; + yield return new object[] { s_invariantCompare, "a2", "a10", CompareOptions.NumericOrdering, -1 }; + yield return new object[] { s_invariantCompare, "2a", "10a", CompareOptions.NumericOrdering, -1 }; + + // With casing + yield return new object[] { s_invariantCompare, "1A02", "1a02", CompareOptions.NumericOrdering | CompareOptions.IgnoreCase, 0 }; + yield return new object[] { s_invariantCompare, "A1", "a2", CompareOptions.NumericOrdering, -1 }; // Numerical differences have higher precedence + yield return new object[] { s_invariantCompare, "A01", "a1", CompareOptions.NumericOrdering, isNls ? -1 : 1 }; // ICU treats 01 == 1 + + // With diacritics + yield return new object[] { s_invariantCompare, "1\u00E102", "1a02", CompareOptions.NumericOrdering | CompareOptions.IgnoreNonSpace, 0 }; + yield return new object[] { s_invariantCompare, "\u00E11", "a2", CompareOptions.NumericOrdering, -1 }; // Numerical differences have higher precedence + yield return new object[] { s_invariantCompare, "\u00E101", "a1", CompareOptions.NumericOrdering, isNls ? -1 : 1 }; // ICU treats 01 == 1 + + // Period is NOT part of the numeric value + yield return new object[] { s_invariantCompare, "0.1", "0.02", CompareOptions.NumericOrdering, -1 }; + } #endregion CompareOptions ignoreKanaIgnoreWidthIgnoreCase = CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth | CompareOptions.IgnoreCase; diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/StringComparerTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/StringComparerTests.cs index bc12ee10a7b8d..5304424d397c7 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/StringComparerTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/StringComparerTests.cs @@ -138,25 +138,28 @@ public void CreateCultureOptions_CreatesValidComparer() Assert.Throws(() => c.Compare(42, "84")); c = StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.NumericOrdering); - if (PlatformDetection.IsHybridGlobalizationOnApplePlatform) - { - Assert.Throws(() => c.Compare("2", "10")); - Assert.Throws(() => c.GetHashCode("42")); - } - else + if (PlatformDetection.IsNumericComparisonSupported) { Assert.Equal(-1, c.Compare("2", "10")); if (PlatformDetection.IsNlsGlobalization) { Assert.NotEqual(0, c.Compare("2", "02")); Assert.NotEqual(c.GetHashCode("2"), c.GetHashCode("02")); + Assert.False(c.Equals("2", "02")); } else { Assert.Equal(0, c.Compare("2", "02")); Assert.Equal(c.GetHashCode("2"), c.GetHashCode("02")); + Assert.True(c.Equals("2", "02")); } } + else + { + Assert.Throws(() => c.Compare("2", "10")); + Assert.Throws(() => c.GetHashCode("42")); + Assert.Throws(() => c.Equals("2", "42")); + } } [Fact] @@ -222,14 +225,20 @@ public void IsWellKnownCultureAwareComparer_TestCases() RunTest(GetNonRandomizedComparer("WrappedAroundStringComparerOrdinalIgnoreCase"), null, default); RunTest(new CustomStringComparer(), null, default); // not an inbox comparer RunTest(ci_enUS.GetStringComparer(CompareOptions.None), ci_enUS, CompareOptions.None); - RunTest(ci_enUS.GetStringComparer(CompareOptions.NumericOrdering), ci_enUS, CompareOptions.NumericOrdering); + if (PlatformDetection.IsNumericComparisonSupported) + { + RunTest(ci_enUS.GetStringComparer(CompareOptions.NumericOrdering), ci_enUS, CompareOptions.NumericOrdering); + } RunTest(ci_enUS.GetStringComparer(CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType), ci_enUS, CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType); RunTest(ci_enUS.GetStringComparer(CompareOptions.Ordinal), null, default); // not linguistic RunTest(ci_enUS.GetStringComparer(CompareOptions.OrdinalIgnoreCase), null, default); // not linguistic RunTest(StringComparer.Create(CultureInfo.InvariantCulture, false), ci_inv, CompareOptions.None); RunTest(StringComparer.Create(CultureInfo.InvariantCulture, true), ci_inv, CompareOptions.IgnoreCase); RunTest(StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.IgnoreSymbols), ci_inv, CompareOptions.IgnoreSymbols); - RunTest(StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.NumericOrdering), ci_inv, CompareOptions.NumericOrdering); + if (PlatformDetection.IsNumericComparisonSupported) + { + RunTest(StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.NumericOrdering), ci_inv, CompareOptions.NumericOrdering); + } // Then, make sure that this API works with common collection types From 74150f7587b232d43a89b24ef3f730c1ddf3afd6 Mon Sep 17 00:00:00 2001 From: Pranav Senthilnathan Date: Wed, 20 Nov 2024 17:45:45 -0800 Subject: [PATCH 8/8] move platform check after argument check --- .../src/System/Globalization/CompareInfo.Icu.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Icu.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Icu.cs index 3d72602ccb804..9b454700f6b3d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Icu.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Icu.cs @@ -716,11 +716,6 @@ private unsafe SortKey IcuCreateSortKey(string source, CompareOptions options) throw new PlatformNotSupportedException(GetPNSEWithReason("CreateSortKey", "non-invariant culture")); return InvariantCreateSortKey(source, options); } -#elif TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS - if (GlobalizationMode.Hybrid) - { - AssertComparisonSupported(options); - } #endif if ((options & ValidCompareMaskOffFlags) != 0) @@ -728,6 +723,13 @@ private unsafe SortKey IcuCreateSortKey(string source, CompareOptions options) throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options)); } +#if TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS + if (GlobalizationMode.Hybrid) + { + AssertComparisonSupported(options); + } +#endif + byte[] keyData; fixed (char* pSource = source) {