diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
index 8684386c6e66f5..e84524dc4ff9c3 100644
--- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
+++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
@@ -1217,6 +1217,7 @@
+
diff --git a/src/libraries/System.Private.CoreLib/src/System/Char.cs b/src/libraries/System.Private.CoreLib/src/System/Char.cs
index ae2238d78dd4c1..6b7d11bda27386 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Char.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Char.cs
@@ -129,6 +129,19 @@ public bool Equals(char obj)
return m_value == obj;
}
+ internal bool Equals(char right, StringComparison comparisonType)
+ {
+ switch (comparisonType)
+ {
+ case StringComparison.Ordinal:
+ return Equals(right);
+ default:
+ ReadOnlySpan leftCharsSlice = [this];
+ ReadOnlySpan rightCharsSlice = [right];
+ return leftCharsSlice.Equals(rightCharsSlice, comparisonType);
+ }
+ }
+
// Compares this object to another object, returning an integer that
// indicates the relationship.
// Returns a value less than zero if this object
diff --git a/src/libraries/System.Private.CoreLib/src/System/CodeDom/Compiler/IndentedTextWriter.cs b/src/libraries/System.Private.CoreLib/src/System/CodeDom/Compiler/IndentedTextWriter.cs
index dc7eca31f0d8f7..6fbe34ff62a546 100644
--- a/src/libraries/System.Private.CoreLib/src/System/CodeDom/Compiler/IndentedTextWriter.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/CodeDom/Compiler/IndentedTextWriter.cs
@@ -114,6 +114,16 @@ public override void Write(char value)
_writer.Write(value);
}
+ ///
+ /// Writes out the specified , inserting tabs at the start of every line.
+ ///
+ /// The to write.
+ public override void Write(Rune value)
+ {
+ OutputTabs();
+ _writer.Write(value);
+ }
+
public override void Write(char[]? buffer)
{
OutputTabs();
@@ -197,6 +207,18 @@ public override async Task WriteAsync(char value)
await _writer.WriteAsync(value).ConfigureAwait(false);
}
+ ///
+ /// Asynchronously writes the specified to the underlying , inserting
+ /// tabs at the start of every line.
+ ///
+ /// The to write.
+ /// A representing the asynchronous operation.
+ public override async Task WriteAsync(Rune value)
+ {
+ await OutputTabsAsync().ConfigureAwait(false);
+ await _writer.WriteAsync(value).ConfigureAwait(false);
+ }
+
///
/// Asynchronously writes the specified number of s from the specified buffer
/// to the underlying , starting at the specified index, and outputting tabs at the
@@ -293,6 +315,17 @@ public override void WriteLine(char value)
_tabsPending = true;
}
+ ///
+ /// Writes out the specified , followed by a line terminator, inserting tabs at the start of every line.
+ ///
+ /// The to write.
+ public override void WriteLine(Rune value)
+ {
+ OutputTabs();
+ _writer.WriteLine(value);
+ _tabsPending = true;
+ }
+
public override void WriteLine(char[]? buffer)
{
OutputTabs();
@@ -404,6 +437,19 @@ public override async Task WriteLineAsync(char value)
_tabsPending = true;
}
+ ///
+ /// Asynchronously writes the specified to the underlying followed by a line terminator, inserting tabs
+ /// at the start of every line.
+ ///
+ /// The character to write.
+ /// A representing the asynchronous operation.
+ public override async Task WriteLineAsync(Rune value)
+ {
+ await OutputTabsAsync().ConfigureAwait(false);
+ await _writer.WriteLineAsync(value).ConfigureAwait(false);
+ _tabsPending = true;
+ }
+
///
/// Asynchronously writes the specified number of characters from the specified buffer followed by a line terminator,
/// to the underlying , starting at the specified index within the buffer, inserting tabs at the start of every line.
diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/TextInfo.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/TextInfo.cs
index f95e54ba338d59..83686710940462 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Globalization/TextInfo.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/TextInfo.cs
@@ -180,6 +180,17 @@ public string ToLower(string str)
return ChangeCaseCommon(str);
}
+ internal void ToLower(ReadOnlySpan source, Span destination)
+ {
+ if (GlobalizationMode.Invariant)
+ {
+ InvariantModeCasing.ToLower(source, destination);
+ return;
+ }
+
+ ChangeCaseCommon(source, destination);
+ }
+
private unsafe char ChangeCase(char c, bool toUpper)
{
Debug.Assert(!GlobalizationMode.Invariant);
@@ -451,6 +462,17 @@ public string ToUpper(string str)
return ChangeCaseCommon(str);
}
+ internal void ToUpper(ReadOnlySpan source, Span destination)
+ {
+ if (GlobalizationMode.Invariant)
+ {
+ InvariantModeCasing.ToUpper(source, destination);
+ return;
+ }
+
+ ChangeCaseCommon(source, destination);
+ }
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static char ToUpperAsciiInvariant(char c)
{
@@ -461,6 +483,50 @@ internal static char ToUpperAsciiInvariant(char c)
return c;
}
+ ///
+ /// Converts the specified rune to lowercase.
+ ///
+ /// The rune to convert to lowercase.
+ /// The specified rune converted to lowercase.
+ public Rune ToLower(Rune value)
+ {
+ // Convert rune to span
+ ReadOnlySpan valueChars = value.AsSpan(stackalloc char[Rune.MaxUtf16CharsPerRune]);
+
+ // Change span to lower and convert to rune
+ if (valueChars.Length == 2)
+ {
+ Span lowerChars = stackalloc char[2];
+ ToLower(valueChars, lowerChars);
+ return new Rune(lowerChars[0], lowerChars[1]);
+ }
+
+ char lowerChar = ToLower(valueChars[0]);
+ return new Rune(lowerChar);
+ }
+
+ ///
+ /// Converts the specified rune to uppercase.
+ ///
+ /// The rune to convert to uppercase.
+ /// The specified rune converted to uppercase.
+ public Rune ToUpper(Rune value)
+ {
+ // Convert rune to span
+ ReadOnlySpan valueChars = value.AsSpan(stackalloc char[Rune.MaxUtf16CharsPerRune]);
+
+ // Change span to upper and convert to rune
+ if (valueChars.Length == 2)
+ {
+ Span upperChars = stackalloc char[2];
+ ToUpper(valueChars, upperChars);
+ return new Rune(upperChars[0], upperChars[1]);
+ }
+
+ char upperChar = ToUpper(valueChars[0]);
+ return new Rune(upperChar);
+ }
+
private bool IsAsciiCasingSameAsInvariant
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/TextWriter.CreateBroadcasting.cs b/src/libraries/System.Private.CoreLib/src/System/IO/TextWriter.CreateBroadcasting.cs
index 0637cafb27e985..fc5d3d18aa5681 100644
--- a/src/libraries/System.Private.CoreLib/src/System/IO/TextWriter.CreateBroadcasting.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/IO/TextWriter.CreateBroadcasting.cs
@@ -132,6 +132,14 @@ public override void Write(char value)
}
}
+ public override void Write(Rune value)
+ {
+ foreach (TextWriter writer in _writers)
+ {
+ writer.Write(value);
+ }
+ }
+
public override void Write(char[] buffer, int index, int count)
{
foreach (TextWriter writer in _writers)
@@ -292,6 +300,14 @@ public override void WriteLine(char value)
}
}
+ public override void WriteLine(Rune value)
+ {
+ foreach (TextWriter writer in _writers)
+ {
+ writer.WriteLine(value);
+ }
+ }
+
public override void WriteLine(char[]? buffer)
{
foreach (TextWriter writer in _writers)
@@ -452,6 +468,14 @@ public override async Task WriteAsync(char value)
}
}
+ public override async Task WriteAsync(Rune value)
+ {
+ foreach (TextWriter writer in _writers)
+ {
+ await writer.WriteAsync(value).ConfigureAwait(false);
+ }
+ }
+
public override async Task WriteAsync(string? value)
{
foreach (TextWriter writer in _writers)
@@ -492,6 +516,14 @@ public override async Task WriteLineAsync(char value)
}
}
+ public override async Task WriteLineAsync(Rune value)
+ {
+ foreach (TextWriter writer in _writers)
+ {
+ await writer.WriteLineAsync(value).ConfigureAwait(false);
+ }
+ }
+
public override async Task WriteLineAsync(string? value)
{
foreach (TextWriter writer in _writers)
diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/TextWriter.cs b/src/libraries/System.Private.CoreLib/src/System/IO/TextWriter.cs
index 035664cc815f5e..cc34daa1d34da0 100644
--- a/src/libraries/System.Private.CoreLib/src/System/IO/TextWriter.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/IO/TextWriter.cs
@@ -125,6 +125,23 @@ public virtual void Write(char value)
{
}
+ ///
+ /// Writes a rune to the text stream.
+ ///
+ /// The rune to write to the text stream.
+ public virtual void Write(Rune value)
+ {
+ // Convert value to span
+ ReadOnlySpan valueChars = value.AsSpan(stackalloc char[Rune.MaxUtf16CharsPerRune]);
+
+ // Write span
+ Write(valueChars[0]);
+ if (valueChars.Length > 1)
+ {
+ Write(valueChars[1]);
+ }
+ }
+
// Writes a character array to the text stream. This default method calls
// Write(char) for each of the characters in the character array.
// If the character array is null, nothing is written.
@@ -343,6 +360,26 @@ public virtual void WriteLine(char value)
WriteLine();
}
+ ///
+ /// Writes a rune followed by a line terminator to the text stream.
+ ///
+ /// The rune to write to the text stream.
+ public virtual void WriteLine(Rune value)
+ {
+ // Convert value to span
+ ReadOnlySpan valueChars = value.AsSpan(stackalloc char[Rune.MaxUtf16CharsPerRune]);
+
+ if (valueChars.Length > 1)
+ {
+ Write(valueChars[0]);
+ WriteLine(valueChars[1]);
+ }
+ else
+ {
+ WriteLine(valueChars[0]);
+ }
+ }
+
// Writes an array of characters followed by a line terminator to the text
// stream.
//
@@ -542,6 +579,28 @@ public virtual Task WriteAsync(char value) =>
t.Item1.Write(t.Item2);
}, new TupleSlim(this, value), CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
+ ///
+ /// Writes a rune to the text stream asynchronously.
+ ///
+ /// The rune to write to the text stream.
+ /// A task that represents the asynchronous write operation.
+ public virtual Task WriteAsync(Rune value)
+ {
+ ReadOnlySpan valueChars = value.AsSpan(stackalloc char[Rune.MaxUtf16CharsPerRune]);
+
+ if (valueChars.Length > 1)
+ {
+ return Task.Factory.StartNew(static state =>
+ {
+ var t = (TupleSlim)state!;
+ t.Item1.Write(t.Item2);
+ t.Item1.Write(t.Item3);
+ }, new TupleSlim(this, valueChars[0], valueChars[1]), CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
+ }
+
+ return WriteAsync(valueChars[0]);
+ }
+
public virtual Task WriteAsync(string? value) =>
Task.Factory.StartNew(static state =>
{
@@ -605,6 +664,28 @@ public virtual Task WriteLineAsync(char value) =>
t.Item1.WriteLine(t.Item2);
}, new TupleSlim(this, value), CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
+ ///
+ /// Writes a rune followed by a line terminator to the text stream asynchronously.
+ ///
+ /// The rune to write to the text stream.
+ /// A task that represents the asynchronous write operation.
+ public virtual Task WriteLineAsync(Rune value)
+ {
+ ReadOnlySpan valueChars = value.AsSpan(stackalloc char[Rune.MaxUtf16CharsPerRune]);
+
+ if (valueChars.Length > 1)
+ {
+ return Task.Factory.StartNew(static state =>
+ {
+ var t = (TupleSlim)state!;
+ t.Item1.Write(t.Item2);
+ t.Item1.WriteLine(t.Item3);
+ }, new TupleSlim(this, valueChars[0], valueChars[1]), CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
+ }
+
+ return WriteLineAsync(valueChars[0]);
+ }
+
public virtual Task WriteLineAsync(string? value) =>
Task.Factory.StartNew(static state =>
{
@@ -702,6 +783,7 @@ public override void Flush() { }
public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public override void Write(char value) { }
+ public override void Write(Rune value) { }
public override void Write(char[]? buffer) { }
public override void Write(char[] buffer, int index, int count) { }
public override void Write(ReadOnlySpan buffer) { }
@@ -722,12 +804,14 @@ public override void Write([StringSyntax(StringSyntaxAttribute.CompositeFormat)]
public override void Write([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, params object?[] arg) { }
public override void Write([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, params ReadOnlySpan
+ /// The Unicode character to be replaced.
+ /// The Unicode character to replace all occurrences of .
+ ///
+ /// A string that is equivalent to this instance except that all instances of are replaced with .
+ /// If is not found in the current instance, the method returns the current instance unchanged.
+ ///
+ public string Replace(Rune oldRune, Rune newRune)
+ {
+ if (Length == 0)
+ {
+ return this;
+ }
+
+ ReadOnlySpan oldChars = oldRune.AsSpan(stackalloc char[Rune.MaxUtf16CharsPerRune]);
+ ReadOnlySpan newChars = newRune.AsSpan(stackalloc char[Rune.MaxUtf16CharsPerRune]);
+
+ return ReplaceCore(this, oldChars, newChars, CompareInfo.Invariant, CompareOptions.Ordinal)
+ ?? this;
+ }
+
///
/// Replaces all newline sequences in the current string with .
///
@@ -1643,6 +1666,41 @@ public string[] Split(char separator, int count, StringSplitOptions options = St
return SplitInternal(new ReadOnlySpan(in separator), count, options);
}
+ ///
+ /// Splits a string into substrings based on a specified delimiting rune and, optionally, options.
+ ///
+ /// A character that delimits the substrings in this string.
+ /// A bitwise combination of the enumeration values that specifies whether to trim substrings and include empty substrings.
+ /// An array whose elements contain the substrings from this instance that are delimited by .
+ public string[] Split(Rune separator, StringSplitOptions options = StringSplitOptions.None)
+ {
+ return Split(separator, int.MaxValue, options);
+ }
+
+ ///
+ /// Splits a string into a maximum number of substrings based on the provided rune separator, optionally omitting empty substrings from the result.
+ ///
+ /// A character that delimits the substrings in this string.
+ /// The maximum number of elements expected in the array.
+ /// A bitwise combination of the enumeration values that specifies whether to trim substrings and include empty substrings.
+ /// 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)
+ {
+ return Split(separatorSpan[0], count, options);
+ }
+
+ ArgumentOutOfRangeException.ThrowIfNegative(count);
+
+ CheckStringSplitOptions(options);
+
+ // Ensure matching the string separator overload.
+ return (count <= 1 || Length == 0) ? CreateSplitArrayOfThisAsSoleValue(options, count) : Split(separatorSpan, count, options);
+ }
+
// Creates an array of strings by splitting this string at each
// occurrence of a separator. The separator is searched for, and if found,
// the substring preceding the occurrence is stored as the first element in
@@ -1836,6 +1894,9 @@ private string[] CreateSplitArrayOfThisAsSoleValue(StringSplitOptions options, i
}
private string[] SplitInternal(string separator, int count, StringSplitOptions options)
+ => Split(separator.AsSpan(), count, options);
+
+ private string[] Split(ReadOnlySpan separator, int count, StringSplitOptions options)
{
var sepListBuilder = new ValueListBuilder(stackalloc int[StackallocIntBufferSizeLimit]);
@@ -2344,6 +2405,51 @@ public unsafe string Trim(char trimChar)
return TrimHelper(&trimChar, 1, TrimType.Both);
}
+ ///
+ /// Removes all leading and trailing instances of a rune from the current string.
+ ///
+ /// A Unicode rune to remove.
+ ///
+ /// The string that remains after all instances of the rune are removed from the start and end of the
+ /// current string. If no runes can be trimmed from the current instance, the method returns the current instance unchanged.
+ ///
+ public string Trim(Rune trimRune)
+ {
+ if (Length == 0)
+ {
+ return this;
+ }
+
+ // Convert trimRune to span
+ ReadOnlySpan trimChars = trimRune.AsSpan(stackalloc char[Rune.MaxUtf16CharsPerRune]);
+
+ // Trim start
+ int index = 0;
+ while (index < Length && this.AsSpan(index).StartsWith(trimChars))
+ {
+ index += trimChars.Length;
+ }
+
+ if (index >= Length)
+ {
+ return Empty;
+ }
+
+ // Trim end
+ int endIndex = Length - 1;
+ while (endIndex >= index && this.AsSpan(index..(endIndex + 1)).EndsWith(trimChars))
+ {
+ endIndex -= trimChars.Length;
+ }
+
+ if (endIndex < index)
+ {
+ return Empty;
+ }
+
+ return this[index..(endIndex + 1)];
+ }
+
// Removes a set of characters from the beginning and end of this string.
public unsafe string Trim(params char[]? trimChars)
{
@@ -2385,6 +2491,39 @@ public unsafe string Trim(params ReadOnlySpan trimChars)
// Removes a set of characters from the beginning of this string.
public unsafe string TrimStart(char trimChar) => TrimHelper(&trimChar, 1, TrimType.Head);
+ ///
+ /// Removes all leading instances of a rune from the current string.
+ ///
+ /// A Unicode rune to remove.
+ ///
+ /// The string that remains after all instances of the rune are removed from the start of the
+ /// current string. If no runes can be trimmed from the current instance, the method returns the current instance unchanged.
+ ///
+ public string TrimStart(Rune trimRune)
+ {
+ if (Length == 0)
+ {
+ return this;
+ }
+
+ // Convert trimRune to span
+ ReadOnlySpan trimChars = trimRune.AsSpan(stackalloc char[Rune.MaxUtf16CharsPerRune]);
+
+ // Trim start
+ int index = 0;
+ while (index < Length && this.AsSpan(index).StartsWith(trimChars))
+ {
+ index += trimChars.Length;
+ }
+
+ if (index >= Length)
+ {
+ return Empty;
+ }
+
+ return this[index..];
+ }
+
// Removes a set of characters from the beginning of this string.
public unsafe string TrimStart(params char[]? trimChars)
{
@@ -2426,6 +2565,39 @@ public unsafe string TrimStart(params ReadOnlySpan trimChars)
// Removes a set of characters from the end of this string.
public unsafe string TrimEnd(char trimChar) => TrimHelper(&trimChar, 1, TrimType.Tail);
+ ///
+ /// Removes all trailing instances of a rune from the current string.
+ ///
+ /// A Unicode rune to remove.
+ ///
+ /// The string that remains after all instances of the rune are removed from the end of the
+ /// current string. If no runes can be trimmed from the current instance, the method returns the current instance unchanged.
+ ///
+ public string TrimEnd(Rune trimRune)
+ {
+ if (Length == 0)
+ {
+ return this;
+ }
+
+ // 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;
+ }
+
+ if (endIndex < 0)
+ {
+ return Empty;
+ }
+
+ return this[..(endIndex + 1)];
+ }
+
// Removes a set of characters from the end of this string.
public unsafe string TrimEnd(params char[]? trimChars)
{
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 addc7386039357..11e1e950f9d973 100644
--- a/src/libraries/System.Private.CoreLib/src/System/String.Searching.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/String.Searching.cs
@@ -1,8 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Buffers;
using System.Globalization;
using System.Runtime.CompilerServices;
+using System.Text;
namespace System
{
@@ -39,10 +41,31 @@ public bool Contains(char value)
public bool Contains(char value, StringComparison comparisonType)
{
#pragma warning disable CA2249 // Consider using 'string.Contains' instead of 'string.IndexOf'... this is the implementation of Contains!
- return IndexOf(value, comparisonType) != -1;
+ return IndexOf(value, comparisonType) >= 0;
#pragma warning restore CA2249
}
+ ///
+ /// Returns a value indicating whether a specified rune occurs within this string using an ordinal comparison.
+ ///
+ /// The rune to seek.
+ /// if occurs within this string; otherwise, .
+ public bool Contains(Rune value)
+ {
+ return Contains(value, StringComparison.Ordinal);
+ }
+
+ ///
+ /// Returns a value indicating whether a specified rune occurs within this string using the specified comparison option.
+ ///
+ /// The rune to seek.
+ /// One of the enumeration values that specifies the rules to use in the comparison.
+ /// if occurs within this string; otherwise, .
+ public bool Contains(Rune value, StringComparison comparisonType)
+ {
+ return IndexOf(value, comparisonType) >= 0;
+ }
+
// Returns the index of the first occurrence of a specified character in the current instance.
// The search starts at startIndex and runs thorough the next count characters.
public int IndexOf(char value) => SpanHelpers.IndexOfChar(ref _firstChar, value, Length);
@@ -262,6 +285,109 @@ public int IndexOf(string value, int startIndex, int count, StringComparison com
};
}
+ ///
+ /// Reports the zero-based index of the first occurrence of the specified rune in the current String object.
+ ///
+ /// The rune to seek.
+ ///
+ /// The zero-based index position of from the start of the current instance
+ /// if that rune is found, or -1 if it is not.
+ ///
+ public int IndexOf(Rune value)
+ {
+ return IndexOf(value, StringComparison.Ordinal);
+ }
+
+ ///
+ /// Reports the zero-based index of the first occurrence of the specified rune in the current String object.
+ /// A parameter specifies the starting search position in the current string.
+ ///
+ /// The rune to seek.
+ /// The search starting position.
+ ///
+ /// The zero-based index position of from the start of the current instance
+ /// if that rune is found, or -1 if it is not.
+ ///
+ public int IndexOf(Rune value, int startIndex)
+ {
+ return IndexOf(value, startIndex, StringComparison.Ordinal);
+ }
+
+ ///
+ /// Reports the zero-based index of the first occurrence of the specified rune in the current String object.
+ /// Parameters specify the starting search position in the current string and the number of characters in the
+ /// current string to search.
+ ///
+ /// The rune to seek.
+ /// The search starting position.
+ /// The number of character positions to examine.
+ ///
+ /// The zero-based index position of from the start of the current instance
+ /// if that rune is found, or -1 if it is not.
+ ///
+ public int IndexOf(Rune value, int startIndex, int count)
+ {
+ return IndexOf(value, startIndex, count, StringComparison.Ordinal);
+ }
+
+ ///
+ /// Reports the zero-based index of the first occurrence of the specified rune in the current String object.
+ /// A parameter specifies the type of search to use for the specified rune.
+ ///
+ /// The rune to seek.
+ /// One of the enumeration values that specifies the rules for the search.
+ ///
+ /// The zero-based index position of from the start of the current instance
+ /// if that rune is found, or -1 if it is not.
+ ///
+ public int IndexOf(Rune value, StringComparison comparisonType)
+ {
+ return IndexOf(value, 0, comparisonType);
+ }
+
+ ///
+ /// Reports the zero-based index of the first occurrence of the specified rune in the current String object.
+ /// Parameters specify the starting search position in the current string and the type of search to use for
+ /// the specified rune.
+ ///
+ /// The rune to seek.
+ /// The search starting position.
+ /// One of the enumeration values that specifies the rules for the search.
+ ///
+ /// The zero-based index position of from the start of the current instance
+ /// if that rune is found, or -1 if it is not.
+ ///
+ internal int IndexOf(Rune value, int startIndex, StringComparison comparisonType)
+ {
+ return IndexOf(value, startIndex, Length - startIndex, comparisonType);
+ }
+
+ ///
+ /// Reports the zero-based index of the first occurrence of the specified rune in the current String object.
+ /// Parameters specify the starting search position in the current string, the number of characters in the
+ /// current string to search, and the type of search to use for the specified rune.
+ ///
+ /// The rune to seek.
+ /// The search starting position.
+ /// The number of character positions to examine.
+ /// One of the enumeration values that specifies the rules for the search.
+ ///
+ /// The zero-based index position of from the start of the current instance
+ /// if that rune is found, or -1 if it is not.
+ ///
+ internal int IndexOf(Rune value, int startIndex, int count, StringComparison comparisonType)
+ {
+ ArgumentOutOfRangeException.ThrowIfLessThan(startIndex, 0);
+ ArgumentOutOfRangeException.ThrowIfLessThan(count, 0);
+ ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex + count, Length);
+
+ // Convert value to span
+ ReadOnlySpan valueChars = value.AsSpan(stackalloc char[Rune.MaxUtf16CharsPerRune]);
+
+ int subIndex = this.AsSpan(startIndex..(startIndex + count)).IndexOf(valueChars, comparisonType);
+ return subIndex < 0 ? subIndex : startIndex + subIndex;
+ }
+
// Returns the index of the last occurrence of a specified character in the current instance.
// The search starts at startIndex and runs backwards to startIndex - count + 1.
// The character at position startIndex is included in the search. startIndex is the larger
@@ -386,5 +512,111 @@ public int LastIndexOf(string value, int startIndex, int count, StringComparison
_ => throw (value is null ? new ArgumentNullException(nameof(value)) : new ArgumentException(SR.NotSupported_StringComparison, nameof(comparisonType))),
};
}
+
+ ///
+ /// Reports the zero-based index of the last occurrence of the specified rune in the current String object.
+ ///
+ /// The rune to seek.
+ ///
+ /// The zero-based index position of from the end of the current instance
+ /// if that rune is found, or -1 if it is not.
+ ///
+ public int LastIndexOf(Rune value)
+ {
+ return LastIndexOf(value, StringComparison.Ordinal);
+ }
+
+ ///
+ /// Reports the zero-based index of the last occurrence of the specified rune in the current String object.
+ /// A parameter specifies the starting search position in the current string.
+ ///
+ /// The rune to seek.
+ /// The search starting position. The search proceeds from toward the beginning of this instance.
+ ///
+ /// The zero-based index position of from the end of the current instance
+ /// if that rune is found, or -1 if it is not.
+ ///
+ public int LastIndexOf(Rune value, int startIndex)
+ {
+ return LastIndexOf(value, startIndex, StringComparison.Ordinal);
+ }
+
+ ///
+ /// Reports the zero-based index of the last occurrence of the specified rune in the current String object.
+ /// Parameters specify the starting search position in the current string and the number of characters in the
+ /// current string to search.
+ ///
+ /// The rune to seek.
+ /// The search starting position. The search proceeds from toward the beginning of this instance.
+ /// The number of character positions to examine.
+ ///
+ /// The zero-based index position of from the end of the current instance
+ /// if that rune is found, or -1 if it is not.
+ ///
+ public int LastIndexOf(Rune value, int startIndex, int count)
+ {
+ return LastIndexOf(value, startIndex, count, StringComparison.Ordinal);
+ }
+
+ ///
+ /// Reports the zero-based index of the last occurrence of the specified rune in the current String object.
+ /// A parameter specifies the type of search to use for the specified rune.
+ ///
+ /// The rune to seek.
+ /// One of the enumeration values that specifies the rules for the search.
+ ///
+ /// The zero-based index position of from the end of the current instance
+ /// if that rune is found, or -1 if it is not.
+ ///
+ public int LastIndexOf(Rune value, StringComparison comparisonType)
+ {
+ return LastIndexOf(value, Length - 1, comparisonType);
+ }
+
+ ///
+ /// Reports the zero-based index of the last occurrence of the specified rune in the current String object.
+ /// Parameters specify the starting search position in the current string and the type of search to use for
+ /// the specified rune.
+ ///
+ /// The rune to seek.
+ /// The search starting position. The search proceeds from toward the beginning of this instance.
+ /// One of the enumeration values that specifies the rules for the search.
+ ///
+ /// The zero-based index position of from the end of the current instance
+ /// if that rune is found, or -1 if it is not.
+ ///
+ internal int LastIndexOf(Rune value, int startIndex, StringComparison comparisonType)
+ {
+ return LastIndexOf(value, startIndex, startIndex + 1, comparisonType);
+ }
+
+ ///
+ /// Reports the zero-based index of the last occurrence of the specified rune in the current String object.
+ /// Parameters specify the starting search position in the current string, the number of characters in the
+ /// current string to search, and the type of search to use for the specified rune.
+ ///
+ /// The rune to seek.
+ /// The search starting position. The search proceeds from toward the beginning of this instance.
+ /// The number of character positions to examine.
+ /// One of the enumeration values that specifies the rules for the search.
+ ///
+ /// The zero-based index position of from the end of the current instance
+ /// if that rune is found, or -1 if it is not.
+ ///
+ internal int LastIndexOf(Rune value, int startIndex, int count, StringComparison comparisonType)
+ {
+ ArgumentOutOfRangeException.ThrowIfLessThan(startIndex, 0);
+ ArgumentOutOfRangeException.ThrowIfLessThan(count, 0);
+ ArgumentOutOfRangeException.ThrowIfLessThan(startIndex - count + 1, 0);
+ ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, Length - 1);
+
+ // Convert value to span
+ ReadOnlySpan valueChars = value.AsSpan(stackalloc char[Rune.MaxUtf16CharsPerRune]);
+
+ int startIndexFromZero = startIndex - count + 1;
+
+ int subIndex = this.AsSpan(startIndexFromZero..(startIndexFromZero + count)).LastIndexOf(valueChars, comparisonType);
+ return subIndex < 0 ? subIndex : startIndexFromZero + subIndex;
+ }
}
}
diff --git a/src/libraries/System.Private.CoreLib/src/System/Text/Rune.cs b/src/libraries/System.Private.CoreLib/src/System/Text/Rune.cs
index 2f0c00c1b4a55a..05c15e3fa841d1 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Text/Rune.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Text/Rune.cs
@@ -294,6 +294,13 @@ private static Rune ChangeCaseCultureAware(Rune rune, CultureInfo culture, bool
public int CompareTo(Rune other) => this.Value - other.Value; // values don't span entire 32-bit domain; won't integer overflow
+ internal ReadOnlySpan AsSpan(Span buffer)
+ {
+ Debug.Assert(buffer.Length >= MaxUtf16CharsPerRune);
+ int charsWritten = EncodeToUtf16(buffer);
+ return buffer.Slice(0, charsWritten);
+ }
+
///
/// Decodes the at the beginning of the provided UTF-16 source buffer.
///
@@ -780,6 +787,29 @@ public int EncodeToUtf8(Span destination)
public bool Equals(Rune other) => this == other;
+ ///
+ /// Returns a value that indicates whether the current instance and a specified rune are equal using the specified comparison option.
+ ///
+ /// The rune to compare with the current instance.
+ /// One of the enumeration values that specifies the rules to use in the comparison.
+ /// if the current instance and are equal; otherwise, .
+ public bool Equals(Rune other, StringComparison comparisonType)
+ {
+ if (comparisonType is StringComparison.Ordinal)
+ {
+ return this == other;
+ }
+
+ // Convert this to span
+ ReadOnlySpan thisChars = AsSpan(stackalloc char[MaxUtf16CharsPerRune]);
+
+ // Convert other to span
+ ReadOnlySpan otherChars = other.AsSpan(stackalloc char[MaxUtf16CharsPerRune]);
+
+ // Compare span equality
+ return thisChars.Equals(otherChars, comparisonType);
+ }
+
public override int GetHashCode() => Value;
#if SYSTEM_PRIVATE_CORELIB
diff --git a/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilder.cs b/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilder.cs
index a159895c0ed721..0dcc60a825f986 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilder.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilder.cs
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Buffers;
+using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
@@ -645,6 +647,14 @@ public ManyChunkInfo(StringBuilder? stringBuilder, int chunkCount)
#endregion
}
+ ///
+ /// Returns an enumeration of from this builder.
+ ///
+ ///
+ /// Invalid sequences will be represented in the enumeration by .
+ ///
+ public StringBuilderRuneEnumerator EnumerateRunes() => new StringBuilderRuneEnumerator(this);
+
///
/// Appends a character 0 or more times to the end of this builder.
///
@@ -1027,6 +1037,20 @@ private void AppendWithExpansion(char value)
m_ChunkLength++;
}
+ ///
+ /// Appends the string representation of a specified to this instance.
+ ///
+ /// The UTF-32-encoded code unit to append.
+ /// A reference to this instance after the append operation has completed.
+ public StringBuilder Append(Rune value)
+ {
+ // Convert value to span
+ ReadOnlySpan valueChars = value.AsSpan(stackalloc char[Rune.MaxUtf16CharsPerRune]);
+
+ // Append span
+ return Append(valueChars);
+ }
+
[CLSCompliant(false)]
public StringBuilder Append(sbyte value) => AppendSpanFormattable(value);
@@ -1327,6 +1351,21 @@ public StringBuilder Insert(int index, char value)
return this;
}
+ ///
+ /// Inserts the string representation of a specified Unicode rune into this instance at the specified character position.
+ ///
+ /// The position in this instance where insertion begins.
+ /// The value to insert.
+ /// A reference to this instance after the insert operation has completed.
+ public StringBuilder Insert(int index, Rune value)
+ {
+ // Convert value to span
+ ReadOnlySpan valueChars = value.AsSpan(stackalloc char[Rune.MaxUtf16CharsPerRune]);
+
+ // Insert span
+ return Insert(index, valueChars);
+ }
+
public StringBuilder Insert(int index, char[]? value)
{
if ((uint)index > (uint)Length)
@@ -2248,6 +2287,40 @@ public StringBuilder Replace(char oldChar, char newChar, int startIndex, int cou
return this;
}
+ ///
+ /// Replaces all occurrences of a specified rune in this instance with another specified rune using an ordinal comparison.
+ ///
+ /// The rune to replace.
+ /// The rune that replaces .
+ /// A reference to this instance with replaced by .
+ public StringBuilder Replace(Rune oldRune, Rune newRune)
+ {
+ return Replace(oldRune, newRune, 0, Length);
+ }
+
+ ///
+ /// Replaces, within a substring of this instance, all occurrences of a specified rune with another specified rune using an ordinal comparison.
+ ///
+ /// The rune to replace.
+ /// The rune that replaces .
+ /// The position in this instance where the substring begins.
+ /// The length of the substring.
+ ///
+ /// A reference to this instance with replaced by in the range
+ /// from to + - 1.
+ ///
+ public StringBuilder Replace(Rune oldRune, Rune newRune, int startIndex, int count)
+ {
+ // Convert oldRune to span
+ ReadOnlySpan oldChars = oldRune.AsSpan(stackalloc char[Rune.MaxUtf16CharsPerRune]);
+
+ // Convert newRune to span
+ ReadOnlySpan newChars = newRune.AsSpan(stackalloc char[Rune.MaxUtf16CharsPerRune]);
+
+ // Replace span with span
+ return Replace(oldChars, newChars, startIndex, count);
+ }
+
///
/// Appends a character buffer to this builder.
///
@@ -2798,6 +2871,54 @@ private void Remove(int startIndex, int count, out StringBuilder chunk, out int
AssertInvariants();
}
+ ///
+ /// Gets the that begins at a specified position in this builder.
+ ///
+ /// The starting position in this builder at which to decode the rune.
+ /// The rune obtained from this builder at the specified .
+ /// The index is out of the range of the builder.
+ /// The rune at the specified index is not valid.
+ public Rune GetRuneAt(int index)
+ {
+ if (TryGetRuneAt(index, out Rune value))
+ {
+ return value;
+ }
+ ThrowHelper.ThrowArgumentException_CannotExtractScalar(ExceptionArgument.index);
+ return default;
+ }
+
+ ///
+ /// Attempts to get the that begins at a specified position in this builder, and return a value that indicates whether the operation succeeded.
+ ///
+ /// The starting position in this builder at which to decode the rune.
+ /// When this method returns, the decoded rune.
+ ///
+ /// if a scalar value was successfully extracted from the specified index;
+ /// if a value could not be extracted because of invalid data.
+ ///
+ /// The index is out of the range of the builder.
+ public bool TryGetRuneAt(int index, out Rune value)
+ {
+ ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, Length);
+ ArgumentOutOfRangeException.ThrowIfNegative(index);
+
+ // Get span at StringBuilder index
+ Span chars = index + 1 < Length
+ ? [this[index], this[index + 1]]
+ : [this[index]];
+
+ OperationStatus status = Rune.DecodeFromUtf16(chars, out Rune result, out _);
+ if (status is OperationStatus.Done)
+ {
+ value = result;
+ return true;
+ }
+
+ value = default;
+ return false;
+ }
+
/// Provides a handler used by the language compiler to append interpolated strings into instances.
[EditorBrowsable(EditorBrowsableState.Never)]
[InterpolatedStringHandler]
diff --git a/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilderRuneEnumerator.cs b/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilderRuneEnumerator.cs
new file mode 100644
index 00000000000000..caa0f717d54d3a
--- /dev/null
+++ b/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilderRuneEnumerator.cs
@@ -0,0 +1,105 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections;
+using System.Collections.Generic;
+
+namespace System.Text
+{
+ ///
+ /// An enumerator for retrieving instances from a .
+ ///
+ public struct StringBuilderRuneEnumerator : IEnumerable, IEnumerator
+ {
+ private readonly StringBuilder _stringBuilder;
+ private Rune _current;
+ private int _nextIndex;
+
+ internal StringBuilderRuneEnumerator(StringBuilder value)
+ {
+ _stringBuilder = value;
+ _current = default;
+ _nextIndex = 0;
+ }
+
+ ///
+ /// Gets the at the current position of the enumerator.
+ ///
+ public readonly Rune Current => _current;
+
+ ///
+ /// Returns the current enumerator instance.
+ ///
+ /// The current enumerator instance.
+ public readonly StringBuilderRuneEnumerator GetEnumerator() => this;
+
+ ///
+ /// Advances the enumerator to the next of the builder.
+ ///
+ ///
+ /// if the enumerator successfully advanced to the next item;
+ /// if the end of the builder has been reached.
+ ///
+ public bool MoveNext()
+ {
+ if ((uint)_nextIndex >= _stringBuilder.Length)
+ {
+ // reached the end of the string
+ _current = default;
+ return false;
+ }
+
+ if (!_stringBuilder.TryGetRuneAt(_nextIndex, out _current))
+ {
+ // replace invalid sequences with U+FFFD
+ _current = Rune.ReplacementChar;
+ }
+
+ // In UTF-16 specifically, invalid sequences always have length 1, which is the same
+ // length as the replacement character U+FFFD. This means that we can always bump the
+ // next index by the current scalar's UTF-16 sequence length. This optimization is not
+ // generally applicable; for example, enumerating scalars from UTF-8 cannot utilize
+ // this same trick.
+
+ _nextIndex += _current.Utf16SequenceLength;
+ return true;
+ }
+
+ ///
+ /// Gets the at the current position of the enumerator.
+ ///
+ readonly object? IEnumerator.Current => _current;
+
+ ///
+ /// Releases all resources used by the current instance.
+ ///
+ ///
+ /// This method performs no operation and produces no side effects.
+ ///
+ readonly void IDisposable.Dispose()
+ {
+ // no-op
+ }
+
+ ///
+ /// Returns the current enumerator instance.
+ ///
+ /// The current enumerator instance.
+ readonly IEnumerator IEnumerable.GetEnumerator() => this;
+
+ ///
+ /// Returns the current enumerator instance.
+ ///
+ /// The current enumerator instance.
+ readonly IEnumerator IEnumerable.GetEnumerator() => this;
+
+ ///
+ /// Resets the current instance to the beginning of the builder.
+ ///
+ void IEnumerator.Reset()
+ {
+ _current = default;
+ _nextIndex = 0;
+ }
+ }
+}
diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs
index 0807ee840135d4..9922faac9a38a3 100644
--- a/src/libraries/System.Runtime/ref/System.Runtime.cs
+++ b/src/libraries/System.Runtime/ref/System.Runtime.cs
@@ -5654,6 +5654,8 @@ public unsafe String(sbyte* value, int startIndex, int length, System.Text.Encod
public static string Concat(System.Collections.Generic.IEnumerable values) { throw null; }
public bool Contains(char value) { throw null; }
public bool Contains(char value, System.StringComparison comparisonType) { throw null; }
+ public bool Contains(System.Text.Rune value) { throw null; }
+ public bool Contains(System.Text.Rune value, System.StringComparison comparisonType) { throw null; }
public bool Contains(string value) { throw null; }
public bool Contains(string value, System.StringComparison comparisonType) { throw null; }
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
@@ -5665,6 +5667,9 @@ public void CopyTo(System.Span destination) { }
public static string Create(System.IFormatProvider? provider, System.Span initialBuffer, [System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute(new string[]{ "provider", "initialBuffer"})] ref System.Runtime.CompilerServices.DefaultInterpolatedStringHandler handler) { throw null; }
public static string Create(int length, TState state, System.Buffers.SpanAction action) where TState : allows ref struct { throw null; }
public bool EndsWith(char value) { throw null; }
+ public bool EndsWith(char value, System.StringComparison comparisonType) { throw null; }
+ public bool EndsWith(System.Text.Rune value) { throw null; }
+ public bool EndsWith(System.Text.Rune value, System.StringComparison comparisonType) { throw null; }
public bool EndsWith(string value) { throw null; }
public bool EndsWith(string value, bool ignoreCase, System.Globalization.CultureInfo? culture) { throw null; }
public bool EndsWith(string value, System.StringComparison comparisonType) { throw null; }
@@ -5707,6 +5712,10 @@ public void CopyTo(System.Span destination) { }
public int IndexOf(string value, int startIndex, int count, System.StringComparison comparisonType) { throw null; }
public int IndexOf(string value, int startIndex, System.StringComparison comparisonType) { throw null; }
public int IndexOf(string value, System.StringComparison comparisonType) { throw null; }
+ public int IndexOf(System.Text.Rune value) { throw null; }
+ public int IndexOf(System.Text.Rune value, int startIndex) { throw null; }
+ public int IndexOf(System.Text.Rune value, int startIndex, int count) { throw null; }
+ public int IndexOf(System.Text.Rune value, System.StringComparison comparisonType) { throw null; }
public int IndexOfAny(char[] anyOf) { throw null; }
public int IndexOfAny(char[] anyOf, int startIndex) { throw null; }
public int IndexOfAny(char[] anyOf, int startIndex, int count) { throw null; }
@@ -5739,6 +5748,10 @@ public void CopyTo(System.Span destination) { }
public int LastIndexOf(string value, int startIndex, int count, System.StringComparison comparisonType) { throw null; }
public int LastIndexOf(string value, int startIndex, System.StringComparison comparisonType) { throw null; }
public int LastIndexOf(string value, System.StringComparison comparisonType) { throw null; }
+ public int LastIndexOf(System.Text.Rune value) { throw null; }
+ public int LastIndexOf(System.Text.Rune value, int startIndex) { throw null; }
+ public int LastIndexOf(System.Text.Rune value, int startIndex, int count) { throw null; }
+ public int LastIndexOf(System.Text.Rune value, System.StringComparison comparisonType) { throw null; }
public int LastIndexOfAny(char[] anyOf) { throw null; }
public int LastIndexOfAny(char[] anyOf, int startIndex) { throw null; }
public int LastIndexOfAny(char[] anyOf, int startIndex, int count) { throw null; }
@@ -5754,6 +5767,7 @@ public void CopyTo(System.Span destination) { }
public string Remove(int startIndex) { throw null; }
public string Remove(int startIndex, int count) { throw null; }
public string Replace(char oldChar, char newChar) { throw null; }
+ public string Replace(System.Text.Rune oldRune, System.Text.Rune newRune) { throw null; }
public string Replace(string oldValue, string? newValue) { throw null; }
public string Replace(string oldValue, string? newValue, bool ignoreCase, System.Globalization.CultureInfo? culture) { throw null; }
public string Replace(string oldValue, string? newValue, System.StringComparison comparisonType) { throw null; }
@@ -5761,6 +5775,8 @@ public void CopyTo(System.Span destination) { }
public string ReplaceLineEndings(string replacementText) { throw null; }
public string[] Split(char separator, int count, System.StringSplitOptions options = System.StringSplitOptions.None) { throw null; }
public string[] Split(char separator, System.StringSplitOptions options = System.StringSplitOptions.None) { throw null; }
+ public string[] Split(System.Text.Rune separator, int count, System.StringSplitOptions options = System.StringSplitOptions.None) { throw null; }
+ public string[] Split(System.Text.Rune separator, System.StringSplitOptions options = System.StringSplitOptions.None) { throw null; }
public string[] Split(params char[]? separator) { throw null; }
public string[] Split(params System.ReadOnlySpan separator) { throw null; }
public string[] Split(char[]? separator, int count) { throw null; }
@@ -5771,6 +5787,9 @@ public void CopyTo(System.Span destination) { }
public string[] Split(string[]? separator, int count, System.StringSplitOptions options) { throw null; }
public string[] Split(string[]? separator, System.StringSplitOptions options) { throw null; }
public bool StartsWith(char value) { throw null; }
+ public bool StartsWith(char value, System.StringComparison comparisonType) { throw null; }
+ public bool StartsWith(System.Text.Rune value) { throw null; }
+ public bool StartsWith(System.Text.Rune value, System.StringComparison comparisonType) { throw null; }
public bool StartsWith(string value) { throw null; }
public bool StartsWith(string value, bool ignoreCase, System.Globalization.CultureInfo? culture) { throw null; }
public bool StartsWith(string value, System.StringComparison comparisonType) { throw null; }
@@ -5809,12 +5828,15 @@ public void CopyTo(System.Span destination) { }
public string ToUpperInvariant() { throw null; }
public string Trim() { throw null; }
public string Trim(char trimChar) { throw null; }
+ public string Trim(System.Text.Rune trimRune) { throw null; }
public string Trim(params char[]? trimChars) { throw null; }
public string TrimEnd() { throw null; }
public string TrimEnd(char trimChar) { throw null; }
+ public string TrimEnd(System.Text.Rune trimRune) { throw null; }
public string TrimEnd(params char[]? trimChars) { throw null; }
public string TrimStart() { throw null; }
public string TrimStart(char trimChar) { throw null; }
+ public string TrimStart(System.Text.Rune trimRune) { throw null; }
public string TrimStart(params char[]? trimChars) { throw null; }
public bool TryCopyTo(System.Span destination) { throw null; }
}
@@ -9896,6 +9918,8 @@ void System.Runtime.Serialization.IDeserializationCallback.OnDeserialization(obj
public string ToTitleCase(string str) { throw null; }
public char ToUpper(char c) { throw null; }
public string ToUpper(string str) { throw null; }
+ public System.Text.Rune ToLower(System.Text.Rune value) { throw null; }
+ public System.Text.Rune ToUpper(System.Text.Rune value) { throw null; }
}
public partial class ThaiBuddhistCalendar : System.Globalization.Calendar
{
@@ -10990,6 +11014,7 @@ public virtual void Flush() { }
public static System.IO.TextWriter Synchronized(System.IO.TextWriter writer) { throw null; }
public virtual void Write(bool value) { }
public virtual void Write(char value) { }
+ public virtual void Write(System.Text.Rune value) { }
public virtual void Write(char[]? buffer) { }
public virtual void Write(char[] buffer, int index, int count) { }
public virtual void Write(decimal value) { }
@@ -11011,6 +11036,7 @@ public virtual void Write(uint value) { }
[System.CLSCompliantAttribute(false)]
public virtual void Write(ulong value) { }
public virtual System.Threading.Tasks.Task WriteAsync(char value) { throw null; }
+ public virtual System.Threading.Tasks.Task WriteAsync(System.Text.Rune value) { throw null; }
public System.Threading.Tasks.Task WriteAsync(char[]? buffer) { throw null; }
public virtual System.Threading.Tasks.Task WriteAsync(char[] buffer, int index, int count) { throw null; }
public virtual System.Threading.Tasks.Task WriteAsync(System.ReadOnlyMemory buffer, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
@@ -11019,6 +11045,7 @@ public virtual void Write(ulong value) { }
public virtual void WriteLine() { }
public virtual void WriteLine(bool value) { }
public virtual void WriteLine(char value) { }
+ public virtual void WriteLine(System.Text.Rune value) { }
public virtual void WriteLine(char[]? buffer) { }
public virtual void WriteLine(char[] buffer, int index, int count) { }
public virtual void WriteLine(decimal value) { }
@@ -11041,6 +11068,7 @@ public virtual void WriteLine(uint value) { }
public virtual void WriteLine(ulong value) { }
public virtual System.Threading.Tasks.Task WriteLineAsync() { throw null; }
public virtual System.Threading.Tasks.Task WriteLineAsync(char value) { throw null; }
+ public virtual System.Threading.Tasks.Task WriteLineAsync(System.Text.Rune value) { throw null; }
public System.Threading.Tasks.Task WriteLineAsync(char[]? buffer) { throw null; }
public virtual System.Threading.Tasks.Task WriteLineAsync(char[] buffer, int index, int count) { throw null; }
public virtual System.Threading.Tasks.Task WriteLineAsync(System.ReadOnlyMemory buffer, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
@@ -15672,6 +15700,7 @@ public enum NormalizationForm
public int EncodeToUtf8(System.Span destination) { throw null; }
public override bool Equals([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] object? obj) { throw null; }
public bool Equals(System.Text.Rune other) { throw null; }
+ public bool Equals(System.Text.Rune other, System.StringComparison comparisonType) { throw null; }
public override int GetHashCode() { throw null; }
public static double GetNumericValue(System.Text.Rune value) { throw null; }
public static System.Text.Rune GetRuneAt(string input, int index) { throw null; }
@@ -15778,6 +15807,7 @@ public StringBuilder(string? value, int startIndex, int length, int capacity) {
public System.Text.StringBuilder Append(bool value) { throw null; }
public System.Text.StringBuilder Append(byte value) { throw null; }
public System.Text.StringBuilder Append(char value) { throw null; }
+ public System.Text.StringBuilder Append(System.Text.Rune value) { throw null; }
[System.CLSCompliantAttribute(false)]
public unsafe System.Text.StringBuilder Append(char* value, int valueCount) { throw null; }
public System.Text.StringBuilder Append(char value, int repeatCount) { throw null; }
@@ -15842,9 +15872,12 @@ public void CopyTo(int sourceIndex, System.Span destination, int count) {
public bool Equals(System.ReadOnlySpan span) { throw null; }
public bool Equals([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] System.Text.StringBuilder? sb) { throw null; }
public System.Text.StringBuilder.ChunkEnumerator GetChunks() { throw null; }
+ public System.Text.StringBuilderRuneEnumerator EnumerateRunes() { throw null; }
+ public Rune GetRuneAt(int index) { throw null; }
public System.Text.StringBuilder Insert(int index, bool value) { throw null; }
public System.Text.StringBuilder Insert(int index, byte value) { throw null; }
public System.Text.StringBuilder Insert(int index, char value) { throw null; }
+ public System.Text.StringBuilder Insert(int index, System.Text.Rune value) { throw null; }
public System.Text.StringBuilder Insert(int index, char[]? value) { throw null; }
public System.Text.StringBuilder Insert(int index, char[]? value, int startIndex, int charCount) { throw null; }
public System.Text.StringBuilder Insert(int index, decimal value) { throw null; }
@@ -15868,6 +15901,8 @@ public void CopyTo(int sourceIndex, System.Span destination, int count) {
public System.Text.StringBuilder Remove(int startIndex, int length) { throw null; }
public System.Text.StringBuilder Replace(char oldChar, char newChar) { throw null; }
public System.Text.StringBuilder Replace(char oldChar, char newChar, int startIndex, int count) { throw null; }
+ public System.Text.StringBuilder Replace(System.Text.Rune oldRune, System.Text.Rune newRune) { throw null; }
+ public System.Text.StringBuilder Replace(System.Text.Rune oldRune, System.Text.Rune newRune, int startIndex, int count) { throw null; }
public System.Text.StringBuilder Replace(System.ReadOnlySpan oldValue, System.ReadOnlySpan newValue) { throw null; }
public System.Text.StringBuilder Replace(System.ReadOnlySpan oldValue, System.ReadOnlySpan newValue, int startIndex, int count) { throw null; }
public System.Text.StringBuilder Replace(string oldValue, string? newValue) { throw null; }
@@ -15875,6 +15910,7 @@ public void CopyTo(int sourceIndex, System.Span destination, int count) {
void System.Runtime.Serialization.ISerializable.GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { }
public override string ToString() { throw null; }
public string ToString(int startIndex, int length) { throw null; }
+ public bool TryGetRuneAt(int index, out Rune value) { throw null; }
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
[System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute]
public partial struct AppendInterpolatedStringHandler
@@ -15904,6 +15940,19 @@ public partial struct ChunkEnumerator
public bool MoveNext() { throw null; }
}
}
+ public partial struct StringBuilderRuneEnumerator : System.Collections.Generic.IEnumerable, System.Collections.Generic.IEnumerator, System.Collections.IEnumerable, System.Collections.IEnumerator, System.IDisposable
+ {
+ private object _dummy;
+ private int _dummyPrimitive;
+ public System.Text.Rune Current { get { throw null; } }
+ object? System.Collections.IEnumerator.Current { get { throw null; } }
+ public System.Text.StringBuilderRuneEnumerator GetEnumerator() { throw null; }
+ public bool MoveNext() { throw null; }
+ System.Collections.Generic.IEnumerator System.Collections.Generic.IEnumerable.GetEnumerator() { throw null; }
+ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; }
+ void System.Collections.IEnumerator.Reset() { }
+ void System.IDisposable.Dispose() { }
+ }
public partial struct StringRuneEnumerator : System.Collections.Generic.IEnumerable, System.Collections.Generic.IEnumerator, System.Collections.IEnumerable, System.Collections.IEnumerator, System.IDisposable
{
private object _dummy;
diff --git a/src/libraries/System.Runtime/tests/System.Globalization.Tests/System/Globalization/TextInfoTests.cs b/src/libraries/System.Runtime/tests/System.Globalization.Tests/System/Globalization/TextInfoTests.cs
index 924de536b6efbd..0afc990cba5db7 100644
--- a/src/libraries/System.Runtime/tests/System.Globalization.Tests/System/Globalization/TextInfoTests.cs
+++ b/src/libraries/System.Runtime/tests/System.Globalization.Tests/System/Globalization/TextInfoTests.cs
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Reflection;
+using System.Text;
using Xunit;
namespace System.Globalization.Tests
@@ -344,6 +345,25 @@ public void ToLower_Null_ThrowsArgumentNullException(string cultureName)
AssertExtensions.Throws("str", () => new CultureInfo(cultureName).TextInfo.ToLower(null));
}
+ public static IEnumerable ToLower_Rune_TestData()
+ {
+ foreach (string cultureName in s_cultureNames)
+ {
+ yield return new object[] { cultureName, new Rune('a'), new Rune('a') };
+ yield return new object[] { cultureName, new Rune('A'), new Rune('a') };
+ yield return new object[] { cultureName, new Rune(0x01F600), new Rune(0x01F600) }; // 😀 → 😀
+ yield return new object[] { cultureName, new Rune(0x010428), new Rune(0x010428) }; // 𐐨 → 𐐨
+ yield return new object[] { cultureName, new Rune(0x010400), new Rune(0x010428) }; // 𐐀 → 𐐨
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(ToLower_Rune_TestData))]
+ public void ToLower_Rune(string name, Rune value, Rune expected)
+ {
+ Assert.Equal(expected, new CultureInfo(name).TextInfo.ToLower(value));
+ }
+
// ToUpper_TestData_netcore has the data which is specific to netcore framework
public static IEnumerable ToUpper_TestData_netcore()
{
@@ -475,6 +495,25 @@ public void ToUpper_Null_ThrowsArgumentNullException(string cultureName)
AssertExtensions.Throws("str", () => new CultureInfo(cultureName).TextInfo.ToUpper(null));
}
+ public static IEnumerable ToUpper_Rune_TestData()
+ {
+ foreach (string cultureName in s_cultureNames)
+ {
+ yield return new object[] { cultureName, new Rune('a'), new Rune('A') };
+ yield return new object[] { cultureName, new Rune('A'), new Rune('A') };
+ yield return new object[] { cultureName, new Rune(0x01F600), new Rune(0x01F600) }; // 😀 → 😀
+ yield return new object[] { cultureName, new Rune(0x010428), new Rune(0x010400) }; // 𐐨 → 𐐀
+ yield return new object[] { cultureName, new Rune(0x010400), new Rune(0x010400) }; // 𐐀 → 𐐀
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(ToUpper_Rune_TestData))]
+ public void ToUpper_Rune(string name, Rune value, Rune expected)
+ {
+ Assert.Equal(expected, new CultureInfo(name).TextInfo.ToUpper(value));
+ }
+
[Theory]
[InlineData("en-US", "TextInfo - en-US")]
[InlineData("fr-FR", "TextInfo - fr-FR")]
diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/TestDataProvider/TestDataProvider.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/TestDataProvider/TestDataProvider.cs
index ba4f048ac6208d..f3878ac5d1cb12 100644
--- a/src/libraries/System.Runtime/tests/System.IO.Tests/TestDataProvider/TestDataProvider.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.Tests/TestDataProvider/TestDataProvider.cs
@@ -12,6 +12,7 @@ namespace System.IO.Tests
public static class TestDataProvider
{
private static readonly char[] s_charData;
+ private static readonly Rune[] s_runeData;
private static readonly char[] s_smallData;
private static readonly char[] s_largeData;
@@ -56,6 +57,14 @@ static TestDataProvider()
'\u00E6'
};
+ var runeData = new List();
+ foreach (char charDataInstance in s_charData)
+ {
+ runeData.Add(new Rune(charDataInstance));
+ }
+ runeData.Add(new Rune(0x01F600));
+ s_runeData = runeData.ToArray();
+
s_smallData = "HELLO".ToCharArray();
var data = new List();
@@ -68,6 +77,8 @@ static TestDataProvider()
public static char[] CharData => s_charData;
+ public static Rune[] RuneData => s_runeData;
+
public static char[] SmallData => s_smallData;
public static char[] LargeData => s_largeData;
diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/TextWriter/TextWriterTests.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/TextWriter/TextWriterTests.cs
index 08dc045e4a5b5b..f38f1265ad6963 100644
--- a/src/libraries/System.Runtime/tests/System.IO.Tests/TextWriter/TextWriterTests.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.Tests/TextWriter/TextWriterTests.cs
@@ -33,6 +33,19 @@ public void WriteCharTest()
}
}
+ [Fact]
+ public void WriteRuneTest()
+ {
+ using (CharArrayTextWriter tw = NewTextWriter)
+ {
+ for (int count = 0; count < TestDataProvider.RuneData.Length; ++count)
+ {
+ tw.Write(TestDataProvider.RuneData[count]);
+ }
+ Assert.Equal(string.Concat(TestDataProvider.RuneData), tw.Text);
+ }
+ }
+
[Fact]
public void WriteCharArrayTest()
{
@@ -271,6 +284,19 @@ public void WriteLineCharTest()
}
}
+ [Fact]
+ public void WriteLineRuneTest()
+ {
+ using (CharArrayTextWriter tw = NewTextWriter)
+ {
+ for (int count = 0; count < TestDataProvider.RuneData.Length; ++count)
+ {
+ tw.WriteLine(TestDataProvider.RuneData[count]);
+ }
+ Assert.Equal(string.Join(tw.NewLine, TestDataProvider.RuneData.Select(r => r.ToString()).ToArray()) + tw.NewLine, tw.Text);
+ }
+ }
+
[Fact]
public void WriteLineCharArrayTest()
{
@@ -496,6 +522,16 @@ public async Task WriteAsyncCharTest()
}
}
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+ public async Task WriteAsyncRuneTest()
+ {
+ using (CharArrayTextWriter tw = NewTextWriter)
+ {
+ await tw.WriteAsync(new Rune(0x01F600));
+ Assert.Equal("\U0001F600", tw.Text);
+ }
+ }
+
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
public async Task WriteAsyncStringTest()
{
@@ -541,6 +577,16 @@ public async Task WriteLineAsyncCharTest()
}
}
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+ public async Task WriteLineAsyncRuneTest()
+ {
+ using (CharArrayTextWriter tw = NewTextWriter)
+ {
+ await tw.WriteLineAsync(new Rune(0x01F600));
+ Assert.Equal("\U0001F600" + tw.NewLine, tw.Text);
+ }
+ }
+
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
public async Task WriteLineAsyncStringTest()
{
diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/String.SplitTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/String.SplitTests.cs
index 317695b20493b0..4a4345c6df0424 100644
--- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/String.SplitTests.cs
+++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/String.SplitTests.cs
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Linq;
+using System.Text;
using Xunit;
namespace System.Tests
@@ -21,6 +22,8 @@ public static void SplitInvalidCount()
AssertExtensions.Throws("count", () => Value.Split(',', Count));
AssertExtensions.Throws("count", () => Value.Split(',', Count, Options));
+ AssertExtensions.Throws("count", () => Value.Split(new Rune(','), Count));
+ AssertExtensions.Throws("count", () => Value.Split(new Rune(','), Count, Options));
AssertExtensions.Throws("count", () => Value.Split(new[] { ',' }, Count));
AssertExtensions.Throws("count", () => Value.Split(new[] { ',' }, Count, Options));
AssertExtensions.Throws("count", () => Value.Split(",", Count));
@@ -38,6 +41,8 @@ public static void SplitInvalidOptions()
{
AssertExtensions.Throws("options", () => Value.Split(',', options));
AssertExtensions.Throws("options", () => Value.Split(',', Count, options));
+ AssertExtensions.Throws("options", () => Value.Split(new Rune(','), options));
+ AssertExtensions.Throws("options", () => Value.Split(new Rune(','), Count, options));
AssertExtensions.Throws("options", () => Value.Split(new[] { ',' }, options));
AssertExtensions.Throws("options", () => Value.Split(new[] { ',' }, Count, options));
AssertExtensions.Throws("options", () => Value.Split(",", options));
@@ -59,10 +64,12 @@ public static void SplitZeroCountEmptyResult()
const int Count = 0;
const StringSplitOptions Options = StringSplitOptions.None;
- string[] expected = new string[0];
+ string[] expected = Array.Empty();
Assert.Equal(expected, Value.Split(',', Count));
Assert.Equal(expected, Value.Split(',', Count, Options));
+ Assert.Equal(expected, Value.Split(new Rune(','), Count));
+ Assert.Equal(expected, Value.Split(new Rune(','), Count, Options));
Assert.Equal(expected, Value.Split(new[] { ',' }, Count));
Assert.Equal(expected, Value.Split(new[] { ',' }, Count, Options));
Assert.Equal(expected, Value.Split(",", Count));
@@ -85,6 +92,8 @@ public static void SplitEmptyValueWithRemoveEmptyEntriesOptionEmptyResult()
Assert.Equal(expected, Value.Split(',', Options));
Assert.Equal(expected, Value.Split(',', Count, Options));
+ Assert.Equal(expected, Value.Split(new Rune(','), Options));
+ Assert.Equal(expected, Value.Split(new Rune(','), Count, Options));
Assert.Equal(expected, Value.Split(new[] { ',' }, Options));
Assert.Equal(expected, Value.Split(new[] { ',' }, Count, Options));
Assert.Equal(expected, Value.Split(",", Options));
@@ -109,6 +118,8 @@ public static void SplitOneCountSingleResult()
Assert.Equal(expected, Value.Split(',', Count));
Assert.Equal(expected, Value.Split(',', Count, Options));
+ Assert.Equal(expected, Value.Split(new Rune(','), Count));
+ Assert.Equal(expected, Value.Split(new Rune(','), Count, Options));
Assert.Equal(expected, Value.Split(new[] { ',' }, Count));
Assert.Equal(expected, Value.Split(new[] { ',' }, Count, Options));
Assert.Equal(expected, Value.Split(",", Count));
@@ -142,6 +153,9 @@ public static void SplitNoMatchSingleResult()
Assert.Equal(expected, Value.Split(','));
Assert.Equal(expected, Value.Split(',', Options));
Assert.Equal(expected, Value.Split(',', Count, Options));
+ Assert.Equal(expected, Value.Split(new Rune(',')));
+ Assert.Equal(expected, Value.Split(new Rune(','), Options));
+ Assert.Equal(expected, Value.Split(new Rune(','), Count, Options));
Assert.Equal(expected, Value.Split(new[] { ',' }));
Assert.Equal(expected, Value.Split((ReadOnlySpan)new[] { ',' }));
Assert.Equal(expected, Value.Split(new[] { ',' }, Options));
@@ -504,12 +518,14 @@ public static void SplitNoMatchSingleResult()
public static void SplitCharSeparator(string value, char separator, int count, StringSplitOptions options, string[] expected)
{
Assert.Equal(expected, value.Split(separator, count, options));
+ Assert.Equal(expected, value.Split(new Rune(separator), count, options));
Assert.Equal(expected, value.Split(new[] { separator }, count, options));
Assert.Equal(expected, value.Split(separator.ToString(), count, options));
Assert.Equal(expected, value.Split(new[] { separator.ToString() }, count, options));
if (count == int.MaxValue)
{
Assert.Equal(expected, value.Split(separator, options));
+ Assert.Equal(expected, value.Split(new Rune(separator), options));
Assert.Equal(expected, value.Split(new[] { separator }, options));
Assert.Equal(expected, value.Split(separator.ToString(), options));
Assert.Equal(expected, value.Split(new[] { separator.ToString() }, options));
@@ -517,12 +533,14 @@ public static void SplitCharSeparator(string value, char separator, int count, S
if (options == StringSplitOptions.None)
{
Assert.Equal(expected, value.Split(separator, count));
+ Assert.Equal(expected, value.Split(new Rune(separator), count));
Assert.Equal(expected, value.Split(new[] { separator }, count));
Assert.Equal(expected, value.Split(separator.ToString(), count));
}
if (count == int.MaxValue && options == StringSplitOptions.None)
{
Assert.Equal(expected, value.Split(separator));
+ Assert.Equal(expected, value.Split(new Rune(separator)));
Assert.Equal(expected, value.Split(new[] { separator }));
Assert.Equal(expected, value.Split((ReadOnlySpan)new[] { separator }));
Assert.Equal(expected, value.Split(separator.ToString()));
@@ -541,6 +559,53 @@ public static void SplitCharSeparator(string value, char separator, int count, S
Assert.Equal(expected, ranges.Take(expected.Length).Select(r => value[r]).ToArray());
}
+ [Theory]
+ [InlineData("", ',', 0, StringSplitOptions.None, new string[0])]
+ [InlineData("", ',', 1, StringSplitOptions.None, new[] { "" })]
+ [InlineData("a,b,c", ',', M, StringSplitOptions.None, new[] { "a", "b", "c" })]
+ [InlineData("a,b,c", ',', 1, StringSplitOptions.None, new[] { "a,b,c" })]
+ [InlineData("a,b,c", ',', 2, StringSplitOptions.None, new[] { "a", "b,c" })]
+ [InlineData("a,b,c", ',', M, StringSplitOptions.RemoveEmptyEntries, new[] { "a", "b", "c" })]
+ [InlineData("a,b,c", ',', 1, StringSplitOptions.RemoveEmptyEntries, new[] { "a,b,c" })]
+ [InlineData("a,b,c", ',', 2, StringSplitOptions.RemoveEmptyEntries, new[] { "a", "b,c" })]
+ [InlineData("a,b,c", ',', M, StringSplitOptions.TrimEntries, new[] { "a", "b", "c" })]
+ [InlineData("a,b,c", ',', 1, StringSplitOptions.TrimEntries, new[] { "a,b,c" })]
+ [InlineData("a,b,c", ',', 2, StringSplitOptions.TrimEntries, new[] { "a", "b,c" })]
+ [InlineData("a,,b, , c", ',', M, StringSplitOptions.None, new[] { "a", "", "b", " ", " c" })]
+ [InlineData("a,,b, , c", ',', M, StringSplitOptions.RemoveEmptyEntries, new[] { "a", "b", " ", " c" })]
+ [InlineData("a,,b, , c", ',', M, StringSplitOptions.TrimEntries, new[] { "a", "", "b", "", "c" })]
+ [InlineData("a,,b, , c", ',', M, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries, new[] { "a", "b", "c" })]
+ [InlineData("A\U0001F600B \U0001F600C", 0x1F600, M, StringSplitOptions.None, new[] { "A", "B ", "C" })]
+ [InlineData("A\U0001F600B \U0001F600C", 0x1F600, 1, StringSplitOptions.None, new[] { "A\U0001F600B \U0001F600C" })]
+ [InlineData("A\U0001F600B \U0001F600C", 0x1F600, 2, StringSplitOptions.None, new[] { "A", "B \U0001F600C" })]
+ [InlineData("A\U0001F600B \U0001F600C", 0x1F600, M, StringSplitOptions.RemoveEmptyEntries, new[] { "A", "B ", "C" })]
+ [InlineData("A\U0001F600B \U0001F600C", 0x1F600, 1, StringSplitOptions.RemoveEmptyEntries, new[] { "A\U0001F600B \U0001F600C" })]
+ [InlineData("A\U0001F600B \U0001F600C", 0x1F600, 2, StringSplitOptions.RemoveEmptyEntries, new[] { "A", "B \U0001F600C" })]
+ [InlineData("A\U0001F600B \U0001F600C", 0x1F600, M, StringSplitOptions.TrimEntries, new[] { "A", "B", "C" })]
+ [InlineData("A\U0001F600B \U0001F600C", 0x1F600, 1, StringSplitOptions.TrimEntries, new[] { "A\U0001F600B \U0001F600C" })]
+ [InlineData("A\U0001F600B \U0001F600C", 0x1F600, 2, StringSplitOptions.TrimEntries, new[] { "A", "B \U0001F600C" })]
+ [InlineData("A\U0001F600B \U0001F600C", 0x1F600, M, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries, new[] { "A", "B", "C" })]
+ [InlineData("A\U0001F600B \U0001F600C", 0x1F600, 1, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries, new[] { "A\U0001F600B \U0001F600C" })]
+ [InlineData("A\U0001F600B \U0001F600C", 0x1F600, 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries, new[] { "A", "B \U0001F600C" })]
+ public static void SplitRuneSeparator(string value, int separatorAsInt, int count, StringSplitOptions options, string[] expected)
+ {
+ var separator = new Rune(separatorAsInt);
+
+ Assert.Equal(expected, value.Split(separator, count, options));
+ if (count == int.MaxValue)
+ {
+ Assert.Equal(expected, value.Split(separator, options));
+ }
+ if (options == StringSplitOptions.None)
+ {
+ Assert.Equal(expected, value.Split(separator, count));
+ }
+ if (count == int.MaxValue && options == StringSplitOptions.None)
+ {
+ Assert.Equal(expected, value.Split(separator));
+ }
+ }
+
[Theory]
[InlineData("", null, 0, StringSplitOptions.None, new string[0])]
[InlineData("", "", 0, StringSplitOptions.None, new string[0])]
diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/StringTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/StringTests.cs
index 4d7258fb4c0985..57673383fc225b 100644
--- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/StringTests.cs
+++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/StringTests.cs
@@ -203,6 +203,28 @@ public static void Contains_Char(string s, char value, bool expected)
Assert.Equal(expected, span.Contains(value));
}
+ public static IEnumerable Contains_Rune_TestData()
+ {
+ yield return new object[] { "Hello", new Rune('H'), true };
+ yield return new object[] { "Hello", new Rune('Z'), false };
+ yield return new object[] { "Hello", new Rune('e'), true };
+ yield return new object[] { "Hello", new Rune('E'), false };
+ yield return new object[] { "", new Rune('H'), false };
+
+ // Non-BMP rune (GRINNING FACE)
+ yield return new object[] { "hello world", new Rune(0x1F600), false };
+ yield return new object[] { "hello \U0001F600 world", new Rune(0x1F600), true };
+ yield return new object[] { "hello world \U0001F600", new Rune(0x1F600), true };
+ yield return new object[] { "\U0001F600 hello world", new Rune(0x1F600), true };
+ }
+
+ [Theory]
+ [MemberData(nameof(Contains_Rune_TestData))]
+ public static void Contains_Rune(string s, Rune value, bool expected)
+ {
+ Assert.Equal(expected, s.Contains(value));
+ }
+
[Theory]
// CurrentCulture
[InlineData("Hello", 'H', StringComparison.CurrentCulture, true)]
@@ -246,6 +268,65 @@ public static void Contains_Char_StringComparison(string s, char value, StringCo
Assert.Equal(expected, s.Contains(value, comparisonType));
}
+ public static IEnumerable Contains_Rune_StringComparison_TestData()
+ {
+ // CurrentCulture
+ yield return new object[] { "Hello", 'H', StringComparison.CurrentCulture, true };
+ yield return new object[] { "Hello", 'Z', StringComparison.CurrentCulture, false };
+ yield return new object[] { "Hello", 'e', StringComparison.CurrentCulture, true };
+ yield return new object[] { "Hello", 'E', StringComparison.CurrentCulture, false };
+ yield return new object[] { "", 'H', StringComparison.CurrentCulture, false };
+ yield return new object[] { "", '\u0301', StringComparison.CurrentCulture, false }; // Using non-ASCII character to test ICU path
+
+ // CurrentCultureIgnoreCase
+ yield return new object[] { "Hello", 'H', StringComparison.CurrentCultureIgnoreCase, true };
+ yield return new object[] { "Hello", 'Z', StringComparison.CurrentCultureIgnoreCase, false };
+ yield return new object[] { "Hello", 'e', StringComparison.CurrentCultureIgnoreCase, true };
+ yield return new object[] { "Hello", 'E', StringComparison.CurrentCultureIgnoreCase, true };
+ yield return new object[] { "", 'H', StringComparison.CurrentCultureIgnoreCase, false };
+
+ // InvariantCulture
+ yield return new object[] { "Hello", 'H', StringComparison.InvariantCulture, true };
+ yield return new object[] { "Hello", 'Z', StringComparison.InvariantCulture, false };
+ yield return new object[] { "Hello", 'e', StringComparison.InvariantCulture, true };
+ yield return new object[] { "Hello", 'E', StringComparison.InvariantCulture, false };
+ yield return new object[] { "", 'H', StringComparison.InvariantCulture, false };
+
+ // InvariantCultureIgnoreCase
+ yield return new object[] { "Hello", 'H', StringComparison.InvariantCultureIgnoreCase, true };
+ yield return new object[] { "Hello", 'Z', StringComparison.InvariantCultureIgnoreCase, false };
+ yield return new object[] { "Hello", 'e', StringComparison.InvariantCultureIgnoreCase, true };
+ yield return new object[] { "Hello", 'E', StringComparison.InvariantCultureIgnoreCase, true };
+ yield return new object[] { "", 'H', StringComparison.InvariantCultureIgnoreCase, false };
+
+ // Ordinal
+ yield return new object[] { "Hello", 'H', StringComparison.Ordinal, true };
+ yield return new object[] { "Hello", 'Z', StringComparison.Ordinal, false };
+ yield return new object[] { "Hello", 'e', StringComparison.Ordinal, true };
+ yield return new object[] { "Hello", 'E', StringComparison.Ordinal, false };
+ yield return new object[] { "", 'H', StringComparison.Ordinal, false };
+
+ // OrdinalIgnoreCase
+ yield return new object[] { "Hello", 'H', StringComparison.OrdinalIgnoreCase, true };
+ yield return new object[] { "Hello", 'Z', StringComparison.OrdinalIgnoreCase, false };
+ yield return new object[] { "Hello", 'e', StringComparison.OrdinalIgnoreCase, true };
+ yield return new object[] { "Hello", 'E', StringComparison.OrdinalIgnoreCase, true };
+ yield return new object[] { "", 'H', StringComparison.OrdinalIgnoreCase, false };
+
+ // Non-BMP rune (GRINNING FACE) with OrdinalIgnoreCase
+ yield return new object[] { "hello world", new Rune(0x1F600), StringComparison.OrdinalIgnoreCase, false };
+ yield return new object[] { "hello \U0001F600 world", new Rune(0x1F600), StringComparison.OrdinalIgnoreCase, true };
+ yield return new object[] { "hello world \U0001F600", new Rune(0x1F600), StringComparison.OrdinalIgnoreCase, true };
+ yield return new object[] { "\U0001F600 hello world", new Rune(0x1F600), StringComparison.OrdinalIgnoreCase, true };
+ }
+
+ [Theory]
+ [MemberData(nameof(Contains_Rune_StringComparison_TestData))]
+ public static void Contains_Rune_StringComparison(string s, Rune value, StringComparison comparisonType, bool expected)
+ {
+ Assert.Equal(expected, s.Contains(value, comparisonType));
+ }
+
public static IEnumerable Contains_String_StringComparison_TestData()
{
yield return new object[] { "Hello", "ello", StringComparison.CurrentCulture, true };
@@ -513,11 +594,71 @@ public static void Contains_InvalidComparisonType_ThrowsArgumentOutOfRangeExcept
[InlineData("\0", '\0', true)]
[InlineData("", 'a', false)]
[InlineData("abcdefghijklmnopqrstuvwxyz", 'z', true)]
- public static void EndsWith(string s, char value, bool expected)
+ public static void EndsWith_Char(string s, char value, bool expected)
{
Assert.Equal(expected, s.EndsWith(value));
}
+ [Theory]
+ [InlineData("Hello", 'o', StringComparison.CurrentCulture, true)]
+ [InlineData("Hello", 'O', StringComparison.CurrentCulture, false)]
+ [InlineData("Hello", 'o', StringComparison.CurrentCultureIgnoreCase, true)]
+ [InlineData("Hello", 'O', StringComparison.CurrentCultureIgnoreCase, true)]
+ [InlineData("Hello", 'o', StringComparison.InvariantCulture, true)]
+ [InlineData("Hello", 'O', StringComparison.InvariantCulture, false)]
+ [InlineData("Hello", 'o', StringComparison.InvariantCultureIgnoreCase, true)]
+ [InlineData("Hello", 'O', StringComparison.InvariantCultureIgnoreCase, true)]
+ [InlineData("Hello", 'o', StringComparison.Ordinal, true)]
+ [InlineData("Hello", 'O', StringComparison.Ordinal, false)]
+ [InlineData("Hello", 'o', StringComparison.OrdinalIgnoreCase, true)]
+ [InlineData("Hello", 'O', StringComparison.OrdinalIgnoreCase, true)]
+ [InlineData("", '\0', StringComparison.Ordinal, false)]
+ [InlineData("\0", '\0', StringComparison.Ordinal, true)]
+ [InlineData("", '\0', StringComparison.OrdinalIgnoreCase, false)]
+ [InlineData("\0", '\0', StringComparison.OrdinalIgnoreCase, true)]
+ public static void EndsWith_Char_StringComparison(string s, char value, StringComparison comparisonType, bool expected)
+ {
+ Assert.Equal(expected, s.EndsWith(value, comparisonType));
+ }
+
+ [Theory]
+ [InlineData("Hello", 'o', true)]
+ [InlineData("Hello", 'O', false)]
+ [InlineData("o", 'o', true)]
+ [InlineData("o", 'O', false)]
+ [InlineData("Hello", 'e', false)]
+ [InlineData("Hello", '\0', false)]
+ [InlineData("", '\0', false)]
+ [InlineData("\0", '\0', true)]
+ [InlineData("", 'a', false)]
+ [InlineData("abcdefghijklmnopqrstuvwxyz", 'z', true)]
+ public static void EndsWith_Rune(string s, int valueAsInt, bool expected)
+ {
+ Assert.Equal(expected, s.EndsWith(new Rune(valueAsInt)));
+ }
+
+ [Theory]
+ [InlineData("Hello", 'o', StringComparison.CurrentCulture, true)]
+ [InlineData("Hello", 'O', StringComparison.CurrentCulture, false)]
+ [InlineData("Hello", 'o', StringComparison.CurrentCultureIgnoreCase, true)]
+ [InlineData("Hello", 'O', StringComparison.CurrentCultureIgnoreCase, true)]
+ [InlineData("Hello", 'o', StringComparison.InvariantCulture, true)]
+ [InlineData("Hello", 'O', StringComparison.InvariantCulture, false)]
+ [InlineData("Hello", 'o', StringComparison.InvariantCultureIgnoreCase, true)]
+ [InlineData("Hello", 'O', StringComparison.InvariantCultureIgnoreCase, true)]
+ [InlineData("Hello", 'o', StringComparison.Ordinal, true)]
+ [InlineData("Hello", 'O', StringComparison.Ordinal, false)]
+ [InlineData("Hello", 'o', StringComparison.OrdinalIgnoreCase, true)]
+ [InlineData("Hello", 'O', StringComparison.OrdinalIgnoreCase, true)]
+ [InlineData("", '\0', StringComparison.Ordinal, false)]
+ [InlineData("\0", '\0', StringComparison.Ordinal, true)]
+ [InlineData("", '\0', StringComparison.OrdinalIgnoreCase, false)]
+ [InlineData("\0", '\0', StringComparison.OrdinalIgnoreCase, true)]
+ public static void EndsWith_Rune_StringComparison(string s, int valueAsInt, StringComparison comparisonType, bool expected)
+ {
+ Assert.Equal(expected, s.EndsWith(new Rune(valueAsInt), comparisonType));
+ }
+
[Theory]
[InlineData(new char[0], new int[0])] // empty
[InlineData(new char[] { 'x', 'y', 'z' }, new int[] { 'x', 'y', 'z' })]
@@ -546,7 +687,7 @@ public static void EnumerateRunes(char[] chars, int[] expected)
// Then use LINQ to ensure IEnumerator<...> works as expected
- int[] enumeratedValues = new string(chars).EnumerateRunes().Select(r => r.Value).ToArray();
+ int[] enumeratedValues = asString.EnumerateRunes().Select(r => r.Value).ToArray();
Assert.Equal(expected, enumeratedValues);
}
@@ -624,11 +765,71 @@ static string FixupSequences(string input)
[InlineData("\0", '\0', true)]
[InlineData("", 'a', false)]
[InlineData("abcdefghijklmnopqrstuvwxyz", 'a', true)]
- public static void StartsWith(string s, char value, bool expected)
+ public static void StartsWith_Char(string s, char value, bool expected)
{
Assert.Equal(expected, s.StartsWith(value));
}
+ [Theory]
+ [InlineData("Hello", 'H', StringComparison.CurrentCulture, true)]
+ [InlineData("Hello", 'h', StringComparison.CurrentCulture, false)]
+ [InlineData("Hello", 'H', StringComparison.CurrentCultureIgnoreCase, true)]
+ [InlineData("Hello", 'h', StringComparison.CurrentCultureIgnoreCase, true)]
+ [InlineData("Hello", 'H', StringComparison.InvariantCulture, true)]
+ [InlineData("Hello", 'h', StringComparison.InvariantCulture, false)]
+ [InlineData("Hello", 'H', StringComparison.InvariantCultureIgnoreCase, true)]
+ [InlineData("Hello", 'h', StringComparison.InvariantCultureIgnoreCase, true)]
+ [InlineData("Hello", 'H', StringComparison.Ordinal, true)]
+ [InlineData("Hello", 'h', StringComparison.Ordinal, false)]
+ [InlineData("Hello", 'H', StringComparison.OrdinalIgnoreCase, true)]
+ [InlineData("Hello", 'h', StringComparison.OrdinalIgnoreCase, true)]
+ [InlineData("", '\0', StringComparison.Ordinal, false)]
+ [InlineData("\0", '\0', StringComparison.Ordinal, true)]
+ [InlineData("", '\0', StringComparison.OrdinalIgnoreCase, false)]
+ [InlineData("\0", '\0', StringComparison.OrdinalIgnoreCase, true)]
+ public static void StartsWith_Char_StringComparison(string s, char value, StringComparison comparisonType, bool expected)
+ {
+ Assert.Equal(expected, s.StartsWith(value, comparisonType));
+ }
+
+ [Theory]
+ [InlineData("Hello", 'H', true)]
+ [InlineData("Hello", 'h', false)]
+ [InlineData("H", 'H', true)]
+ [InlineData("H", 'h', false)]
+ [InlineData("Hello", 'e', false)]
+ [InlineData("Hello", '\0', false)]
+ [InlineData("", '\0', false)]
+ [InlineData("\0", '\0', true)]
+ [InlineData("", 'a', false)]
+ [InlineData("abcdefghijklmnopqrstuvwxyz", 'a', true)]
+ public static void StartsWith_Rune(string s, int valueAsInt, bool expected)
+ {
+ Assert.Equal(expected, s.StartsWith(new Rune(valueAsInt)));
+ }
+
+ [Theory]
+ [InlineData("Hello", 'H', StringComparison.CurrentCulture, true)]
+ [InlineData("Hello", 'h', StringComparison.CurrentCulture, false)]
+ [InlineData("Hello", 'H', StringComparison.CurrentCultureIgnoreCase, true)]
+ [InlineData("Hello", 'h', StringComparison.CurrentCultureIgnoreCase, true)]
+ [InlineData("Hello", 'H', StringComparison.InvariantCulture, true)]
+ [InlineData("Hello", 'h', StringComparison.InvariantCulture, false)]
+ [InlineData("Hello", 'H', StringComparison.InvariantCultureIgnoreCase, true)]
+ [InlineData("Hello", 'h', StringComparison.InvariantCultureIgnoreCase, true)]
+ [InlineData("Hello", 'H', StringComparison.Ordinal, true)]
+ [InlineData("Hello", 'h', StringComparison.Ordinal, false)]
+ [InlineData("Hello", 'H', StringComparison.OrdinalIgnoreCase, true)]
+ [InlineData("Hello", 'h', StringComparison.OrdinalIgnoreCase, true)]
+ [InlineData("", '\0', StringComparison.Ordinal, false)]
+ [InlineData("\0", '\0', StringComparison.Ordinal, true)]
+ [InlineData("", '\0', StringComparison.OrdinalIgnoreCase, false)]
+ [InlineData("\0", '\0', StringComparison.OrdinalIgnoreCase, true)]
+ public static void StartsWith_Rune_StringComparison(string s, int valueAsInt, StringComparison comparisonType, bool expected)
+ {
+ Assert.Equal(expected, s.StartsWith(new Rune(valueAsInt), comparisonType));
+ }
+
public static IEnumerable Join_Char_StringArray_TestData()
{
yield return new object[] { '|', new string[0], 0, 0, "" };
@@ -716,6 +917,23 @@ public static void Join_Char_InvalidStartIndexCount_ThrowsArgumentOutOfRangeExce
AssertExtensions.Throws("startIndex", () => string.Join('|', new string[] { "Foo" }, startIndex, count));
}
+ public static IEnumerable Replace_Rune_Rune_TestData()
+ {
+ yield return new object[] { "abc", new Rune('a'), new Rune('b'), "bbc" };
+ yield return new object[] { "abc", new Rune('a'), new Rune('a'), "abc" };
+ yield return new object[] { "abcabccbacba", new Rune('C'), new Rune('F'), "abcabccbacba" };
+ yield return new object[] { "私は私", new Rune('私'), new Rune('海'), "海は海" };
+ yield return new object[] { "Cat dog Bear.", new Rune(0x1F600), new Rune(0x1F601), "Cat dog Bear." };
+ yield return new object[] { "Cat dog\U0001F600 Bear.", new Rune(0x1F600), new Rune(0x1F601), "Cat dog\U0001F601 Bear." };
+ }
+
+ [Theory]
+ [MemberData(nameof(Replace_Rune_Rune_TestData))]
+ public void Replace_Rune_ReturnsExpected(string original, Rune oldValue, Rune newValue, string expected)
+ {
+ Assert.Equal(expected, original.Replace(oldValue, newValue));
+ }
+
public static IEnumerable Replace_StringComparison_TestData()
{
yield return new object[] { "abc", "abc", "def", StringComparison.CurrentCulture, "def" };
@@ -1298,19 +1516,36 @@ public static void IndexOf_Invalid_Char()
AssertExtensions.Throws("comparisonType", () => "foo".IndexOf('o', StringComparison.OrdinalIgnoreCase + 1));
}
+ public static IEnumerable IndexOf_Rune_StringComparison_TestData()
+ {
+ yield return new object[] { "Hello\uD801\uDC28", new Rune('\uD801', '\uDC4f'), StringComparison.Ordinal, -1};
+ yield return new object[] { "Hello\uD801\uDC28", new Rune('\uD801', '\uDC00'), StringComparison.OrdinalIgnoreCase, 5};
+
+ yield return new object[] { "Hello\u0200\u0202", new Rune('\u0201'), StringComparison.OrdinalIgnoreCase, 5};
+ yield return new object[] { "Hello\u0200\u0202", new Rune('\u0201'), StringComparison.Ordinal, -1};
+ }
+
+ [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsIcuGlobalization))]
+ [MemberData(nameof(IndexOf_Rune_StringComparison_TestData))]
+ public static void IndexOf_Rune_StringComparison(string source, Rune target, StringComparison stringComparison, int expected)
+ {
+ Assert.Equal(expected, source.IndexOf(target, stringComparison));
+ }
+
public static IEnumerable IndexOf_String_StringComparison_TestData()
{
yield return new object[] { "Hello\uD801\uDC28", "\uD801\uDC4f", StringComparison.Ordinal, -1};
yield return new object[] { "Hello\uD801\uDC28", "\uD801\uDC00", StringComparison.OrdinalIgnoreCase, 5};
+
yield return new object[] { "Hello\u0200\u0202", "\u0201\u0203", StringComparison.OrdinalIgnoreCase, 5};
yield return new object[] { "Hello\u0200\u0202", "\u0201\u0203", StringComparison.Ordinal, -1};
+
yield return new object[] { "Hello\uD801\uDC00", "\uDC00", StringComparison.Ordinal, 6};
yield return new object[] { "Hello\uD801\uDC00", "\uDC00", StringComparison.OrdinalIgnoreCase, 6};
yield return new object[] { "Hello\uD801\uDC00", "\uD801", StringComparison.OrdinalIgnoreCase, 5};
yield return new object[] { "Hello\uD801\uDC00", "\uD801\uDC00", StringComparison.Ordinal, 5};
}
-
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsIcuGlobalization))]
[MemberData(nameof(IndexOf_String_StringComparison_TestData))]
public static void IndexOf_Ordinal_Misc(string source, string target, StringComparison stringComparison, int expected)
@@ -1318,14 +1553,34 @@ public static void IndexOf_Ordinal_Misc(string source, string target, StringComp
Assert.Equal(expected, source.IndexOf(target, stringComparison));
}
+ public static IEnumerable LastIndexOf_Rune_StringComparison_TestData()
+ {
+ yield return new object[] { "\uD801\uDC28Hello", new Rune('\uD801', '\uDC4f'), StringComparison.Ordinal, -1};
+ yield return new object[] { "\uD801\uDC28Hello", new Rune('\uD801', '\uDC00'), StringComparison.OrdinalIgnoreCase, 0};
+ yield return new object[] { "\uD801\uDC28Hello\uD801\uDC28", new Rune('\uD801', '\uDC00'), StringComparison.OrdinalIgnoreCase, 7};
+
+ yield return new object[] { "\u0200\u0202Hello", new Rune('\u0201'), StringComparison.OrdinalIgnoreCase, 0};
+ yield return new object[] { "\u0200\u0202Hello\u0200\u0202", new Rune('\u0201'), StringComparison.OrdinalIgnoreCase, 7}; // \u0200 is uppercase of \u0201
+ yield return new object[] { "\u0200\u0202Hello", new Rune('\u0201'), StringComparison.Ordinal, -1};
+ }
+
+ [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsIcuGlobalization))]
+ [MemberData(nameof(LastIndexOf_Rune_StringComparison_TestData))]
+ public static void LastIndexOf_Rune_StringComparison(string source, Rune target, StringComparison stringComparison, int expected)
+ {
+ Assert.Equal(expected, source.LastIndexOf(target, stringComparison));
+ }
+
public static IEnumerable LastIndexOf_String_StringComparison_TestData()
{
yield return new object[] { "\uD801\uDC28Hello", "\uD801\uDC4f", 6, StringComparison.Ordinal, -1};
yield return new object[] { "\uD801\uDC28Hello", "\uD801\uDC00", 6, StringComparison.OrdinalIgnoreCase, 0};
yield return new object[] { "\uD801\uDC28Hello\uD801\uDC28", "\uD801\uDC00", 1, StringComparison.OrdinalIgnoreCase, 0};
+
yield return new object[] { "\u0200\u0202Hello", "\u0201\u0203", 6, StringComparison.OrdinalIgnoreCase, 0};
yield return new object[] { "\u0200\u0202Hello\u0200\u0202", "\u0201\u0203", 1, StringComparison.OrdinalIgnoreCase, 0};
yield return new object[] { "\u0200\u0202Hello", "\u0201\u0203", 6, StringComparison.Ordinal, -1};
+
yield return new object[] { "\uD801\uDC00Hello", "\uDC00", 6, StringComparison.Ordinal, 1};
yield return new object[] { "\uD801\uDC00Hello\uDC00", "\uDC00", 3, StringComparison.Ordinal, 1};
yield return new object[] { "\uD801\uDC00Hello", "\uDC00", 6, StringComparison.OrdinalIgnoreCase, 1};
@@ -1921,6 +2176,39 @@ public static void MakeSureNoTrimChecksGoOutOfRange_Memory()
}
}
+ [Theory]
+ [InlineData("hello", 'h', "ello")]
+ [InlineData(" ", ' ', "")]
+ [InlineData("\U0001F600\U0001F600\U0001F601\U0001F600", 0x1F600, "\U0001F601")]
+ [InlineData("\U0001F600\U0001F600\U0001F601\U0001F600", 0x1F601, "\U0001F600\U0001F600\U0001F601\U0001F600")]
+ [InlineData("", ' ', "")]
+ public static void Trim_Rune(string source, int trimRuneAsInt, string expected)
+ {
+ Assert.Equal(expected, source.Trim(new Rune(trimRuneAsInt)));
+ }
+
+ [Theory]
+ [InlineData("hello", 'h', "ello")]
+ [InlineData(" ", ' ', "")]
+ [InlineData("\U0001F600\U0001F600\U0001F601\U0001F600", 0x1F600, "\U0001F601\U0001F600")]
+ [InlineData("\U0001F600\U0001F600\U0001F601\U0001F600", 0x1F601, "\U0001F600\U0001F600\U0001F601\U0001F600")]
+ [InlineData("", ' ', "")]
+ public static void TrimStart_Rune(string source, int trimRuneAsInt, string expected)
+ {
+ Assert.Equal(expected, source.TrimStart(new Rune(trimRuneAsInt)));
+ }
+
+ [Theory]
+ [InlineData("hello", 'h', "hello")]
+ [InlineData(" ", ' ', "")]
+ [InlineData("\U0001F600\U0001F600\U0001F601\U0001F600", 0x1F600, "\U0001F600\U0001F600\U0001F601")]
+ [InlineData("\U0001F600\U0001F600\U0001F601\U0001F600", 0x1F601, "\U0001F600\U0001F600\U0001F601\U0001F600")]
+ [InlineData("", ' ', "")]
+ public static void TrimEnd_Rune(string source, int trimRuneAsInt, string expected)
+ {
+ Assert.Equal(expected, source.TrimEnd(new Rune(trimRuneAsInt)));
+ }
+
[OuterLoop]
[Theory]
[InlineData(CompareOptions.None)]
diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Text/RuneTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Text/RuneTests.cs
index 378bf7d92d88e8..9d76aa00c50dd9 100644
--- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Text/RuneTests.cs
+++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Text/RuneTests.cs
@@ -332,6 +332,39 @@ public static void Equals_OperatorEqual_OperatorNotEqual(int first, int other, b
Assert.NotEqual(expected, a != b);
}
+ [Theory]
+ [InlineData('a', 'a', StringComparison.CurrentCulture, true)]
+ [InlineData('a', 'A', StringComparison.CurrentCulture, false)]
+ [InlineData(0x1F600, 0x1F600, StringComparison.CurrentCulture, true)]
+ [InlineData(0x1F601, 0x1F600, StringComparison.CurrentCulture, false)]
+ [InlineData('a', 'a', StringComparison.CurrentCultureIgnoreCase, true)]
+ [InlineData('a', 'A', StringComparison.CurrentCultureIgnoreCase, true)]
+ [InlineData(0x1F600, 0x1F600, StringComparison.CurrentCultureIgnoreCase, true)]
+ [InlineData(0x1F601, 0x1F600, StringComparison.CurrentCultureIgnoreCase, false)]
+ [InlineData('a', 'a', StringComparison.InvariantCulture, true)]
+ [InlineData('a', 'A', StringComparison.InvariantCulture, false)]
+ [InlineData(0x1F600, 0x1F600, StringComparison.InvariantCulture, true)]
+ [InlineData(0x1F601, 0x1F600, StringComparison.InvariantCulture, false)]
+ [InlineData('a', 'a', StringComparison.InvariantCultureIgnoreCase, true)]
+ [InlineData('a', 'A', StringComparison.InvariantCultureIgnoreCase, true)]
+ [InlineData(0x1F600, 0x1F600, StringComparison.InvariantCultureIgnoreCase, true)]
+ [InlineData(0x1F601, 0x1F600, StringComparison.InvariantCultureIgnoreCase, false)]
+ [InlineData('a', 'a', StringComparison.Ordinal, true)]
+ [InlineData('a', 'A', StringComparison.Ordinal, false)]
+ [InlineData(0x1F600, 0x1F600, StringComparison.Ordinal, true)]
+ [InlineData(0x1F601, 0x1F600, StringComparison.Ordinal, false)]
+ [InlineData('a', 'a', StringComparison.OrdinalIgnoreCase, true)]
+ [InlineData('a', 'A', StringComparison.OrdinalIgnoreCase, true)]
+ [InlineData(0x1F600, 0x1F600, StringComparison.OrdinalIgnoreCase, true)]
+ [InlineData(0x1F601, 0x1F600, StringComparison.OrdinalIgnoreCase, false)]
+ public static void Equals_StringComparison(int first, int other, StringComparison comparisonType, bool expected)
+ {
+ Rune a = new Rune(first);
+ Rune b = new Rune(other);
+
+ Assert.Equal(expected, a.Equals(b, comparisonType));
+ }
+
[Theory]
[InlineData(0)]
[InlineData('a')]
diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Text/StringBuilderTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Text/StringBuilderTests.cs
index 27a9a02eded2f9..e9a9741a4b1fc1 100644
--- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Text/StringBuilderTests.cs
+++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Text/StringBuilderTests.cs
@@ -554,6 +554,21 @@ public static void Append_Char_NoSpareCapacity_ThrowsArgumentOutOfRangeException
AssertExtensions.Throws("repeatCount", "requiredLength", () => builder.Append('a', 1));
}
+ [Theory]
+ [InlineData("Hello", '\0', "Hello\0")]
+ [InlineData("Hello", 'a', "Helloa")]
+ [InlineData("", 'b', "b")]
+ [InlineData("Hello", 'c', "Helloc")]
+ [InlineData("Hello", 0x1F600, "Hello\U0001F600")]
+ public static void Append_Rune(string original, int valueAsInt, string expected)
+ {
+ var value = new Rune(valueAsInt);
+
+ var builder = new StringBuilder(original);
+ builder.Append(value);
+ Assert.Equal(expected, builder.ToString());
+ }
+
[Theory]
[InlineData("Hello", new char[] { 'a', 'b', 'c' }, 1, "Helloa")]
[InlineData("Hello", new char[] { 'a', 'b', 'c' }, 2, "Helloab")]
@@ -1180,6 +1195,52 @@ public static void EqualsTest(StringBuilder sb1, StringBuilder sb2, bool expecte
Assert.Equal(expected, sb1.Equals(sb2));
}
+ [Theory]
+ [InlineData("a", 0, (int)'a')]
+ [InlineData("ab", 1, (int)'b')]
+ [InlineData("x\U0001F46Ey", 3, (int)'y')]
+ [InlineData("x\U0001F46Ey", 1, 0x1F46E)] // U+1F46E POLICE OFFICER
+ public static void GetRuneAt_TryGetRuneAt_Utf16_Success(string inputString, int index, int expectedScalarValue)
+ {
+ var inputStringBuilder = new StringBuilder(inputString);
+
+ // GetRuneAt
+ Assert.Equal(expectedScalarValue, inputStringBuilder.GetRuneAt(index).Value);
+
+ // TryGetRuneAt
+ Assert.True(inputStringBuilder.TryGetRuneAt(index, out Rune rune));
+ Assert.Equal(expectedScalarValue, rune.Value);
+ }
+
+ // Our unit test runner doesn't deal well with malformed literal strings, so
+ // we smuggle it as a char[] and turn it into a string within the test itself.
+ [Theory]
+ [InlineData(new char[] { 'x', '\uD83D', '\uDC6E', 'y' }, 2)] // attempt to index into the middle of a UTF-16 surrogate pair
+ [InlineData(new char[] { 'x', '\uD800', 'y' }, 1)] // high surrogate not followed by low surrogate
+ [InlineData(new char[] { 'x', '\uDFFF', '\uDFFF' }, 1)] // attempt to start at a low surrogate
+ [InlineData(new char[] { 'x', '\uD800' }, 1)] // end of string reached before could complete surrogate pair
+ public static void GetRuneAt_TryGetRuneAt_Utf16_InvalidData(char[] inputCharArray, int index)
+ {
+ var inputStringBuilder = new StringBuilder(new string(inputCharArray));
+
+ // GetRuneAt
+ Assert.Throws("index", () => inputStringBuilder.GetRuneAt(index));
+
+ // TryGetRuneAt
+ Assert.False(inputStringBuilder.TryGetRuneAt(index, out Rune rune));
+ Assert.Equal(0, rune.Value);
+ }
+
+ [Fact]
+ public static void GetRuneAt_TryGetRuneAt_Utf16_BadArgs()
+ {
+ // negative index specified
+ Assert.Throws("index", () => new StringBuilder("hello").GetRuneAt(-1));
+
+ // index goes past end of string
+ Assert.Throws("index", () => new StringBuilder(string.Empty).GetRuneAt(0));
+ }
+
[Theory]
[InlineData("Hello", 0, (uint)0, "0Hello")]
[InlineData("Hello", 3, (uint)123, "Hel123lo")]
@@ -1312,6 +1373,31 @@ public static void Insert_Char_Invalid()
AssertExtensions.Throws("requiredLength", () => builder.Insert(builder.Length, '\0')); // New length > builder.MaxCapacity
}
+ [Theory]
+ [InlineData("Hello", 0, '\0', "\0Hello")]
+ [InlineData("Hello", 3, 'a', "Helalo")]
+ [InlineData("Hello", 5, 'b', "Hellob")]
+ [InlineData("hi\U0001F600hello", 7, 0x1F600, "hi\U0001F600hel\U0001F600lo")]
+ public static void Insert_Rune(string original, int index, int valueAsInt, string expected)
+ {
+ var value = new Rune(valueAsInt);
+
+ var builder = new StringBuilder(original);
+ builder.Insert(index, value);
+ Assert.Equal(expected, builder.ToString());
+ }
+
+ [Fact]
+ public static void Insert_Rune_Invalid()
+ {
+ var builder = new StringBuilder(0, 5);
+ builder.Append("Hello");
+
+ AssertExtensions.Throws("index", () => builder.Insert(-1, new Rune('\0'))); // Index < 0
+ AssertExtensions.Throws("index", () => builder.Insert(builder.Length + 1, new Rune('\0'))); // Index > builder.Length
+ AssertExtensions.Throws("requiredLength", () => builder.Insert(builder.Length, new Rune('\0'))); // New length > builder.MaxCapacity
+ }
+
public static IEnumerable Insert_Float_TestData()
{
yield return new object[] { "Hello", 0, (float)0, "0Hello" };
@@ -1727,6 +1813,55 @@ public static void Replace_Char_Invalid()
AssertExtensions.Throws("count", () => builder.Replace('a', 'b', 4, 2)); // Count + start index > builder.Length
}
+ [Theory]
+ [InlineData("", 'a', '!', 0, 0, "")]
+ [InlineData("aaaabbbbccccdddd", 'a', '!', 0, 16, "!!!!bbbbccccdddd")]
+ [InlineData("aaaabbbbccccdddd", 'a', '!', 0, 4, "!!!!bbbbccccdddd")]
+ [InlineData("aaaabbbbccccdddd", 'a', '!', 2, 3, "aa!!bbbbccccdddd")]
+ [InlineData("aaaabbbbccccdddd", 'a', '!', 4, 1, "aaaabbbbccccdddd")]
+ [InlineData("aaaabbbbccccdddd", 'b', '!', 0, 0, "aaaabbbbccccdddd")]
+ [InlineData("aaaabbbbccccdddd", 'a', '!', 16, 0, "aaaabbbbccccdddd")]
+ [InlineData("aaaabbbbccccdddd", 'e', '!', 0, 16, "aaaabbbbccccdddd")]
+ [InlineData("a\U0001F600b\U0001F600c\U0001F600", 0x0001F600, '!', 0, 9, "a!b!c!")]
+ public static void Replace_Rune(string value, int oldRuneAsInt, int newRuneAsInt, int startIndex, int count, string expected)
+ {
+ var oldRune = new Rune(oldRuneAsInt);
+ var newRune = new Rune(newRuneAsInt);
+
+ StringBuilder builder;
+ if (startIndex == 0 && count == value.Length)
+ {
+ // Use Replace(Rune, Rune)
+ builder = new StringBuilder(value);
+ builder.Replace(oldRune, newRune);
+ Assert.Equal(expected, builder.ToString());
+ }
+ // Use Replace(Rune, Rune, int, int)
+ builder = new StringBuilder(value);
+ builder.Replace(oldRune, newRune, startIndex, count);
+ Assert.Equal(expected, builder.ToString());
+ }
+
+ [Fact]
+ public static void Replace_Rune_StringBuilderWithMultipleChunks()
+ {
+ StringBuilder builder = StringBuilderWithMultipleChunks();
+ builder.Replace(new Rune('a'), new Rune('b'), 0, builder.Length);
+ Assert.Equal(new string('b', builder.Length), builder.ToString());
+ }
+
+ [Fact]
+ public static void Replace_Rune_Invalid()
+ {
+ var builder = new StringBuilder("Hello");
+ AssertExtensions.Throws("startIndex", () => builder.Replace(new Rune('a'), new Rune('b'), -1, 0)); // Start index < 0
+ AssertExtensions.Throws("count", () => builder.Replace(new Rune('a'), new Rune('b'), 0, -1)); // Count < 0
+
+ AssertExtensions.Throws("startIndex", () => builder.Replace(new Rune('a'), new Rune('b'), 6, 0)); // Count + start index > builder.Length
+ AssertExtensions.Throws("count", () => builder.Replace(new Rune('a'), new Rune('b'), 5, 1)); // Count + start index > builder.Length
+ AssertExtensions.Throws("count", () => builder.Replace(new Rune('a'), new Rune('b'), 4, 2)); // Count + start index > builder.Length
+ }
+
[Theory]
[InlineData("Hello", 0, 5, "Hello")]
[InlineData("Hello", 2, 3, "llo")]
@@ -2181,6 +2316,38 @@ public static void Equals_String(StringBuilder sb1, string value, bool expected)
Assert.Equal(expected, sb1.Equals(value.AsSpan()));
}
+ [Theory]
+ [InlineData(new char[0], new int[0])] // empty
+ [InlineData(new char[] { 'x', 'y', 'z' }, new int[] { 'x', 'y', 'z' })]
+ [InlineData(new char[] { 'x', '\uD86D', '\uDF54', 'y' }, new int[] { 'x', 0x2B754, 'y' })] // valid surrogate pair
+ [InlineData(new char[] { 'x', '\uD86D', 'y' }, new int[] { 'x', 0xFFFD, 'y' })] // standalone high surrogate
+ [InlineData(new char[] { 'x', '\uDF54', 'y' }, new int[] { 'x', 0xFFFD, 'y' })] // standalone low surrogate
+ [InlineData(new char[] { 'x', '\uD86D' }, new int[] { 'x', 0xFFFD })] // standalone high surrogate at end of string
+ [InlineData(new char[] { 'x', '\uDF54' }, new int[] { 'x', 0xFFFD })] // standalone low surrogate at end of string
+ [InlineData(new char[] { 'x', '\uD86D', '\uD86D', 'y' }, new int[] { 'x', 0xFFFD, 0xFFFD, 'y' })] // two high surrogates should be two replacement chars
+ [InlineData(new char[] { 'x', '\uFFFD', 'y' }, new int[] { 'x', 0xFFFD, 'y' })] // literal U+FFFD
+ public static void EnumerateRunes(char[] chars, int[] expected)
+ {
+ // Test data is smuggled as char[] instead of straight-up string since the test framework
+ // doesn't like invalid UTF-16 literals.
+
+ StringBuilder asStringBuilder = new StringBuilder(new string(chars));
+
+ // First, use a straight-up foreach keyword to ensure pattern matching works as expected
+
+ List enumeratedScalarValues = new List();
+ foreach (Rune rune in asStringBuilder.EnumerateRunes())
+ {
+ enumeratedScalarValues.Add(rune.Value);
+ }
+ Assert.Equal(expected, enumeratedScalarValues.ToArray());
+
+ // Then use LINQ to ensure IEnumerator<...> works as expected
+
+ int[] enumeratedValues = asStringBuilder.EnumerateRunes().Select(r => r.Value).ToArray();
+ Assert.Equal(expected, enumeratedValues);
+ }
+
[Fact]
public static void ForEach()
{