From af49288b790fdcd849469a86086191a91256dcff Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Mon, 17 Jun 2024 00:36:04 +0200 Subject: [PATCH] Add Span.StartsWith(T) and EndsWith(T) (#103458) * Add Span.StartsWith(T) and EndsWith(T) * Add tests * Use it in a few places * Add AggressiveInlining --- .../Common/src/System/Net/CookieComparer.cs | 4 +-- .../src/System/Diagnostics/Process.Windows.cs | 2 +- .../System.Memory/ref/System.Memory.cs | 2 ++ .../System/Buffers/SequenceReader.Search.cs | 2 +- .../System.Memory/tests/Span/EndsWith.T.cs | 29 +++++++++++++++++++ .../System.Memory/tests/Span/StartsWith.T.cs | 29 +++++++++++++++++++ .../src/System/IO/FileStatus.Unix.cs | 2 +- .../src/System/IO/Path.Unix.cs | 2 +- .../src/System/MemoryExtensions.cs | 18 ++++++++++++ .../src/System/Type.Helpers.cs | 2 +- .../System/Security/Cryptography/Helpers.cs | 2 +- 11 files changed, 86 insertions(+), 8 deletions(-) diff --git a/src/libraries/Common/src/System/Net/CookieComparer.cs b/src/libraries/Common/src/System/Net/CookieComparer.cs index 529ec06a51a1f..a2980c5c8746e 100644 --- a/src/libraries/Common/src/System/Net/CookieComparer.cs +++ b/src/libraries/Common/src/System/Net/CookieComparer.cs @@ -24,8 +24,8 @@ internal static bool Equals(Cookie left, Cookie right) internal static bool EqualDomains(ReadOnlySpan left, ReadOnlySpan 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); } diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs index 6054597a37fb6..c8096a1394c70 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs @@ -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 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('"'); diff --git a/src/libraries/System.Memory/ref/System.Memory.cs b/src/libraries/System.Memory/ref/System.Memory.cs index edacc807a04fc..2b527425384e7 100644 --- a/src/libraries/System.Memory/ref/System.Memory.cs +++ b/src/libraries/System.Memory/ref/System.Memory.cs @@ -268,6 +268,7 @@ public static void CopyTo(this T[]? source, System.Span destination) { } public static bool EndsWith(this System.ReadOnlySpan span, System.ReadOnlySpan value, System.StringComparison comparisonType) { throw null; } public static bool EndsWith(this System.ReadOnlySpan span, System.ReadOnlySpan value) where T : System.IEquatable? { throw null; } public static bool EndsWith(this System.Span span, System.ReadOnlySpan value) where T : System.IEquatable? { throw null; } + public static bool EndsWith(this System.ReadOnlySpan span, T value) where T : System.IEquatable? { throw null; } public static System.Text.SpanLineEnumerator EnumerateLines(this System.ReadOnlySpan span) { throw null; } public static System.Text.SpanLineEnumerator EnumerateLines(this System.Span span) { throw null; } public static System.Text.SpanRuneEnumerator EnumerateRunes(this System.ReadOnlySpan span) { throw null; } @@ -356,6 +357,7 @@ public static void Sort(this System.Span keys, Sy public static bool StartsWith(this System.ReadOnlySpan span, System.ReadOnlySpan value, System.StringComparison comparisonType) { throw null; } public static bool StartsWith(this System.ReadOnlySpan span, System.ReadOnlySpan value) where T : System.IEquatable? { throw null; } public static bool StartsWith(this System.Span span, System.ReadOnlySpan value) where T : System.IEquatable? { throw null; } + public static bool StartsWith(this System.ReadOnlySpan span, T value) where T : System.IEquatable? { throw null; } public static int ToLower(this System.ReadOnlySpan source, System.Span destination, System.Globalization.CultureInfo? culture) { throw null; } public static int ToLowerInvariant(this System.ReadOnlySpan source, System.Span destination) { throw null; } public static int ToUpper(this System.ReadOnlySpan source, System.Span destination, System.Globalization.CultureInfo? culture) { throw null; } diff --git a/src/libraries/System.Memory/src/System/Buffers/SequenceReader.Search.cs b/src/libraries/System.Memory/src/System/Buffers/SequenceReader.Search.cs index 27aef7056647d..f76f6e3844035 100644 --- a/src/libraries/System.Memory/src/System/Buffers/SequenceReader.Search.cs +++ b/src/libraries/System.Memory/src/System/Buffers/SequenceReader.Search.cs @@ -142,7 +142,7 @@ private bool TryReadToSlow(out ReadOnlySequence 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; diff --git a/src/libraries/System.Memory/tests/Span/EndsWith.T.cs b/src/libraries/System.Memory/tests/Span/EndsWith.T.cs index b320e46c936a9..ce6fcdfe05ba1 100644 --- a/src/libraries/System.Memory/tests/Span/EndsWith.T.cs +++ b/src/libraries/System.Memory/tests/Span/EndsWith.T.cs @@ -88,5 +88,34 @@ public static void OnEndsWithOfEqualSpansMakeSureEveryElementIsCompared() } } } + + [Fact] + public static void EndsWithSingle() + { + ReadOnlySpan 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 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")); + } } } diff --git a/src/libraries/System.Memory/tests/Span/StartsWith.T.cs b/src/libraries/System.Memory/tests/Span/StartsWith.T.cs index 36acc7a296d94..a1aa2ce99838c 100644 --- a/src/libraries/System.Memory/tests/Span/StartsWith.T.cs +++ b/src/libraries/System.Memory/tests/Span/StartsWith.T.cs @@ -150,5 +150,34 @@ public static void MakeSureNoStartsWithChecksGoOutOfRange() Assert.True(b); } } + + [Fact] + public static void StartsWithSingle() + { + ReadOnlySpan 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 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")); + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStatus.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStatus.Unix.cs index 283404991036a..a7b9da7e9197d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStatus.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStatus.Unix.cs @@ -168,7 +168,7 @@ internal bool IsFileSystemEntryHidden(ReadOnlySpan path, ReadOnlySpan fileName) => fileName.Length > 0 && fileName[0] == '.'; + internal static bool IsNameHidden(ReadOnlySpan 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 diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Path.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Path.Unix.cs index 1c8a9551174f4..b0af7e709220a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Path.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Path.Unix.cs @@ -137,7 +137,7 @@ public static bool IsPathRooted([NotNullWhen(true)] string? path) public static bool IsPathRooted(ReadOnlySpan path) { - return path.Length > 0 && path[0] == PathInternal.DirectorySeparatorChar; + return path.StartsWith(PathInternal.DirectorySeparatorChar); } /// diff --git a/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.cs b/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.cs index f088e3ced535a..45ec3c59614f5 100644 --- a/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.cs +++ b/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.cs @@ -2619,6 +2619,24 @@ ref MemoryMarshal.GetReference(value), valueLength); } + /// + /// Determines whether the specified value appears at the start of the span. + /// + /// The span to search. + /// The value to compare. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool StartsWith(this ReadOnlySpan span, T value) where T : IEquatable? => + span.Length != 0 && (span[0]?.Equals(value) ?? (object?)value is null); + + /// + /// Determines whether the specified value appears at the end of the span. + /// + /// The span to search. + /// The value to compare. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool EndsWith(this ReadOnlySpan span, T value) where T : IEquatable? => + span.Length != 0 && (span[^1]?.Equals(value) ?? (object?)value is null); + /// /// Reverses the sequence of the elements in the entire span. /// diff --git a/src/libraries/System.Private.CoreLib/src/System/Type.Helpers.cs b/src/libraries/System.Private.CoreLib/src/System/Type.Helpers.cs index 0e435d60718a6..251085957c39f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Type.Helpers.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Type.Helpers.cs @@ -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); diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/Helpers.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/Helpers.cs index 116cf5b7ee8e0..a232dfa5f4983 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/Helpers.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/Helpers.cs @@ -84,7 +84,7 @@ internal static byte[] LaxDecodeHexString(this string hexString) ReadOnlySpan s = hexString; - if (s.Length != 0 && s[0] == '\u200E') + if (s.StartsWith('\u200E')) { s = s.Slice(1); }