Skip to content

Commit

Permalink
Add Span.StartsWith(T) and EndsWith(T) (#103458)
Browse files Browse the repository at this point in the history
* Add Span.StartsWith(T) and EndsWith(T)

* Add tests

* Use it in a few places

* Add AggressiveInlining
  • Loading branch information
MihaZupan committed Jun 16, 2024
1 parent 2d9373a commit af49288
Show file tree
Hide file tree
Showing 11 changed files with 86 additions and 8 deletions.
4 changes: 2 additions & 2 deletions src/libraries/Common/src/System/Net/CookieComparer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ internal static bool Equals(Cookie left, Cookie right)

internal static bool EqualDomains(ReadOnlySpan<char> left, ReadOnlySpan<char> right)
{
if (left.Length != 0 && left[0] == '.') left = left.Slice(1);
if (right.Length != 0 && right[0] == '.') right = right.Slice(1);
if (left.StartsWith('.')) left = left.Slice(1);
if (right.StartsWith('.')) right = right.Slice(1);

return left.Equals(right, StringComparison.OrdinalIgnoreCase);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -674,7 +674,7 @@ private static void BuildCommandLine(ProcessStartInfo startInfo, ref ValueString
// problems (it specifies exactly which part of the string
// is the file to execute).
ReadOnlySpan<char> fileName = startInfo.FileName.AsSpan().Trim();
bool fileNameIsQuoted = fileName.Length > 0 && fileName[0] == '\"' && fileName[fileName.Length - 1] == '\"';
bool fileNameIsQuoted = fileName.StartsWith('"') && fileName.EndsWith('"');
if (!fileNameIsQuoted)
{
commandLine.Append('"');
Expand Down
2 changes: 2 additions & 0 deletions src/libraries/System.Memory/ref/System.Memory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ public static partial class MemoryExtensions
public static bool EndsWith(this System.ReadOnlySpan<char> span, System.ReadOnlySpan<char> value, System.StringComparison comparisonType) { throw null; }
public static bool EndsWith<T>(this System.ReadOnlySpan<T> span, System.ReadOnlySpan<T> value) where T : System.IEquatable<T>? { throw null; }
public static bool EndsWith<T>(this System.Span<T> span, System.ReadOnlySpan<T> value) where T : System.IEquatable<T>? { throw null; }
public static bool EndsWith<T>(this System.ReadOnlySpan<T> span, T value) where T : System.IEquatable<T>? { throw null; }
public static System.Text.SpanLineEnumerator EnumerateLines(this System.ReadOnlySpan<char> span) { throw null; }
public static System.Text.SpanLineEnumerator EnumerateLines(this System.Span<char> span) { throw null; }
public static System.Text.SpanRuneEnumerator EnumerateRunes(this System.ReadOnlySpan<char> span) { throw null; }
Expand Down Expand Up @@ -356,6 +357,7 @@ public static partial class MemoryExtensions
public static bool StartsWith(this System.ReadOnlySpan<char> span, System.ReadOnlySpan<char> value, System.StringComparison comparisonType) { throw null; }
public static bool StartsWith<T>(this System.ReadOnlySpan<T> span, System.ReadOnlySpan<T> value) where T : System.IEquatable<T>? { throw null; }
public static bool StartsWith<T>(this System.Span<T> span, System.ReadOnlySpan<T> value) where T : System.IEquatable<T>? { throw null; }
public static bool StartsWith<T>(this System.ReadOnlySpan<T> span, T value) where T : System.IEquatable<T>? { throw null; }
public static int ToLower(this System.ReadOnlySpan<char> source, System.Span<char> destination, System.Globalization.CultureInfo? culture) { throw null; }
public static int ToLowerInvariant(this System.ReadOnlySpan<char> source, System.Span<char> destination) { throw null; }
public static int ToUpper(this System.ReadOnlySpan<char> source, System.Span<char> destination, System.Globalization.CultureInfo? culture) { throw null; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ private bool TryReadToSlow(out ReadOnlySequence<T> sequence, T delimiter, T deli
else
{
// No delimiter, need to check the end of the span for odd number of escapes then advance
if (remaining.Length > 0 && remaining[remaining.Length - 1].Equals(delimiterEscape))
if (remaining.EndsWith(delimiterEscape))
{
int escapeCount = 1;
int i = remaining.Length - 2;
Expand Down
29 changes: 29 additions & 0 deletions src/libraries/System.Memory/tests/Span/EndsWith.T.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,5 +88,34 @@ public static void OnEndsWithOfEqualSpansMakeSureEveryElementIsCompared()
}
}
}

[Fact]
public static void EndsWithSingle()
{
ReadOnlySpan<char> chars = [];
Assert.False(chars.EndsWith('\0'));
Assert.False(chars.EndsWith('f'));

chars = "foo";
Assert.True(chars.EndsWith(chars[^1]));
Assert.True(chars.EndsWith('o'));
Assert.False(chars.EndsWith('f'));

scoped ReadOnlySpan<string> strings = [];
Assert.False(strings.EndsWith((string)null));
Assert.False(strings.EndsWith("foo"));

strings = ["foo", "bar"];
Assert.True(strings.EndsWith(strings[^1]));
Assert.True(strings.EndsWith("bar"));
Assert.True(strings.EndsWith("*bar".Substring(1)));
Assert.False(strings.EndsWith("foo"));
Assert.False(strings.EndsWith((string)null));

strings = ["foo", null];
Assert.True(strings.EndsWith(strings[^1]));
Assert.True(strings.EndsWith((string)null));
Assert.False(strings.EndsWith("foo"));
}
}
}
29 changes: 29 additions & 0 deletions src/libraries/System.Memory/tests/Span/StartsWith.T.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,5 +150,34 @@ public static void MakeSureNoStartsWithChecksGoOutOfRange()
Assert.True(b);
}
}

[Fact]
public static void StartsWithSingle()
{
ReadOnlySpan<char> chars = [];
Assert.False(chars.StartsWith('\0'));
Assert.False(chars.StartsWith('f'));

chars = "foo";
Assert.True(chars.StartsWith(chars[0]));
Assert.True(chars.StartsWith('f'));
Assert.False(chars.StartsWith('o'));

scoped ReadOnlySpan<string> strings = [];
Assert.False(strings.StartsWith((string)null));
Assert.False(strings.StartsWith("foo"));

strings = ["foo", "bar"];
Assert.True(strings.StartsWith(strings[0]));
Assert.True(strings.StartsWith("foo"));
Assert.True(strings.StartsWith("*foo".Substring(1)));
Assert.False(strings.StartsWith("bar"));
Assert.False(strings.StartsWith((string)null));

strings = [null, "bar"];
Assert.True(strings.StartsWith(strings[0]));
Assert.True(strings.StartsWith((string)null));
Assert.False(strings.StartsWith("bar"));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ internal bool IsFileSystemEntryHidden(ReadOnlySpan<char> path, ReadOnlySpan<char
return HasHiddenFlag;
}

internal static bool IsNameHidden(ReadOnlySpan<char> fileName) => fileName.Length > 0 && fileName[0] == '.';
internal static bool IsNameHidden(ReadOnlySpan<char> fileName) => fileName.StartsWith('.');

// Returns true if the path points to a directory, or if the path is a symbolic link
// that points to a directory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ public static bool IsPathRooted([NotNullWhen(true)] string? path)

public static bool IsPathRooted(ReadOnlySpan<char> path)
{
return path.Length > 0 && path[0] == PathInternal.DirectorySeparatorChar;
return path.StartsWith(PathInternal.DirectorySeparatorChar);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2619,6 +2619,24 @@ public static unsafe int SequenceCompareTo<T>(this ReadOnlySpan<T> span, ReadOnl
valueLength);
}

/// <summary>
/// Determines whether the specified value appears at the start of the span.
/// </summary>
/// <param name="span">The span to search.</param>
/// <param name="value">The value to compare.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool StartsWith<T>(this ReadOnlySpan<T> span, T value) where T : IEquatable<T>? =>
span.Length != 0 && (span[0]?.Equals(value) ?? (object?)value is null);

/// <summary>
/// Determines whether the specified value appears at the end of the span.
/// </summary>
/// <param name="span">The span to search.</param>
/// <param name="value">The value to compare.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool EndsWith<T>(this ReadOnlySpan<T> span, T value) where T : IEquatable<T>? =>
span.Length != 0 && (span[^1]?.Equals(value) ?? (object?)value is null);

/// <summary>
/// Reverses the sequence of the elements in the entire span.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,7 @@ private static bool FilterNameImpl(MemberInfo m, object filterCriteria, StringCo
}

// Check to see if this is a prefix or exact match requirement
if (str.Length > 0 && str[str.Length - 1] == '*')
if (str.EndsWith('*'))
{
str = str.Slice(0, str.Length - 1);
return name.StartsWith(str, comparison);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ internal static byte[] LaxDecodeHexString(this string hexString)

ReadOnlySpan<char> s = hexString;

if (s.Length != 0 && s[0] == '\u200E')
if (s.StartsWith('\u200E'))
{
s = s.Slice(1);
}
Expand Down

0 comments on commit af49288

Please sign in to comment.