diff --git a/src/System.Memory/src/System/MemoryExtensions.Portable.cs b/src/System.Memory/src/System/MemoryExtensions.Portable.cs index e941813e9d86..a480fcbcd672 100644 --- a/src/System.Memory/src/System/MemoryExtensions.Portable.cs +++ b/src/System.Memory/src/System/MemoryExtensions.Portable.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Globalization; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace System { @@ -40,10 +41,25 @@ public static bool Equals(this ReadOnlySpan span, ReadOnlySpan other { return span.SequenceEqual(other); } + else if (comparisonType == StringComparison.OrdinalIgnoreCase) + { + if (span.Length != value.Length) + return false; + return EqualsOrdinalIgnoreCase(span, value); + } return span.ToString().Equals(other.ToString(), comparisonType); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool EqualsOrdinalIgnoreCase(ReadOnlySpan span, ReadOnlySpan value) + { + Debug.Assert(span.Length == value.Length); + if (value.Length == 0) // span.Length == value.Length == 0 + return true; + return (CompareToOrdinalIgnoreCase(span, value) == 0); + } + /// /// Compares the specified and using the specified , /// and returns an integer that indicates their relative position in the sort order. @@ -57,10 +73,60 @@ public static int CompareTo(this ReadOnlySpan span, ReadOnlySpan oth { return span.SequenceCompareTo(other); } + else if (comparisonType == StringComparison.OrdinalIgnoreCase) + { + return CompareToOrdinalIgnoreCase(span, value); + } return string.Compare(span.ToString(), other.ToString(), comparisonType); } + // Borrowed from https://github.com/dotnet/coreclr/blob/master/src/mscorlib/shared/System/Globalization/CompareInfo.cs#L539 + private static unsafe int CompareToOrdinalIgnoreCase(ReadOnlySpan strA, ReadOnlySpan strB) + { + int length = Math.Min(strA.Length, strB.Length); + int range = length; + + fixed (char* ap = &MemoryMarshal.GetReference(strA)) + fixed (char* bp = &MemoryMarshal.GetReference(strB)) + { + char* a = ap; + char* b = bp; + + while (length != 0 && (*a <= 0x7F) && (*b <= 0x7F)) + { + int charA = *a; + int charB = *b; + + if (charA == charB) + { + a++; b++; + length--; + continue; + } + + // uppercase both chars - notice that we need just one compare per char + if ((uint)(charA - 'a') <= 'z' - 'a') charA -= 0x20; + if ((uint)(charB - 'a') <= 'z' - 'a') charB -= 0x20; + + // Return the (case-insensitive) difference between them. + if (charA != charB) + return charA - charB; + + // Next char + a++; b++; + length--; + } + + if (length == 0) + return strA.Length - strB.Length; + + range -= length; + + return string.Compare(strA.Slice(range).ToString(), strB.Slice(range).ToString(), StringComparison.OrdinalIgnoreCase); + } + } + /// /// Reports the zero-based index of the first occurrence of the specified in the current . /// The source span. @@ -175,6 +241,10 @@ public static bool EndsWith(this ReadOnlySpan span, ReadOnlySpan oth { return span.EndsWith(other); } + else if (comparisonType == StringComparison.OrdinalIgnoreCase) + { + return value.Length <= span.Length && EqualsOrdinalIgnoreCase(span.Slice(span.Length - value.Length), value); + } string sourceString = span.ToString(); string valueString = other.ToString(); @@ -193,6 +263,10 @@ public static bool StartsWith(this ReadOnlySpan span, ReadOnlySpan o { return span.StartsWith(other); } + else if (comparisonType == StringComparison.OrdinalIgnoreCase) + { + return value.Length <= span.Length && EqualsOrdinalIgnoreCase(span.Slice(0, value.Length), value); + } string sourceString = span.ToString(); string valueString = other.ToString(); diff --git a/src/System.Memory/tests/ReadOnlySpan/CompareTo.cs b/src/System.Memory/tests/ReadOnlySpan/CompareTo.cs index ab1f294fe16e..5033c324c8c3 100644 --- a/src/System.Memory/tests/ReadOnlySpan/CompareTo.cs +++ b/src/System.Memory/tests/ReadOnlySpan/CompareTo.cs @@ -115,9 +115,12 @@ 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( - string.Compare(firstSpan.ToString(), secondSpan.ToString(), StringComparison.OrdinalIgnoreCase), - firstSpan.CompareTo(secondSpan, StringComparison.OrdinalIgnoreCase)); + 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));