diff --git a/docs/design/features/globalization-hybrid-mode.md b/docs/design/features/globalization-hybrid-mode.md index bdbdb27b09e42..1dec94595b1c4 100644 --- a/docs/design/features/globalization-hybrid-mode.md +++ b/docs/design/features/globalization-hybrid-mode.md @@ -398,6 +398,12 @@ Affected public APIs: - String.Compare, - String.Equals. +Mapped to Apple Native API `compare:options:range:locale:`(https://developer.apple.com/documentation/foundation/nsstring/1414561-compare?language=objc) +This implementation uses normalization techniques such as `precomposedStringWithCanonicalMapping`, +which can result in behavior differences compared to other platforms. +Specifically, the use of precomposed strings and additional locale-based string folding can affect the results of comparisons. +Due to these differences, the exact result of string compariso on Apple platforms may differ. + The number of `CompareOptions` and `NSStringCompareOptions` combinations are limited. Originally supported combinations can be found [here for CompareOptions](https://learn.microsoft.com/dotnet/api/system.globalization.compareoptions) and [here for NSStringCompareOptions](https://developer.apple.com/documentation/foundation/nsstringcompareoptions). - `IgnoreSymbols` is not supported because there is no equivalent in native api. Throws `PlatformNotSupportedException`. diff --git a/src/libraries/Common/tests/Tests/System/StringTests.cs b/src/libraries/Common/tests/Tests/System/StringTests.cs index b756c06d87d6f..45c3bce3c343a 100644 --- a/src/libraries/Common/tests/Tests/System/StringTests.cs +++ b/src/libraries/Common/tests/Tests/System/StringTests.cs @@ -1011,7 +1011,6 @@ public static void MakeSureNoCompareToChecksGoOutOfRange_StringComparison() } [Fact] - [ActiveIssue("https://github.com/dotnet/runtime/issues/95338", typeof(PlatformDetection), nameof(PlatformDetection.IsHybridGlobalizationOnApplePlatform))] public static void CompareToNoMatch_StringComparison() { for (int length = 1; length < 150; length++) @@ -1035,24 +1034,29 @@ public static void CompareToNoMatch_StringComparison() var secondSpan = new ReadOnlySpan(second); Assert.True(0 > firstSpan.CompareTo(secondSpan, StringComparison.Ordinal)); - // Due to differences in the implementation, the exact result of CompareTo will not necessarily match with string.Compare. - // However, the sign will match, which is what defines correctness. - Assert.Equal( - Math.Sign(string.Compare(firstSpan.ToString(), secondSpan.ToString(), StringComparison.OrdinalIgnoreCase)), - Math.Sign(firstSpan.CompareTo(secondSpan, StringComparison.OrdinalIgnoreCase))); - - Assert.Equal( - string.Compare(firstSpan.ToString(), secondSpan.ToString(), StringComparison.CurrentCulture), - firstSpan.CompareTo(secondSpan, StringComparison.CurrentCulture)); - Assert.Equal( - string.Compare(firstSpan.ToString(), secondSpan.ToString(), StringComparison.CurrentCultureIgnoreCase), - firstSpan.CompareTo(secondSpan, StringComparison.CurrentCultureIgnoreCase)); - Assert.Equal( - string.Compare(firstSpan.ToString(), secondSpan.ToString(), StringComparison.InvariantCulture), - firstSpan.CompareTo(secondSpan, StringComparison.InvariantCulture)); - Assert.Equal( - string.Compare(firstSpan.ToString(), secondSpan.ToString(), StringComparison.InvariantCultureIgnoreCase), - firstSpan.CompareTo(secondSpan, StringComparison.InvariantCultureIgnoreCase)); + // On Apple platforms, string comparison is handled by native Apple functions, which apply normalization techniques + // like `precomposedStringWithCanonicalMapping`. This can lead to differences in behavior compared to other platforms. + if (PlatformDetection.IsNotHybridGlobalizationOnApplePlatform) + { + // Due to differences in the implementation, the exact result of CompareTo will not necessarily match with string.Compare. + // However, the sign will match, which is what defines correctness. + Assert.Equal( + Math.Sign(string.Compare(firstSpan.ToString(), secondSpan.ToString(), StringComparison.OrdinalIgnoreCase)), + Math.Sign(firstSpan.CompareTo(secondSpan, StringComparison.OrdinalIgnoreCase))); + + Assert.Equal( + string.Compare(firstSpan.ToString(), secondSpan.ToString(), StringComparison.CurrentCulture), + firstSpan.CompareTo(secondSpan, StringComparison.CurrentCulture)); + Assert.Equal( + string.Compare(firstSpan.ToString(), secondSpan.ToString(), StringComparison.CurrentCultureIgnoreCase), + firstSpan.CompareTo(secondSpan, StringComparison.CurrentCultureIgnoreCase)); + Assert.Equal( + string.Compare(firstSpan.ToString(), secondSpan.ToString(), StringComparison.InvariantCulture), + firstSpan.CompareTo(secondSpan, StringComparison.InvariantCulture)); + Assert.Equal( + string.Compare(firstSpan.ToString(), secondSpan.ToString(), StringComparison.InvariantCultureIgnoreCase), + firstSpan.CompareTo(secondSpan, StringComparison.InvariantCultureIgnoreCase)); + } } } } @@ -1286,7 +1290,6 @@ public static void ContainsMatchDifferentSpans_StringComparison() } [Fact] - [ActiveIssue("https://github.com/dotnet/runtime/issues/95338", typeof(PlatformDetection), nameof(PlatformDetection.IsHybridGlobalizationOnApplePlatform))] public static void ContainsNoMatch_StringComparison() { for (int length = 1; length < 150; length++) @@ -1312,19 +1315,24 @@ public static void ContainsNoMatch_StringComparison() Assert.False(firstSpan.Contains(secondSpan, StringComparison.OrdinalIgnoreCase)); - // Different behavior depending on OS - Assert.Equal( - firstSpan.ToString().StartsWith(secondSpan.ToString(), StringComparison.CurrentCulture), - firstSpan.Contains(secondSpan, StringComparison.CurrentCulture)); - Assert.Equal( - firstSpan.ToString().StartsWith(secondSpan.ToString(), StringComparison.CurrentCultureIgnoreCase), - firstSpan.Contains(secondSpan, StringComparison.CurrentCultureIgnoreCase)); - Assert.Equal( - firstSpan.ToString().StartsWith(secondSpan.ToString(), StringComparison.InvariantCulture), - firstSpan.Contains(secondSpan, StringComparison.InvariantCulture)); - Assert.Equal( - firstSpan.ToString().StartsWith(secondSpan.ToString(), StringComparison.InvariantCultureIgnoreCase), - firstSpan.Contains(secondSpan, StringComparison.InvariantCultureIgnoreCase)); + // On Apple platforms, string comparison is handled by native Apple functions, which apply normalization techniques + // like `precomposedStringWithCanonicalMapping`. This can lead to differences in behavior compared to other platforms. + if (PlatformDetection.IsNotHybridGlobalizationOnApplePlatform) + { + // Different behavior depending on OS + Assert.Equal( + firstSpan.ToString().StartsWith(secondSpan.ToString(), StringComparison.CurrentCulture), + firstSpan.Contains(secondSpan, StringComparison.CurrentCulture)); + Assert.Equal( + firstSpan.ToString().StartsWith(secondSpan.ToString(), StringComparison.CurrentCultureIgnoreCase), + firstSpan.Contains(secondSpan, StringComparison.CurrentCultureIgnoreCase)); + Assert.Equal( + firstSpan.ToString().StartsWith(secondSpan.ToString(), StringComparison.InvariantCulture), + firstSpan.Contains(secondSpan, StringComparison.InvariantCulture)); + Assert.Equal( + firstSpan.ToString().StartsWith(secondSpan.ToString(), StringComparison.InvariantCultureIgnoreCase), + firstSpan.Contains(secondSpan, StringComparison.InvariantCultureIgnoreCase)); + } } } } @@ -2113,7 +2121,6 @@ public static void EndsWithMatchDifferentSpans_StringComparison() } [Fact] - [ActiveIssue("https://github.com/dotnet/runtime/issues/95338", typeof(PlatformDetection), nameof(PlatformDetection.IsHybridGlobalizationOnApplePlatform))] public static void EndsWithNoMatch_StringComparison() { for (int length = 1; length < 150; length++) @@ -7379,7 +7386,6 @@ public static void StartsWithMatchDifferentSpans_StringComparison() } [Fact] - [ActiveIssue("https://github.com/dotnet/runtime/issues/95338", typeof(PlatformDetection), nameof(PlatformDetection.IsHybridGlobalizationOnApplePlatform))] public static void StartsWithNoMatch_StringComparison() { for (int length = 1; length < 150; length++) diff --git a/src/libraries/System.Data.Common/tests/System/Data/SqlTypes/SqlStringSortingTest.cs b/src/libraries/System.Data.Common/tests/System/Data/SqlTypes/SqlStringSortingTest.cs index 7fa294f8fe95d..7e2277986c5ac 100644 --- a/src/libraries/System.Data.Common/tests/System/Data/SqlTypes/SqlStringSortingTest.cs +++ b/src/libraries/System.Data.Common/tests/System/Data/SqlTypes/SqlStringSortingTest.cs @@ -37,8 +37,9 @@ public static class SqlStringSortingTest private static readonly UnicodeEncoding s_unicodeEncoding = new UnicodeEncoding(bigEndian: false, byteOrderMark: false, throwOnInvalidBytes: true); - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotInvariantGlobalization))] - [ActiveIssue("https://github.com/dotnet/runtime/issues/95338", typeof(PlatformDetection), nameof(PlatformDetection.IsHybridGlobalizationOnApplePlatform))] + // On Apple platforms, the string comparison implementation relies on native Apple functions which uses normalization techniques, which can result in behavior differences compared to other platforms. + // Specifically, the use of precomposed strings and additional locale-based string folding can affect the results of comparisons with certain options like `IgnoreKanaType`. + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotInvariantGlobalization), nameof(PlatformDetection.IsNotHybridGlobalizationOnApplePlatform))] [InlineData("ja-JP", 0x0411)] // Japanese - Japan [InlineData("ar-SA", 0x0401)] // Arabic - Saudi Arabia [InlineData("de-DE", 0x0407)] // German - Germany diff --git a/src/libraries/System.Runtime/tests/System.Globalization.Extensions.Tests/GetStringComparerTests.cs b/src/libraries/System.Runtime/tests/System.Globalization.Extensions.Tests/GetStringComparerTests.cs index 3046f50a98c3e..0735620db91af 100644 --- a/src/libraries/System.Runtime/tests/System.Globalization.Extensions.Tests/GetStringComparerTests.cs +++ b/src/libraries/System.Runtime/tests/System.Globalization.Extensions.Tests/GetStringComparerTests.cs @@ -19,8 +19,7 @@ public void GetStringComparer_Invalid() AssertExtensions.Throws("options", () => new CultureInfo("tr-TR").CompareInfo.GetStringComparer(CompareOptions.OrdinalIgnoreCase | CompareOptions.IgnoreCase)); } - [Theory] - [ActiveIssue("https://github.com/dotnet/runtime/issues/95338", typeof(PlatformDetection), nameof(PlatformDetection.IsHybridGlobalizationOnApplePlatform))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotHybridGlobalizationOnApplePlatform))] [InlineData("hello", "hello", "fr-FR", CompareOptions.IgnoreCase, 0, 0)] [InlineData("hello", "HELLo", "fr-FR", CompareOptions.IgnoreCase, 0, 0)] [InlineData("hello", null, "fr-FR", CompareOptions.IgnoreCase, 1, 1)] 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 e05bcbf453e91..aa05f00d4b6e6 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 @@ -14,8 +14,9 @@ public class CompareInfoHashCodeTests : CompareInfoTestsBase { [OuterLoop] - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsIcuGlobalization))] - [ActiveIssue("https://github.com/dotnet/runtime/issues/95338", typeof(PlatformDetection), nameof(PlatformDetection.IsHybridGlobalizationOnApplePlatform))] + // On Apple platforms, string comparison is handled by native Apple functions, which apply normalization techniques + // like `precomposedStringWithCanonicalMapping`. This can lead to differences in behavior compared to other platforms. + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsIcuGlobalization), nameof(PlatformDetection.IsNotHybridGlobalizationOnApplePlatform))] public void CheckHashingInLineWithEqual() { int additionalCollisions = 0; diff --git a/src/libraries/System.Runtime/tests/System.Reflection.Tests/AssemblyNameTests.cs b/src/libraries/System.Runtime/tests/System.Reflection.Tests/AssemblyNameTests.cs index 873d3d474545a..f5225adc18b78 100644 --- a/src/libraries/System.Runtime/tests/System.Reflection.Tests/AssemblyNameTests.cs +++ b/src/libraries/System.Runtime/tests/System.Reflection.Tests/AssemblyNameTests.cs @@ -221,8 +221,7 @@ public void CultureName_Set(AssemblyName assemblyName, string originalCultureNam Assert.Equal(new AssemblyName(expectedEqualString).FullName, assemblyName.FullName); } - [Fact] - [ActiveIssue("https://github.com/dotnet/runtime/issues/95338", typeof(PlatformDetection), nameof(PlatformDetection.IsHybridGlobalizationOnApplePlatform))] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotHybridGlobalizationOnApplePlatform))] public void CultureName_Set_Invalid_ThrowsCultureNotFoundException() { var assemblyName = new AssemblyName("Test"); diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DateTimeTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DateTimeTests.cs index 554d128a15933..ef51adaecc6a5 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DateTimeTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DateTimeTests.cs @@ -1982,7 +1982,10 @@ public static IEnumerable Parse_ValidInput_Succeeds_MemberData() yield return new object[] { "#2020-5-7T09:37:00.0000000+00:00#\0", CultureInfo.InvariantCulture, TimeZoneInfo.ConvertTimeFromUtc(new DateTime(2020, 5, 7, 9, 37, 0, DateTimeKind.Utc), TimeZoneInfo.Local) }; yield return new object[] { "2020-5-7T09:37:00.0000000+00:00", CultureInfo.InvariantCulture, TimeZoneInfo.ConvertTimeFromUtc(new DateTime(2020, 5, 7, 9, 37, 0, DateTimeKind.Utc), TimeZoneInfo.Local) }; - if (PlatformDetection.IsNotInvariantGlobalization) + // On Apple platforms, the handling calendars relies on native Apple APIs (NSCalendar). + // These APIs can cause differences in behavior when parsing or formatting dates compared to other platforms. + // Specifically, the way Apple handles calendar identifiers and date formats for cultures like "he-IL" may lead to variations in the output. + if (PlatformDetection.IsNotInvariantGlobalization && PlatformDetection.IsNotHybridGlobalizationOnApplePlatform) { DateTime today = DateTime.Today; var hebrewCulture = new CultureInfo("he-IL"); @@ -2003,7 +2006,6 @@ public static IEnumerable Parse_ValidInput_Succeeds_MemberData() } [Theory] - [ActiveIssue("https://github.com/dotnet/runtime/issues/95338", typeof(PlatformDetection), nameof(PlatformDetection.IsHybridGlobalizationOnApplePlatform))] [MemberData(nameof(Parse_ValidInput_Succeeds_MemberData))] public static void Parse_ValidInput_Succeeds(string input, CultureInfo culture, DateTime? expected) { @@ -2464,7 +2466,6 @@ public static IEnumerable ToString_MatchesExpected_MemberData() } [Theory] - [ActiveIssue("https://github.com/dotnet/runtime/issues/95338", typeof(PlatformDetection), nameof(PlatformDetection.IsHybridGlobalizationOnApplePlatform))] [MemberData(nameof(Parse_ValidInput_Succeeds_MemberData))] public static void Parse_Span_ValidInput_Succeeds(string input, CultureInfo culture, DateTime? expected) { diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Text/RuneTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Text/RuneTests.cs index f7e20fbfe8938..578d09f3eaad6 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Text/RuneTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Text/RuneTests.cs @@ -57,7 +57,8 @@ public static void Casing_Invariant(int original, int upper, int lower) Assert.Equal(new Rune(lower), Rune.ToLowerInvariant(rune)); } - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsIcuGlobalizationAndNotHybridOnBrowser))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsIcuGlobalizationAndNotHybridOnBrowser), nameof(PlatformDetection.IsNotHybridGlobalizationOnApplePlatform))] + // HybridGlobalization on Apple mobile platforms has issues with casing dotless I // HybridGlobalization on Browser uses Invariant HashCode and SortKey, so its effect does not match this of ICU [InlineData('0', '0', '0')] [InlineData('a', 'A', 'a')] @@ -71,7 +72,6 @@ public static void Casing_Invariant(int original, int upper, int lower) [InlineData('\u0131', '\u0131', '\u0131')] // U+0131 LATIN SMALL LETTER DOTLESS I [InlineData(0x10400, 0x10400, 0x10428)] // U+10400 DESERET CAPITAL LETTER LONG I [InlineData(0x10428, 0x10400, 0x10428)] // U+10428 DESERET SMALL LETTER LONG I - [ActiveIssue("https://github.com/dotnet/runtime/issues/95338", typeof(PlatformDetection), nameof(PlatformDetection.IsHybridGlobalizationOnApplePlatform))] public static void ICU_Casing_Invariant(int original, int upper, int lower) { var rune = new Rune(original);