diff --git a/docs/design/features/globalization-hybrid-mode.md b/docs/design/features/globalization-hybrid-mode.md index 9840c30fed0bba..7ac8a86411a1dd 100644 --- a/docs/design/features/globalization-hybrid-mode.md +++ b/docs/design/features/globalization-hybrid-mode.md @@ -30,7 +30,7 @@ Due to these differences, the exact result of string compariso on Apple platform 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`. +- `IgnoreSymbols` is supported by filtering ignorable symbols on the managed side before invoking the native API. - `IgnoreKanaType` is implemented using [`kCFStringTransformHiraganaKatakana`](https://developer.apple.com/documentation/corefoundation/kcfstringtransformhiraganakatakana?language=objc) then comparing strings. @@ -71,10 +71,6 @@ The number of `CompareOptions` and `NSStringCompareOptions` combinations are lim `CompareOptions.IgnoreWidth` is mapped to `NSStringCompareOptions.NSWidthInsensitiveSearch | NSStringCompareOptions.NSLiteralSearch` -- All combinations that contain below `CompareOptions` always throw `PlatformNotSupportedException`: - - `IgnoreSymbols` - ## String starts with / ends with Affected public APIs: @@ -91,7 +87,7 @@ Apple Native API does not expose locale-sensitive endsWith/startsWith function. - `IgnoreSymbols` - As there is no IgnoreSymbols equivalent in NSStringCompareOptions all `CompareOptions` combinations that include `IgnoreSymbols` throw `PlatformNotSupportedException` + Supported by filtering ignorable symbols on the managed side prior to comparison using native API. ## String indexing @@ -129,7 +125,7 @@ Not covered case: - `IgnoreSymbols` - As there is no IgnoreSymbols equivalent in NSStringCompareOptions all `CompareOptions` combinations that include `IgnoreSymbols` throw `PlatformNotSupportedException` + Supported by filtering ignorable symbols on the managed side prior to comparison using native API. - Some letters consist of more than one grapheme. 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 aa7b91b4fdebf0..b1442d87b6a3a2 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 @@ -78,16 +78,15 @@ private unsafe int IcuIndexOfCore(ReadOnlySpan source, ReadOnlySpan } else { +#if TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS + if (GlobalizationMode.Hybrid) + return IndexOfCoreNative(target, source, options, fromBeginning, matchLengthPtr); +#endif // GetReference may return nullptr if the input span is defaulted. The native layer handles // this appropriately; no workaround is needed on the managed side. - fixed (char* pSource = &MemoryMarshal.GetReference(source)) fixed (char* pTarget = &MemoryMarshal.GetReference(target)) { -#if TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS - if (GlobalizationMode.Hybrid) - return IndexOfCoreNative(pTarget, target.Length, pSource, source.Length, options, fromBeginning, matchLengthPtr); -#endif if (fromBeginning) return Interop.Globalization.IndexOf(_sortHandle, pTarget, target.Length, pSource, source.Length, options, matchLengthPtr); else @@ -207,7 +206,7 @@ private unsafe int IndexOfOrdinalIgnoreCaseHelper(ReadOnlySpan source, Rea InteropCall: #if TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS if (GlobalizationMode.Hybrid) - return IndexOfCoreNative(b, target.Length, a, source.Length, options, fromBeginning, matchLengthPtr); + return IndexOfCoreNative(target, source, options, fromBeginning, matchLengthPtr); #endif if (fromBeginning) return Interop.Globalization.IndexOf(_sortHandle, b, target.Length, a, source.Length, options, matchLengthPtr); @@ -301,7 +300,7 @@ private unsafe int IndexOfOrdinalHelper(ReadOnlySpan source, ReadOnlySpan< InteropCall: #if TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS if (GlobalizationMode.Hybrid) - return IndexOfCoreNative(b, target.Length, a, source.Length, options, fromBeginning, matchLengthPtr); + return IndexOfCoreNative(target, source, options, fromBeginning, matchLengthPtr); #endif if (fromBeginning) return Interop.Globalization.IndexOf(_sortHandle, b, target.Length, a, source.Length, options, matchLengthPtr); @@ -328,13 +327,13 @@ private unsafe bool IcuStartsWith(ReadOnlySpan source, ReadOnlySpan } else { +#if TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS + if (GlobalizationMode.Hybrid) + return NativeStartsWith(prefix, source, options); +#endif fixed (char* pSource = &MemoryMarshal.GetReference(source)) // could be null (or otherwise unable to be dereferenced) fixed (char* pPrefix = &MemoryMarshal.GetReference(prefix)) { -#if TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS - if (GlobalizationMode.Hybrid) - return NativeStartsWith(pPrefix, prefix.Length, pSource, source.Length, options); -#endif return Interop.Globalization.StartsWith(_sortHandle, pPrefix, prefix.Length, pSource, source.Length, options, matchLengthPtr); } } @@ -416,7 +415,7 @@ private unsafe bool StartsWithOrdinalIgnoreCaseHelper(ReadOnlySpan source, InteropCall: #if TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS if (GlobalizationMode.Hybrid) - return NativeStartsWith(bp, prefix.Length, ap, source.Length, options); + return NativeStartsWith(prefix, source, options); #endif return Interop.Globalization.StartsWith(_sortHandle, bp, prefix.Length, ap, source.Length, options, matchLengthPtr); } @@ -488,7 +487,7 @@ private unsafe bool StartsWithOrdinalHelper(ReadOnlySpan source, ReadOnlyS InteropCall: #if TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS if (GlobalizationMode.Hybrid) - return NativeStartsWith(bp, prefix.Length, ap, source.Length, options); + return NativeStartsWith(prefix, source, options); #endif return Interop.Globalization.StartsWith(_sortHandle, bp, prefix.Length, ap, source.Length, options, matchLengthPtr); } @@ -512,13 +511,13 @@ private unsafe bool IcuEndsWith(ReadOnlySpan source, ReadOnlySpan su } else { +#if TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS + if (GlobalizationMode.Hybrid) + return NativeEndsWith(suffix, source, options); +#endif fixed (char* pSource = &MemoryMarshal.GetReference(source)) // could be null (or otherwise unable to be dereferenced) fixed (char* pSuffix = &MemoryMarshal.GetReference(suffix)) { -#if TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS - if (GlobalizationMode.Hybrid) - return NativeEndsWith(pSuffix, suffix.Length, pSource, source.Length, options); -#endif return Interop.Globalization.EndsWith(_sortHandle, pSuffix, suffix.Length, pSource, source.Length, options, matchLengthPtr); } } @@ -601,7 +600,7 @@ private unsafe bool EndsWithOrdinalIgnoreCaseHelper(ReadOnlySpan source, R InteropCall: #if TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS if (GlobalizationMode.Hybrid) - return NativeEndsWith(bp, suffix.Length, ap, source.Length, options); + return NativeEndsWith(suffix, source, options); #endif return Interop.Globalization.EndsWith(_sortHandle, bp, suffix.Length, ap, source.Length, options, matchLengthPtr); } @@ -673,7 +672,7 @@ private unsafe bool EndsWithOrdinalHelper(ReadOnlySpan source, ReadOnlySpa InteropCall: #if TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS if (GlobalizationMode.Hybrid) - return NativeEndsWith(bp, suffix.Length, ap, source.Length, options); + return NativeEndsWith(suffix, source, options); #endif return Interop.Globalization.EndsWith(_sortHandle, bp, suffix.Length, ap, source.Length, options, matchLengthPtr); } diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.iOS.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.iOS.cs index 3ad9df0fcc25eb..f2976bc073c731 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.iOS.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.iOS.cs @@ -4,6 +4,8 @@ using System.Buffers; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; @@ -18,59 +20,205 @@ private enum ErrorCodes ERROR_MIXED_COMPOSITION_NOT_FOUND = -3, } - private unsafe int CompareStringNative(ReadOnlySpan string1, ReadOnlySpan string2, CompareOptions options) + private const int StackAllocThreshold = 150; + + private unsafe int CompareStringNative(scoped ReadOnlySpan string1, scoped ReadOnlySpan string2, CompareOptions options) { Debug.Assert(!GlobalizationMode.Invariant); Debug.Assert(!GlobalizationMode.UseNls); AssertComparisonSupported(options); + using SymbolFilteringBuffer buffer1 = new SymbolFilteringBuffer(); + using SymbolFilteringBuffer buffer2 = new SymbolFilteringBuffer(); + + Span stackBuffer1 = stackalloc char[StackAllocThreshold]; + Span stackBuffer2 = stackalloc char[StackAllocThreshold]; + + // Handle IgnoreSymbols preprocessing + if ((options & CompareOptions.IgnoreSymbols) != 0) + { + string1 = !buffer1.TryFilterString(string1, stackBuffer1, Span.Empty) ? string1 : + buffer1.RentedFilteredBuffer is not null ? + buffer1.RentedFilteredBuffer.AsSpan(0, buffer1.FilteredLength) : + stackBuffer1.Slice(0, buffer1.FilteredLength); + + string2 = !buffer2.TryFilterString(string2, stackBuffer2, Span.Empty) ? string2 : + buffer2.RentedFilteredBuffer is not null ? + buffer2.RentedFilteredBuffer.AsSpan(0, buffer2.FilteredLength) : + stackBuffer2.Slice(0, buffer2.FilteredLength); + + // Remove the flag before passing to native since we handled it here + options &= ~CompareOptions.IgnoreSymbols; + } + // GetReference may return nullptr if the input span is defaulted. The native layer handles // this appropriately; no workaround is needed on the managed side. - int result; fixed (char* pString1 = &MemoryMarshal.GetReference(string1)) fixed (char* pString2 = &MemoryMarshal.GetReference(string2)) { - result = Interop.Globalization.CompareStringNative(m_name, m_name.Length, pString1, string1.Length, pString2, string2.Length, options); + int result = Interop.Globalization.CompareStringNative(m_name, m_name.Length, pString1, string1.Length, pString2, string2.Length, options); + Debug.Assert(result != (int)ErrorCodes.ERROR_COMPARISON_OPTIONS_NOT_FOUND); + return result; } - - Debug.Assert(result != (int)ErrorCodes.ERROR_COMPARISON_OPTIONS_NOT_FOUND); - - return result; } - private unsafe int IndexOfCoreNative(char* target, int cwTargetLength, char* pSource, int cwSourceLength, CompareOptions options, bool fromBeginning, int* matchLengthPtr) + private unsafe int IndexOfCoreNative(scoped ReadOnlySpan target, scoped ReadOnlySpan source, CompareOptions options, bool fromBeginning, int* matchLengthPtr) { AssertComparisonSupported(options); - Interop.Range result = Interop.Globalization.IndexOfNative(m_name, m_name.Length, target, cwTargetLength, pSource, cwSourceLength, options, fromBeginning); - Debug.Assert(result.Location != (int)ErrorCodes.ERROR_COMPARISON_OPTIONS_NOT_FOUND); - if (result.Location == (int)ErrorCodes.ERROR_MIXED_COMPOSITION_NOT_FOUND) - throw new PlatformNotSupportedException(SR.PlatformNotSupported_HybridGlobalizationWithMixedCompositions); + bool ignoreSymbols = (options & CompareOptions.IgnoreSymbols) != 0; + + using SymbolFilteringBuffer targetBuffer = new SymbolFilteringBuffer(); + using SymbolFilteringBuffer sourceBuffer = new SymbolFilteringBuffer(); + + Span stackTargetBuffer = stackalloc char[StackAllocThreshold]; + Span stackSourceBuffer = stackalloc char[StackAllocThreshold]; + Span stackSourceIndexMap = stackalloc int[StackAllocThreshold]; + + // If we are ignoring symbols, preprocess the strings by removing specified Unicode categories. + if (ignoreSymbols) + { + target = !targetBuffer.TryFilterString(target, stackTargetBuffer, Span.Empty) ? target : + targetBuffer.RentedFilteredBuffer is not null ? + targetBuffer.RentedFilteredBuffer.AsSpan(0, targetBuffer.FilteredLength) : + stackTargetBuffer.Slice(0, targetBuffer.FilteredLength); + + source = !sourceBuffer.TryFilterString(source, stackSourceBuffer, stackSourceIndexMap) ? source : + sourceBuffer.RentedFilteredBuffer is not null ? + sourceBuffer.RentedFilteredBuffer.AsSpan(0, sourceBuffer.FilteredLength) : + stackSourceBuffer.Slice(0, sourceBuffer.FilteredLength); + + // Remove the flag before passing to native since we handled it here + options &= ~CompareOptions.IgnoreSymbols; + } + + Interop.Range result; + fixed (char* pTarget = &MemoryMarshal.GetReference(target)) + fixed (char* pSource = &MemoryMarshal.GetReference(source)) + { + result = Interop.Globalization.IndexOfNative(m_name, m_name.Length, pTarget, target.Length, pSource, source.Length, options, fromBeginning); + Debug.Assert(result.Location != (int)ErrorCodes.ERROR_COMPARISON_OPTIONS_NOT_FOUND); + if (result.Location == (int)ErrorCodes.ERROR_MIXED_COMPOSITION_NOT_FOUND) + throw new PlatformNotSupportedException(SR.PlatformNotSupported_HybridGlobalizationWithMixedCompositions); + } + + int nativeLocation = result.Location; + int nativeLength = result.Length; + + // If not ignoring symbols / nothing found / an error code / no index map (no symbols found in source), just propagate. + // sourceBuffer.FilteredLength == 0 means no symbols were found in source, so no index map was created. + if (!ignoreSymbols || sourceBuffer.FilteredLength == 0 || nativeLocation < 0) + { + if (matchLengthPtr != null) + *matchLengthPtr = nativeLength; + return nativeLocation; + } + + Span rentedIndexMap = sourceBuffer.RentedIndexMapBuffer is not null ? + sourceBuffer.RentedIndexMapBuffer.AsSpan(0, sourceBuffer.FilteredLength) : + stackSourceIndexMap.Slice(0, sourceBuffer.FilteredLength); + + // If ignoring symbols, map filtered indices back to original indices, expanding match length to include removed symbol chars inside the span. + int originalStart = rentedIndexMap[nativeLocation]; + int filteredEnd = nativeLocation + nativeLength - 1; + + Debug.Assert(filteredEnd < source.Length, + $"Filtered end index {filteredEnd} should not exceed the length of the filtered string {source.Length}. nativeLocation={nativeLocation}, nativeLength={nativeLength}"); + + // Find the end position of the character at filteredEnd in the original string. + int endCharStartPos = rentedIndexMap[filteredEnd]; + + // Check if the previous position belongs to the same character (first unit of a surrogate pair) + int firstUnit = (filteredEnd > 0 && rentedIndexMap[filteredEnd - 1] == endCharStartPos) + ? filteredEnd - 1 + : filteredEnd; + + // Check if the next position belongs to the same character (second unit of a surrogate pair) + int lastUnit = (filteredEnd + 1 < source.Length && rentedIndexMap[filteredEnd + 1] == endCharStartPos) + ? filteredEnd + 1 + : filteredEnd; + + int endCharWidth = lastUnit - firstUnit + 1; + int originalEnd = endCharStartPos + endCharWidth; + int originalLength = originalEnd - originalStart; + if (matchLengthPtr != null) - *matchLengthPtr = result.Length; + *matchLengthPtr = originalLength; - return result.Location; + return originalStart; } - private unsafe bool NativeStartsWith(char* pPrefix, int cwPrefixLength, char* pSource, int cwSourceLength, CompareOptions options) + private unsafe bool NativeStartsWith(scoped ReadOnlySpan prefix, scoped ReadOnlySpan source, CompareOptions options) { AssertComparisonSupported(options); - int result = Interop.Globalization.StartsWithNative(m_name, m_name.Length, pPrefix, cwPrefixLength, pSource, cwSourceLength, options); - Debug.Assert(result != (int)ErrorCodes.ERROR_COMPARISON_OPTIONS_NOT_FOUND); + using SymbolFilteringBuffer prefixBuffer = new SymbolFilteringBuffer(); + using SymbolFilteringBuffer sourceBuffer = new SymbolFilteringBuffer(); + + Span stackPrefixBuffer = stackalloc char[StackAllocThreshold]; + Span stackSourceBuffer = stackalloc char[StackAllocThreshold]; + + // Handle IgnoreSymbols preprocessing + if ((options & CompareOptions.IgnoreSymbols) != 0) + { + prefix = !prefixBuffer.TryFilterString(prefix, stackPrefixBuffer, Span.Empty) ? prefix : + prefixBuffer.RentedFilteredBuffer is not null ? + prefixBuffer.RentedFilteredBuffer.AsSpan(0, prefixBuffer.FilteredLength) : + stackPrefixBuffer.Slice(0, prefixBuffer.FilteredLength); - return result > 0 ? true : false; + source = !sourceBuffer.TryFilterString(source, stackSourceBuffer, Span.Empty) ? source : + sourceBuffer.RentedFilteredBuffer is not null ? + sourceBuffer.RentedFilteredBuffer.AsSpan(0, sourceBuffer.FilteredLength) : + stackSourceBuffer.Slice(0, sourceBuffer.FilteredLength); + + // Remove the flag before passing to native since we handled it here + options &= ~CompareOptions.IgnoreSymbols; + } + + fixed (char* pPrefix = &MemoryMarshal.GetReference(prefix)) + fixed (char* pSource = &MemoryMarshal.GetReference(source)) + { + int result = Interop.Globalization.StartsWithNative(m_name, m_name.Length, pPrefix, prefix.Length, pSource, source.Length, options); + Debug.Assert(result != (int)ErrorCodes.ERROR_COMPARISON_OPTIONS_NOT_FOUND); + return result > 0; + } } - private unsafe bool NativeEndsWith(char* pSuffix, int cwSuffixLength, char* pSource, int cwSourceLength, CompareOptions options) + private unsafe bool NativeEndsWith(scoped ReadOnlySpan suffix, scoped ReadOnlySpan source, CompareOptions options) { AssertComparisonSupported(options); - int result = Interop.Globalization.EndsWithNative(m_name, m_name.Length, pSuffix, cwSuffixLength, pSource, cwSourceLength, options); - Debug.Assert(result != (int)ErrorCodes.ERROR_COMPARISON_OPTIONS_NOT_FOUND); + using SymbolFilteringBuffer suffixBuffer = new SymbolFilteringBuffer(); + using SymbolFilteringBuffer sourceBuffer = new SymbolFilteringBuffer(); + + Span stackSuffixBuffer = stackalloc char[StackAllocThreshold]; + Span stackSourceBuffer = stackalloc char[StackAllocThreshold]; + + // Handle IgnoreSymbols preprocessing + if ((options & CompareOptions.IgnoreSymbols) != 0) + { + suffix = !suffixBuffer.TryFilterString(suffix, stackSuffixBuffer, Span.Empty) ? suffix : + suffixBuffer.RentedFilteredBuffer is not null ? + suffixBuffer.RentedFilteredBuffer.AsSpan(0, suffixBuffer.FilteredLength) : + stackSuffixBuffer.Slice(0, suffixBuffer.FilteredLength); + + source = !sourceBuffer.TryFilterString(source, stackSourceBuffer, Span.Empty) ? source : + sourceBuffer.RentedFilteredBuffer is not null ? + sourceBuffer.RentedFilteredBuffer.AsSpan(0, sourceBuffer.FilteredLength) : + stackSourceBuffer.Slice(0, sourceBuffer.FilteredLength); + + // Remove the flag before passing to native since we handled it here + options &= ~CompareOptions.IgnoreSymbols; + } - return result > 0 ? true : false; + fixed (char* pSuffix = &MemoryMarshal.GetReference(suffix)) + fixed (char* pSource = &MemoryMarshal.GetReference(source)) + { + int result = Interop.Globalization.EndsWithNative(m_name, m_name.Length, pSuffix, suffix.Length, pSource, source.Length, options); + Debug.Assert(result != (int)ErrorCodes.ERROR_COMPARISON_OPTIONS_NOT_FOUND); + return result > 0; + } } private static void AssertComparisonSupported(CompareOptions options) @@ -80,9 +228,166 @@ private static void AssertComparisonSupported(CompareOptions options) } private const CompareOptions SupportedCompareOptions = CompareOptions.None | CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace | - CompareOptions.IgnoreWidth | CompareOptions.StringSort | CompareOptions.IgnoreKanaType; + CompareOptions.IgnoreWidth | CompareOptions.StringSort | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreSymbols; private static string GetPNSE(CompareOptions options) => SR.Format(SR.PlatformNotSupported_HybridGlobalizationWithCompareOptions, options); } + + internal struct SymbolFilteringBuffer : IDisposable + { + public SymbolFilteringBuffer() + { + RentedFilteredBuffer = null; + RentedIndexMapBuffer = null; + FilteredLength = 0; + } + + public char[]? RentedFilteredBuffer { get; set; } + public int[]? RentedIndexMapBuffer { get; set; } + public int FilteredLength { get; set; } + + /// + /// Attempt to filter ignorable symbols from the input string. + /// + /// If the input contains ignorable symbols and the stack buffers are not large enough to + /// hold the filtered result, the method may fall back to heap allocation. The index mapping buffer is only + /// populated if index mapping is needed. This method does not modify the original input. + /// The input string to be filtered, represented as a read-only span of UTF-16 characters. + /// A stack-allocated buffer to receive the filtered characters. If this buffer is not long enough to hold the filtered result, + /// the method may fall back to using the ArrayPool. + /// + /// A stack-allocated buffer to receive the mapping from filtered character positions to their original indices + /// in the input. if this span is empty, means no need to create the index mapping. + /// true if the string is filtered removing symbols from there. false if there is no symbols found in the input. + internal bool TryFilterString( + ReadOnlySpan input, + Span filteredStackBuffer, + Span indexMapStackBuffer) + { + if (RentedFilteredBuffer is not null || RentedIndexMapBuffer is not null) + { + Dispose(); + } + + int i = 0; + int consumed = 0; + + // Fast path: scan through the input until we find the first ignorable symbol + for (; i < input.Length;) + { + Rune.DecodeFromUtf16(input.Slice(i), out Rune rune, out consumed); + if (IsIgnorableSymbol(rune)) + { + break; + } + i += consumed; + } + + // If we scanned the entire input without finding any ignorable symbols, return false + if (i >= input.Length) + { + return false; + } + + // Found symbols - decide whether to use stack or heap allocation + Span outSpan = input.Length <= filteredStackBuffer.Length ? filteredStackBuffer : (RentedFilteredBuffer = ArrayPool.Shared.Rent(input.Length)); + // Copy the initial segment that contains no ignorable symbols (positions 0 to i-1) + input.Slice(0, i).CopyTo(outSpan); + + Span indexMap = indexMapStackBuffer.IsEmpty ? + Span.Empty : + (indexMapStackBuffer.Length >= input.Length ? indexMapStackBuffer : (RentedIndexMapBuffer = ArrayPool.Shared.Rent(input.Length))); + + // Initialize the index map for the initial segment with identity mapping + if (!indexMap.IsEmpty) + { + for (int j = 0; j < i; j++) + { + indexMap[j] = j; + } + } + + FilteredLength = i; + i += consumed; // skip the ignorable symbol we just found + + for (; i < input.Length;) + { + Rune.DecodeFromUtf16(input.Slice(i), out Rune rune, out consumed); + if (!IsIgnorableSymbol(rune)) + { + // Copy the UTF-16 units and map each filtered position to the start of the original character + outSpan[FilteredLength] = input[i]; + if (!indexMap.IsEmpty) + { + indexMap[FilteredLength] = i; + } + FilteredLength++; + + if (consumed > 1) + { + outSpan[FilteredLength] = input[i + 1]; + if (!indexMap.IsEmpty) + { + indexMap[FilteredLength] = i + 1; + } + FilteredLength++; + } + } + i += consumed; + } + + return true; + } + + /// + /// Determines whether the specified rune should be ignored when using CompareOptions.IgnoreSymbols. + /// + /// The rune to check. + /// + /// true if the rune should be ignored; otherwise, false. + /// + /// + /// This method returns true for: + /// - All separator categories (SpaceSeparator, LineSeparator, ParagraphSeparator) + /// - All punctuation categories (ConnectorPunctuation through OtherPunctuation) + /// - All symbol categories (MathSymbol through ModifierSymbol) + /// - Whitespace control characters (tab, line feed, vertical tab, form feed, carriage return, etc.) + /// + private static bool IsIgnorableSymbol(Rune rune) + { + UnicodeCategory category = CharUnicodeInfo.GetUnicodeCategory(rune.Value); + + // Check for separator categories (11-13) + if (category >= UnicodeCategory.SpaceSeparator && category <= UnicodeCategory.ParagraphSeparator) + return true; + + // Check for punctuation/symbol categories (18-27) + if (category >= UnicodeCategory.ConnectorPunctuation && category <= UnicodeCategory.ModifierSymbol) + return true; + + // For Control (14) and Format (15) categories, only include whitespace characters + // This includes: tab (U+0009), LF (U+000A), VT (U+000B), FF (U+000C), CR (U+000D), NEL (U+0085) + if (category == UnicodeCategory.Control || category == UnicodeCategory.Format) + return Rune.IsWhiteSpace(rune); + + return false; + } + + public void Dispose() + { + if (RentedFilteredBuffer is not null) + { + ArrayPool.Shared.Return(RentedFilteredBuffer); + RentedFilteredBuffer = null; + } + if (RentedIndexMapBuffer is not null) + { + ArrayPool.Shared.Return(RentedIndexMapBuffer); + RentedIndexMapBuffer = null; + } + + FilteredLength = 0; + } + } } 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 c3e5fd0e586a81..a8e955e9a6eaa3 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 @@ -225,11 +225,7 @@ public static IEnumerable Compare_TestData() yield return new object[] { s_invariantCompare, "FooBar", "Foo\u0400Bar", CompareOptions.Ordinal, -1 }; yield return new object[] { s_invariantCompare, "FooBA\u0300R", "FooB\u00C0R", CompareOptions.IgnoreNonSpace, 0 }; - // In HybridGlobalization on Apple platforms IgnoreSymbols is not supported - if (PlatformDetection.IsNotHybridGlobalizationOnApplePlatform) - { - yield return new object[] { s_invariantCompare, "Test's", "Tests", CompareOptions.IgnoreSymbols, 0 }; - } + yield return new object[] { s_invariantCompare, "Test's", "Tests", CompareOptions.IgnoreSymbols, 0 }; yield return new object[] { s_invariantCompare, "Test's", "Tests", CompareOptions.StringSort, -1 }; yield return new object[] { s_invariantCompare, null, "Tests", CompareOptions.None, -1 }; @@ -251,26 +247,15 @@ public static IEnumerable Compare_TestData() yield return new object[] { s_invariantCompare, "\uFF9E", "\u3099", CompareOptions.IgnoreCase, 0 }; yield return new object[] { s_invariantCompare, "\u20A9", "\uFFE6", CompareOptions.IgnoreCase, -1 }; yield return new object[] { s_invariantCompare, "\u20A9", "\uFFE6", CompareOptions.None, -1 }; - - // In HybridGlobalization mode on Apple platforms IgnoreSymbols is not supported - if (PlatformDetection.IsNotHybridGlobalizationOnApplePlatform) - { - yield return new object[] { s_invariantCompare, "\u0021", "\uFF01", CompareOptions.IgnoreSymbols, 0 }; - yield return new object[] { s_invariantCompare, "\uFF65", "\u30FB", CompareOptions.IgnoreSymbols, 0 }; - yield return new object[] { s_invariantCompare, "\u00A2", "\uFFE0", CompareOptions.IgnoreSymbols, 0 }; - yield return new object[] { s_invariantCompare, "$", "&", CompareOptions.IgnoreSymbols, 0 }; - } + yield return new object[] { s_invariantCompare, "\u0021", "\uFF01", CompareOptions.IgnoreSymbols, 0 }; + yield return new object[] { s_invariantCompare, "\uFF65", "\u30FB", CompareOptions.IgnoreSymbols, 0 }; + yield return new object[] { s_invariantCompare, "\u00A2", "\uFFE0", CompareOptions.IgnoreSymbols, 0 }; + yield return new object[] { s_invariantCompare, "$", "&", CompareOptions.IgnoreSymbols, 0 }; yield return new object[] { s_invariantCompare, "\u0021", "\uFF01", CompareOptions.None, -1 }; yield return new object[] { s_invariantCompare, "\u20A9", "\uFFE6", CompareOptions.IgnoreWidth, 0 }; yield return new object[] { s_invariantCompare, "\u0021", "\uFF01", CompareOptions.IgnoreWidth, 0 }; yield return new object[] { s_invariantCompare, "\uFF66", "\u30F2", CompareOptions.IgnoreWidth, 0 }; - - // In HybridGlobalization mode on Apple platforms IgnoreSymbols is not supported - if(PlatformDetection.IsNotHybridGlobalizationOnApplePlatform) - { - yield return new object[] { s_invariantCompare, "\uFF66", "\u30F2", CompareOptions.IgnoreSymbols, s_expectedHalfToFullFormsComparison }; - } - + yield return new object[] { s_invariantCompare, "\uFF66", "\u30F2", CompareOptions.IgnoreSymbols, s_expectedHalfToFullFormsComparison }; yield return new object[] { s_invariantCompare, "\uFF66", "\u30F2", CompareOptions.IgnoreCase, s_expectedHalfToFullFormsComparison }; yield return new object[] { s_invariantCompare, "\uFF66", "\u30F2", CompareOptions.IgnoreNonSpace, s_expectedHalfToFullFormsComparison }; yield return new object[] { s_invariantCompare, "\uFF66", "\u30F2", CompareOptions.None, s_expectedHalfToFullFormsComparison }; 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 9e6e46db401f4e..25c44d4f0921e8 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 @@ -82,10 +82,59 @@ public static IEnumerable IndexOf_TestData() yield return new object[] { s_invariantCompare, "", "\u200d", 0, 0, CompareOptions.None, 0, 0 }; yield return new object[] { s_invariantCompare, "hello", "\u200d", 1, 3, CompareOptions.IgnoreCase, 1, 0 }; - // Ignore symbols - if (PlatformDetection.IsNotHybridGlobalizationOnApplePlatform) // IgnoreSymbols are not supported - yield return new object[] { s_invariantCompare, "More Test's", "Tests", 0, 11, CompareOptions.IgnoreSymbols, 5, 6 }; + // Ignore symbols - punctuation characters + yield return new object[] { s_invariantCompare, "More Test's", "Tests", 0, 11, CompareOptions.IgnoreSymbols, 5, 6 }; + yield return new object[] { s_invariantCompare, "-Testing", "Testing", 0, 8, CompareOptions.IgnoreSymbols, 1, 7 }; + yield return new object[] { s_invariantCompare, "Test(ing)", "Testing", 0, 9, CompareOptions.IgnoreSymbols, 0, 8 }; + yield return new object[] { s_invariantCompare, "Testing]", "Testing", 0, 8, CompareOptions.IgnoreSymbols, 0, 7 }; + yield return new object[] { s_invariantCompare, "{Test}ing", "Testing", 0, 9, CompareOptions.IgnoreSymbols, 1, 8 }; + yield return new object[] { s_invariantCompare, "\"Testing", "Testing", 0, 8, CompareOptions.IgnoreSymbols, 1, 7 }; + + // Ignore symbols - currency and math symbols + yield return new object[] { s_invariantCompare, "$Testing", "Testing", 0, 8, CompareOptions.IgnoreSymbols, 1, 7 }; + yield return new object[] { s_invariantCompare, "Test€ing", "Testing", 0, 8, CompareOptions.IgnoreSymbols, 0, 8 }; + yield return new object[] { s_invariantCompare, "Testing¢", "Testing", 0, 8, CompareOptions.IgnoreSymbols, 0, 7 }; + yield return new object[] { s_invariantCompare, "+Testing", "Testing", 0, 8, CompareOptions.IgnoreSymbols, 1, 7 }; + yield return new object[] { s_invariantCompare, "Test=ing", "Testing", 0, 8, CompareOptions.IgnoreSymbols, 0, 8 }; + yield return new object[] { s_invariantCompare, "Testing%", "Testing", 0, 8, CompareOptions.IgnoreSymbols, 0, 7 }; + yield return new object[] { s_invariantCompare, "x$test$xtest$x", "test", 0, 14, CompareOptions.IgnoreSymbols, 2, 4 }; + + // Ignore symbols - whitespace characters + yield return new object[] { s_invariantCompare, " Testing", "Testing", 0, 8, CompareOptions.IgnoreSymbols, 1, 7 }; + yield return new object[] { s_invariantCompare, "Testing ", "Testing", 0, 8, CompareOptions.IgnoreSymbols, 0, 7 }; + yield return new object[] { s_invariantCompare, "Test\u00A0ing", "Testing", 0, 8, CompareOptions.IgnoreSymbols, 0, 8 }; // Non-breaking space + yield return new object[] { s_invariantCompare, "Testing\u2028", "Testing", 0, 8, CompareOptions.IgnoreSymbols, 0, 7 }; // Line separator + yield return new object[] { s_invariantCompare, "\u2029Testing", "Testing", 0, 8, CompareOptions.IgnoreSymbols, 1, 7 }; // Paragraph separator + yield return new object[] { s_invariantCompare, "Test\ting\n", "Testing", 0, 9, CompareOptions.IgnoreSymbols, 0, 8 }; // Tab and newline + + // Ignore symbols - multiple whitespace and punctuation + yield return new object[] { s_invariantCompare, " Testing, ", "Testing", 0, 12, CompareOptions.IgnoreSymbols, 2, 7 }; + yield return new object[] { s_invariantCompare, "'Te st i ng!", "Testing", 0, 13, CompareOptions.IgnoreSymbols, 1, 11 }; + + // Ignore symbols - mixed categories + yield return new object[] { s_invariantCompare, "$Te%s t&ing+", "Testing", 0, 12, CompareOptions.IgnoreSymbols, 1, 10 }; + yield return new object[] { s_invariantCompare, ", Hello World!", "HelloWorld", 0, 14, CompareOptions.IgnoreSymbols, 2, 11 }; + + // Ignore symbols - source contains surrogates (to match) + symbols (to ignore) + yield return new object[] { s_invariantCompare, "$\U0001D7D8Testing!", "\U0001D7D8Testing", 0, 11, CompareOptions.IgnoreSymbols, 1, 9 }; + yield return new object[] { s_invariantCompare, "Test$\U0001D7D8ing", "Test\U0001D7D8ing", 0, 10, CompareOptions.IgnoreSymbols, 0, 10 }; + yield return new object[] { s_invariantCompare, "$Testing\U0001D7DA", "Testing\U0001D7DA", 0, 10, CompareOptions.IgnoreSymbols, 1, 9 }; + yield return new object[] { s_invariantCompare, "$\U0001D7D8Test!\U0001D7D9ing", "\U0001D7D8Test\U0001D7D9ing", 0, 13, CompareOptions.IgnoreSymbols, 1, 12 }; + yield return new object[] { s_invariantCompare, "\U0001D7D8 Test$ \U0001D7D9 ing!", "\U0001D7D8 Test \U0001D7D9 ing", 0, 16, CompareOptions.IgnoreSymbols, 0, 15 }; + yield return new object[] { s_invariantCompare, "!$\U0001D7D8Test\U0001D7D9ing\U0001D7DA!", "\U0001D7D8Test\U0001D7D9ing\U0001D7DA", 0, 16, CompareOptions.IgnoreSymbols, 2, 13 }; + + // With symbols - should not match yield return new object[] { s_invariantCompare, "More Test's", "Tests", 0, 11, CompareOptions.None, -1, 0 }; + yield return new object[] { s_invariantCompare, "Tes ting", "Testing", 0, 8, CompareOptions.None, -1, 0 }; + yield return new object[] { s_invariantCompare, "'Te st i ng!", "Testing", 0, 11, CompareOptions.IgnoreSymbols, -1, 0 }; // Not enough characters to match + + // Ignore symbols - long strings (over 256 chars) to test ArrayPool buffer allocation on iOS + if (PlatformDetection.IsHybridGlobalizationOnApplePlatform) + { + yield return new object[] { s_invariantCompare, new string('a', 100) + new string('b', 50) + "$" + new string('b', 50) + "!" + new string('c', 100), new string('b', 100), 0, 302, CompareOptions.IgnoreSymbols, 100, 101 }; + yield return new object[] { s_invariantCompare, new string('a', 100) + new string('b', 100) + new string('c', 100), new string('b', 100), 0, 300, CompareOptions.IgnoreSymbols, 100, 100 }; + } + yield return new object[] { s_invariantCompare, "cbabababdbaba", "ab", 0, 13, CompareOptions.None, 2, 2 }; // Ordinal should be case-sensitive 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 3fba71b155c171..d645cf4a70913c 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 @@ -72,29 +72,23 @@ public static IEnumerable IsPrefix_TestData() yield return new object[] { s_invariantCompare, "\uD800\uD800", "\uD800\uD800", CompareOptions.None, true, 2 }; // Ignore symbols - if (PlatformDetection.IsNotHybridGlobalizationOnApplePlatform) - { - yield return new object[] { s_invariantCompare, "Test's can be interesting", "Tests", CompareOptions.IgnoreSymbols, true, 6 }; - yield return new object[] { s_invariantCompare, "Test's can be interesting", "Tests", CompareOptions.None, false, 0 }; - } + yield return new object[] { s_invariantCompare, "Test's can be interesting", "Tests", CompareOptions.IgnoreSymbols, true, 6 }; + yield return new object[] { s_invariantCompare, "Test's can be interesting", "Tests", CompareOptions.None, false, 0 }; // Platform differences if (PlatformDetection.IsNlsGlobalization) { - if (PlatformDetection.IsNotHybridGlobalizationOnApplePlatform) - { - yield return new object[] { s_hungarianCompare, "dzsdzsfoobar", "ddzsf", CompareOptions.None, true, 7 }; - yield return new object[] { s_invariantCompare, "''Tests", "Tests", CompareOptions.IgnoreSymbols, true, 7 }; - yield return new object[] { s_frenchCompare, "\u0153", "oe", CompareOptions.None, true, 1 }; - } + yield return new object[] { s_hungarianCompare, "dzsdzsfoobar", "ddzsf", CompareOptions.None, true, 7 }; + yield return new object[] { s_invariantCompare, "''Tests", "Tests", CompareOptions.IgnoreSymbols, true, 7 }; + yield return new object[] { s_frenchCompare, "\u0153", "oe", CompareOptions.None, true, 1 }; yield return new object[] { s_invariantCompare, "\uD800\uDC00", "\uD800", CompareOptions.None, true, 1 }; yield return new object[] { s_invariantCompare, "\uD800\uDC00", "\uD800", CompareOptions.IgnoreCase, true, 1 }; } else { yield return new object[] { s_hungarianCompare, "dzsdzsfoobar", "ddzsf", CompareOptions.None, false, 0 }; - if (PlatformDetection.IsNotHybridGlobalizationOnApplePlatform) - yield return new object[] { s_invariantCompare, "''Tests", "Tests", CompareOptions.IgnoreSymbols, false, 0 }; + // Bug in ICU (non-Apple) implementation, correct result should be true (https://github.com/dotnet/runtime/issues/118521) + yield return new object[] { s_invariantCompare, "''Tests", "Tests", CompareOptions.IgnoreSymbols, PlatformDetection.IsHybridGlobalizationOnApplePlatform ? true : false, 0 }; yield return new object[] { s_frenchCompare, "\u0153", "oe", CompareOptions.None, false, 0 }; if (PlatformDetection.IsNotHybridGlobalizationOnApplePlatform) { 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 d545be88e67476..80c911a5a4a69b 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 @@ -80,11 +80,8 @@ public static IEnumerable IsSuffix_TestData() yield return new object[] { s_invariantCompare, "\uD800\uD800", "\uD800\uD800", CompareOptions.None, true, 2 }; // Ignore symbols - if (PlatformDetection.IsNotHybridGlobalizationOnApplePlatform) - { - yield return new object[] { s_invariantCompare, "More Test's", "Tests", CompareOptions.IgnoreSymbols, true, 6 }; - yield return new object[] { s_invariantCompare, "More Test's", "Tests", CompareOptions.None, false, 0 }; - } + yield return new object[] { s_invariantCompare, "More Test's", "Tests", CompareOptions.IgnoreSymbols, true, 6 }; + yield return new object[] { s_invariantCompare, "More Test's", "Tests", CompareOptions.None, false, 0 }; // NULL character yield return new object[] { s_invariantCompare, "a\u0000b", "a\u0000b", CompareOptions.None, true, 3 }; 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 83556e601faec7..b1e0e4c10b9bd2 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 @@ -100,10 +100,46 @@ public static IEnumerable LastIndexOf_TestData() yield return new object[] { s_invariantCompare, "\u0001F601", "\u200d", 1, 2, CompareOptions.None, 2, 0}; // \u0001F601 is GRINNING FACE WITH SMILING EYES surrogate character yield return new object[] { s_invariantCompare, "AA\u200DA", "\u200d", 3, 4, CompareOptions.None, 4, 0}; - // Ignore symbols - if (PlatformDetection.IsNotHybridGlobalizationOnApplePlatform) // IgnoreSymbols are not supported - yield return new object[] { s_invariantCompare, "More Test's", "Tests", 10, 11, CompareOptions.IgnoreSymbols, 5, 6 }; + // Ignore symbols - punctuation characters + yield return new object[] { s_invariantCompare, "More Test's", "Tests", 10, 11, CompareOptions.IgnoreSymbols, 5, 6 }; + yield return new object[] { s_invariantCompare, "-Testing", "Testing", 7, 8, CompareOptions.IgnoreSymbols, 1, 7 }; + yield return new object[] { s_invariantCompare, "Testing]", "Testing", 7, 8, CompareOptions.IgnoreSymbols, 0, 7 }; + yield return new object[] { s_invariantCompare, "\"Testing", "Testing", 7, 8, CompareOptions.IgnoreSymbols, 1, 7 }; + + // Ignore symbols - currency and math symbols + yield return new object[] { s_invariantCompare, "$Testing", "Testing", 7, 8, CompareOptions.IgnoreSymbols, 1, 7 }; + yield return new object[] { s_invariantCompare, "Test€ing", "Testing", 7, 8, CompareOptions.IgnoreSymbols, 0, 8 }; + yield return new object[] { s_invariantCompare, "Testing¢", "Testing", 7, 8, CompareOptions.IgnoreSymbols, 0, 7 }; + yield return new object[] { s_invariantCompare, "Test=ing", "Testing", 7, 8, CompareOptions.IgnoreSymbols, 0, 8 }; + + // Ignore symbols - whitespace characters + yield return new object[] { s_invariantCompare, " Testing", "Testing", 7, 8, CompareOptions.IgnoreSymbols, 1, 7 }; + yield return new object[] { s_invariantCompare, "Testing ", "Testing", 7, 8, CompareOptions.IgnoreSymbols, 0, 7 }; + yield return new object[] { s_invariantCompare, "Test\u00A0ing", "Testing", 7, 8, CompareOptions.IgnoreSymbols, 0, 8 }; // Non-breaking space + yield return new object[] { s_invariantCompare, "Testing\u2028", "Testing", 7, 8, CompareOptions.IgnoreSymbols, 0, 7 }; // Line separator + yield return new object[] { s_invariantCompare, "Test\ting\n", "Testing", 8, 9, CompareOptions.IgnoreSymbols, 0, 8 }; // Tab and newline + + // Ignore symbols - multiple whitespace and punctuation + yield return new object[] { s_invariantCompare, " Testing, ", "Testing", 10, 11, CompareOptions.IgnoreSymbols, 2, 7 }; + yield return new object[] { s_invariantCompare, "'Te st i ng!", "Testing", 12, 13, CompareOptions.IgnoreSymbols, 1, 11 }; + + // Ignore symbols - mixed categories + yield return new object[] { s_invariantCompare, "$Te%s t&ing+", "Testing", 11, 12, CompareOptions.IgnoreSymbols, 1, 10 }; + yield return new object[] { s_invariantCompare, ", Hello World!", "HelloWorld", 13, 14, CompareOptions.IgnoreSymbols, 2, 11 }; + yield return new object[] { s_invariantCompare, "x$test$xtest$x", "test", 13, 14, CompareOptions.IgnoreSymbols, 8, 4 }; + + // Ignore symbols - source contains surrogates (to match) + symbols (to ignore) + yield return new object[] { s_invariantCompare, "$\U0001D7D8Testing!", "\U0001D7D8Testing", 10, 11, CompareOptions.IgnoreSymbols, 1, 9 }; + yield return new object[] { s_invariantCompare, "Test$\U0001D7D8ing", "Test\U0001D7D8ing", 9, 10, CompareOptions.IgnoreSymbols, 0, 10 }; + yield return new object[] { s_invariantCompare, "$Testing\U0001D7DA", "Testing\U0001D7DA", 9, 10, CompareOptions.IgnoreSymbols, 1, 9 }; + yield return new object[] { s_invariantCompare, "$\U0001D7D8Test!\U0001D7D9ing", "\U0001D7D8Test\U0001D7D9ing", 12, 13, CompareOptions.IgnoreSymbols, 1, 12 }; + yield return new object[] { s_invariantCompare, "\U0001D7D8 Test$ \U0001D7D9 ing!", "\U0001D7D8 Test \U0001D7D9 ing", 14, 15, CompareOptions.IgnoreSymbols, 0, 15 }; + yield return new object[] { s_invariantCompare, "!$\U0001D7D8Test\U0001D7D9ing\U0001D7DA!", "\U0001D7D8Test\U0001D7D9ing\U0001D7DA", 14, 15, CompareOptions.IgnoreSymbols, 2, 13 }; + + // With symbols - should not match yield return new object[] { s_invariantCompare, "More Test's", "Tests", 10, 11, CompareOptions.None, -1, 0 }; + yield return new object[] { s_invariantCompare, "Tes ting", "Testing", 7, 8, CompareOptions.None, -1, 0 }; + yield return new object[] { s_invariantCompare, "cbabababdbaba", "ab", 12, 13, CompareOptions.None, 10, 2 }; // Platform differences diff --git a/src/native/libs/System.Globalization.Native/pal_collation.m b/src/native/libs/System.Globalization.Native/pal_collation.m index 56af941fb40033..4c6f54f8a27726 100644 --- a/src/native/libs/System.Globalization.Native/pal_collation.m +++ b/src/native/libs/System.Globalization.Native/pal_collation.m @@ -13,14 +13,14 @@ #if defined(APPLE_HYBRID_GLOBALIZATION) // Enum that corresponds to C# CompareOptions -typedef enum +typedef enum : int32_t { - None = 0, - IgnoreCase = 1, - IgnoreNonSpace = 2, - IgnoreKanaType = 8, - IgnoreWidth = 16, - StringSort = 536870912, + None = 0x00000000, + IgnoreCase = 0x00000001, + IgnoreNonSpace = 0x00000002, + IgnoreKanaType = 0x00000008, + IgnoreWidth = 0x00000010, + StringSort = 0x20000000, } CompareOptions; typedef enum @@ -45,7 +45,7 @@ return currentLocale; } -static bool IsComparisonOptionSupported(int32_t comparisonOptions) +static bool IsComparisonOptionSupported(CompareOptions comparisonOptions) { int32_t supportedOptions = None | IgnoreCase | IgnoreNonSpace | IgnoreWidth | StringSort | IgnoreKanaType; if ((comparisonOptions | supportedOptions) != supportedOptions) @@ -53,7 +53,7 @@ static bool IsComparisonOptionSupported(int32_t comparisonOptions) return true; } -static NSStringCompareOptions ConvertFromCompareOptionsToNSStringCompareOptions(int32_t comparisonOptions, bool isLiteralSearchSupported) +static NSStringCompareOptions ConvertFromCompareOptionsToNSStringCompareOptions(CompareOptions comparisonOptions, bool isLiteralSearchSupported) { // To achieve an equivalent search behavior to the default in ICU, // NSLiteralSearch is employed as the default search option.