Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.

Commit

Permalink
Optimize the string-like Span APIs for OrdinalIgnoreCase (portable Sp…
Browse files Browse the repository at this point in the history
…an) (#28239)

* Optimize the string-like Span APIs for OrdinalIgnoreCase (portable Span)

* Addressing PR feedback
  • Loading branch information
ahsonkhan committed Mar 21, 2018
1 parent 2169709 commit a8cfc6a
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 2 deletions.
74 changes: 74 additions & 0 deletions src/System.Memory/src/System/MemoryExtensions.Portable.cs
Expand Up @@ -5,6 +5,7 @@
using System.Diagnostics;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace System
{
Expand Down Expand Up @@ -40,10 +41,25 @@ public static bool Equals(this ReadOnlySpan<char> span, ReadOnlySpan<char> other
{
return span.SequenceEqual<char>(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<char> span, ReadOnlySpan<char> value)
{
Debug.Assert(span.Length == value.Length);
if (value.Length == 0) // span.Length == value.Length == 0
return true;
return (CompareToOrdinalIgnoreCase(span, value) == 0);
}

/// <summary>
/// Compares the specified <paramref name="span"/> and <paramref name="other"/> using the specified <paramref name="comparisonType"/>,
/// and returns an integer that indicates their relative position in the sort order.
Expand All @@ -57,10 +73,60 @@ public static int CompareTo(this ReadOnlySpan<char> span, ReadOnlySpan<char> 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<char> strA, ReadOnlySpan<char> 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);
}
}

/// <summary>
/// Reports the zero-based index of the first occurrence of the specified <paramref name="value"/> in the current <paramref name="span"/>.
/// <param name="span">The source span.</param>
Expand Down Expand Up @@ -175,6 +241,10 @@ public static bool EndsWith(this ReadOnlySpan<char> span, ReadOnlySpan<char> oth
{
return span.EndsWith<char>(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();
Expand All @@ -193,6 +263,10 @@ public static bool StartsWith(this ReadOnlySpan<char> span, ReadOnlySpan<char> o
{
return span.StartsWith<char>(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();
Expand Down
7 changes: 5 additions & 2 deletions src/System.Memory/tests/ReadOnlySpan/CompareTo.cs
Expand Up @@ -115,9 +115,12 @@ public static void CompareToNoMatch_StringComparison()
var secondSpan = new ReadOnlySpan<char>(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));
Expand Down

0 comments on commit a8cfc6a

Please sign in to comment.