From 41224356bfc918b724905100f382ab09e7315147 Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Fri, 8 May 2026 03:30:16 +0200 Subject: [PATCH 1/2] Improve some string Rune helpers --- .../src/System/String.Comparison.cs | 18 ++++- .../src/System/String.Manipulation.cs | 79 +++++++------------ .../src/System/String.Searching.cs | 5 ++ 3 files changed, 48 insertions(+), 54 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/String.Comparison.cs b/src/libraries/System.Private.CoreLib/src/System/String.Comparison.cs index 665e9b40feadf1..541045aa712aa1 100644 --- a/src/libraries/System.Private.CoreLib/src/System/String.Comparison.cs +++ b/src/libraries/System.Private.CoreLib/src/System/String.Comparison.cs @@ -523,7 +523,14 @@ public bool EndsWith(char value, StringComparison comparisonType) /// if matches the end of this instance; otherwise, . public bool EndsWith(Rune value) { - return EndsWith(value, StringComparison.Ordinal); + if (value.IsBmp) + { + return EndsWith((char)value.Value); + } + + UnicodeUtility.GetUtf16SurrogatesFromSupplementaryPlaneScalar((uint)value.Value, out char highSurrogate, out char lowSurrogate); + + return Length > 1 && this[^2] == highSurrogate && this[^1] == lowSurrogate; } /// @@ -1134,7 +1141,14 @@ public bool StartsWith(char value, StringComparison comparisonType) /// if value matches the beginning of this string; otherwise, . public bool StartsWith(Rune value) { - return StartsWith(value, StringComparison.Ordinal); + if (value.IsBmp) + { + return StartsWith((char)value.Value); + } + + UnicodeUtility.GetUtf16SurrogatesFromSupplementaryPlaneScalar((uint)value.Value, out char highSurrogate, out char lowSurrogate); + + return Length > 1 && _firstChar == highSurrogate && this[1] == lowSurrogate; } /// diff --git a/src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs b/src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs index 343cf7bc9734d8..788abe5d311a50 100644 --- a/src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs +++ b/src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs @@ -1454,9 +1454,9 @@ private string ReplaceHelper(int oldValueLength, string newValue, ReadOnlySpan public string Replace(Rune oldRune, Rune newRune) { - if (Length == 0) + if (oldRune.IsBmp && newRune.IsBmp) { - return this; + return Replace((char)oldRune.Value, (char)newRune.Value); } ReadOnlySpan oldChars = oldRune.AsSpan(stackalloc char[Rune.MaxUtf16CharsPerRune]); @@ -1682,17 +1682,17 @@ public string[] Split(Rune separator, StringSplitOptions options = StringSplitOp /// An array whose elements contain the substrings from this instance that are delimited by . public string[] Split(Rune separator, int count, StringSplitOptions options = StringSplitOptions.None) { - ReadOnlySpan separatorSpan = separator.AsSpan(stackalloc char[Rune.MaxUtf16CharsPerRune]); - - if (separatorSpan.Length == 1) + if (separator.IsBmp) { - return Split(separatorSpan[0], count, options); + return Split((char)separator.Value, count, options); } ArgumentOutOfRangeException.ThrowIfNegative(count); CheckStringSplitOptions(options); + ReadOnlySpan separatorSpan = separator.AsSpan(stackalloc char[Rune.MaxUtf16CharsPerRune]); + // Ensure matching the string separator overload. return (count <= 1 || Length == 0) ? CreateSplitArrayOfThisAsSoleValue(options, count) : Split(separatorSpan, count, options); } @@ -2411,39 +2411,28 @@ public unsafe string Trim(char trimChar) /// public string Trim(Rune trimRune) { - if (Length == 0) + if (trimRune.IsBmp) { - return this; + return Trim((char)trimRune.Value); } - // Convert trimRune to span - ReadOnlySpan trimChars = trimRune.AsSpan(stackalloc char[Rune.MaxUtf16CharsPerRune]); + UnicodeUtility.GetUtf16SurrogatesFromSupplementaryPlaneScalar((uint)trimRune.Value, out char highSurrogate, out char lowSurrogate); // Trim start int index = 0; - while (index < Length && this.AsSpan(index).StartsWith(trimChars)) + while ((uint)(index + 1) < (uint)Length && this[index] == highSurrogate && this[index + 1] == lowSurrogate) { - index += trimChars.Length; - } - - if (index >= Length) - { - return Empty; + index += 2; } // Trim end - int endIndex = Length - 1; - while (endIndex >= index && this.AsSpan(index..(endIndex + 1)).EndsWith(trimChars)) - { - endIndex -= trimChars.Length; - } - - if (endIndex < index) + int endIndex = Length - 2; + while (endIndex > index && this[endIndex] == highSurrogate && this[endIndex + 1] == lowSurrogate) { - return Empty; + endIndex -= 2; } - return this[index..(endIndex + 1)]; + return this[index..(endIndex + 2)]; } // Removes a set of characters from the beginning and end of this string. @@ -2497,24 +2486,17 @@ public unsafe string Trim(params ReadOnlySpan trimChars) /// public string TrimStart(Rune trimRune) { - if (Length == 0) + if (trimRune.IsBmp) { - return this; + return TrimStart((char)trimRune.Value); } - // Convert trimRune to span - ReadOnlySpan trimChars = trimRune.AsSpan(stackalloc char[Rune.MaxUtf16CharsPerRune]); + UnicodeUtility.GetUtf16SurrogatesFromSupplementaryPlaneScalar((uint)trimRune.Value, out char highSurrogate, out char lowSurrogate); - // Trim start int index = 0; - while (index < Length && this.AsSpan(index).StartsWith(trimChars)) + while ((uint)(index + 1) < (uint)Length && this[index] == highSurrogate && this[index + 1] == lowSurrogate) { - index += trimChars.Length; - } - - if (index >= Length) - { - return Empty; + index += 2; } return this[index..]; @@ -2571,27 +2553,20 @@ public unsafe string TrimStart(params ReadOnlySpan trimChars) /// public string TrimEnd(Rune trimRune) { - if (Length == 0) + if (trimRune.IsBmp) { - return this; + return TrimEnd((char)trimRune.Value); } - // Convert trimRune to span - ReadOnlySpan trimChars = trimRune.AsSpan(stackalloc char[Rune.MaxUtf16CharsPerRune]); - - // Trim end - int endIndex = Length - 1; - while (endIndex >= 0 && this.AsSpan(..(endIndex + 1)).EndsWith(trimChars)) - { - endIndex -= trimChars.Length; - } + UnicodeUtility.GetUtf16SurrogatesFromSupplementaryPlaneScalar((uint)trimRune.Value, out char highSurrogate, out char lowSurrogate); - if (endIndex < 0) + int endIndex = Length - 2; + while ((uint)endIndex < (uint)Length && this[endIndex] == highSurrogate && this[endIndex + 1] == lowSurrogate) { - return Empty; + endIndex -= 2; } - return this[..(endIndex + 1)]; + return this[..(endIndex + 2)]; } // Removes a set of characters from the end of this string. diff --git a/src/libraries/System.Private.CoreLib/src/System/String.Searching.cs b/src/libraries/System.Private.CoreLib/src/System/String.Searching.cs index f2218eb276603b..3120477f686e01 100644 --- a/src/libraries/System.Private.CoreLib/src/System/String.Searching.cs +++ b/src/libraries/System.Private.CoreLib/src/System/String.Searching.cs @@ -52,6 +52,11 @@ public bool Contains(char value, StringComparison comparisonType) /// if occurs within this string; otherwise, . public bool Contains(Rune value) { + if (value.IsBmp) + { + return Contains((char)value.Value); + } + return Contains(value, StringComparison.Ordinal); } From 9f572a2a3651c23bb2188baac0e200f41b2787a6 Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Fri, 8 May 2026 18:12:20 +0200 Subject: [PATCH 2/2] Move early exit check further up Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../src/System/String.Manipulation.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs b/src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs index 788abe5d311a50..2d4ace7d1bfdda 100644 --- a/src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs +++ b/src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs @@ -1691,10 +1691,14 @@ public string[] Split(Rune separator, int count, StringSplitOptions options = St CheckStringSplitOptions(options); - ReadOnlySpan separatorSpan = separator.AsSpan(stackalloc char[Rune.MaxUtf16CharsPerRune]); - // Ensure matching the string separator overload. - return (count <= 1 || Length == 0) ? CreateSplitArrayOfThisAsSoleValue(options, count) : Split(separatorSpan, count, options); + if (count <= 1 || Length == 0) + { + return CreateSplitArrayOfThisAsSoleValue(options, count); + } + + ReadOnlySpan separatorSpan = separator.AsSpan(stackalloc char[Rune.MaxUtf16CharsPerRune]); + return Split(separatorSpan, count, options); } // Creates an array of strings by splitting this string at each