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/CompareInfo.Icu.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Icu.cs index a6e37956fd155..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 @@ -723,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) { @@ -776,6 +783,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 +839,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 +906,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.Private.CoreLib/src/System/Globalization/CompareInfo.Nls.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Nls.cs index bb24e9242fa0e..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 @@ -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 @@ -607,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 8ea2223764d8d..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 @@ -175,19 +175,19 @@ 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); + ((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.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..33d9e23d780c4 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareOptions.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareOptions.cs @@ -3,17 +3,75 @@ 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. 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. + + /// + /// 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. } } 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 48bd22c6007aa..6af894665b155 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -9186,6 +9186,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..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 @@ -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,56 @@ public static IEnumerable Compare_Kana_TestData() public static IEnumerable Compare_TestData() { + // PlatformDetection.IsHybridGlobalizationOnBrowser does not support IgnoreNonSpace alone, it needs to be with IgnoreKanaType + CompareOptions validIgnoreNonSpaceOption = + PlatformDetection.IsHybridGlobalizationOnBrowser + ? CompareOptions.IgnoreNonSpace | CompareOptions.IgnoreKanaType + : CompareOptions.IgnoreNonSpace; + + #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 CompareOptions validIgnoreKanaTypeOption = PlatformDetection.IsHybridGlobalizationOnBrowser ? CompareOptions.IgnoreKanaType | CompareOptions.IgnoreCase : @@ -195,7 +247,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) @@ -217,9 +269,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 }; @@ -272,12 +324,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 }; @@ -463,6 +515,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 +572,23 @@ 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.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] + [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() { @@ -623,6 +700,14 @@ public static IEnumerable Compare_HiraganaAndKatakana_TestData() CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth | CompareOptions.IgnoreSymbols | CompareOptions.IgnoreNonSpace | CompareOptions.IgnoreCase, } : Array.Empty(); + + 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 aa05f00d4b6e6..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,14 +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 - }; + 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))] @@ -99,7 +105,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 +127,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..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 @@ -21,6 +21,50 @@ 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 + 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; 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..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 @@ -312,6 +312,39 @@ 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 }; + + 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 }; + yield return new object[] { "A01", "a1", CompareOptions.NumericOrdering, -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 }; + + 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..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 @@ -136,6 +136,30 @@ 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); + 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] @@ -201,12 +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); + 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); + 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 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..026cd942ea99e 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,53 +152,60 @@ 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) { +function compareStrings (string1: string, string2: string, locale: string | undefined, compareOptions: number): number { + let options: Intl.CollatorOptions | undefined = undefined; + + const numericOrderingFlag = 0x20; + if (compareOptions & numericOrderingFlag) { + options = { numeric: 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); // a ≠ b, a ≠ á, a ≠ A + 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); // a ≠ b, a ≠ á, a ≠ A + 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); // a ≠ b, a ≠ á, a ≠ A + 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 }); // by default ignorePunctuation: false + 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 }); // a ≠ b, a ≠ á, a ≠ A + 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" }); // a ≠ b, a ≠ á, a = A + 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" }); // a ≠ b, a = á, a ≠ A + 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" }); // a ≠ b, a = á, a = A + 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 }); // a ≠ b, a ≠ á, a = A + 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 });// a ≠ b, a = á, a ≠ A + 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 }); // a ≠ b, a = á, a = A + return string1.localeCompare(string2, locale, { sensitivity: "base", ignorePunctuation: true, ...options }); // a ≠ b, a = á, a = A case 2: case 3: case 6: @@ -240,7 +247,7 @@ function compareStrings (string1: string, string2: string, locale: string | unde // 29: IgnoreKanaType | IgnoreWidth | IgnoreSymbols | IgnoreCase // 30: IgnoreKanaType | IgnoreWidth | IgnoreSymbols | IgnoreNonSpace // 31: IgnoreKanaType | IgnoreWidth | IgnoreSymbols | IgnoreNonSpace | IgnoreCase - throw new Error(`Invalid comparison option. Option=${casePicker}`); + throw new Error(`Invalid comparison option. Option=${compareOptions}`); } } 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; }