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 arg) { } public override Task WriteAsync(char value) => Task.CompletedTask; + public override Task WriteAsync(Rune value) => Task.CompletedTask; public override Task WriteAsync(string? value) => Task.CompletedTask; public override Task WriteAsync(StringBuilder? value, CancellationToken cancellationToken = default) => Task.CompletedTask; public override Task WriteAsync(char[] buffer, int index, int count) => Task.CompletedTask; public override Task WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) => Task.CompletedTask; public override void WriteLine() { } public override void WriteLine(char value) { } + public override void WriteLine(Rune value) { } public override void WriteLine(char[]? buffer) { } public override void WriteLine(char[] buffer, int index, int count) { } public override void WriteLine(ReadOnlySpan buffer) { } @@ -748,6 +832,7 @@ public override void WriteLine([StringSyntax(StringSyntaxAttribute.CompositeForm public override void WriteLine([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, params object?[] arg) { } public override void WriteLine([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, params ReadOnlySpan arg) { } public override Task WriteLineAsync(char value) => Task.CompletedTask; + public override Task WriteLineAsync(Rune value) => Task.CompletedTask; public override Task WriteLineAsync(string? value) => Task.CompletedTask; public override Task WriteLineAsync(StringBuilder? value, CancellationToken cancellationToken = default) => Task.CompletedTask; public override Task WriteLineAsync(char[] buffer, int index, int count) => Task.CompletedTask; @@ -805,6 +890,9 @@ protected override void Dispose(bool disposing) [MethodImpl(MethodImplOptions.Synchronized)] public override void Write(char value) => _out.Write(value); + [MethodImpl(MethodImplOptions.Synchronized)] + public override void Write(Rune value) => _out.Write(value); + [MethodImpl(MethodImplOptions.Synchronized)] public override void Write(char[]? buffer) => _out.Write(buffer); @@ -868,6 +956,9 @@ protected override void Dispose(bool disposing) [MethodImpl(MethodImplOptions.Synchronized)] public override void WriteLine(char value) => _out.WriteLine(value); + [MethodImpl(MethodImplOptions.Synchronized)] + public override void WriteLine(Rune value) => _out.WriteLine(value); + [MethodImpl(MethodImplOptions.Synchronized)] public override void WriteLine(decimal value) => _out.WriteLine(value); @@ -943,6 +1034,13 @@ public override Task WriteAsync(char value) return Task.CompletedTask; } + [MethodImpl(MethodImplOptions.Synchronized)] + public override Task WriteAsync(Rune value) + { + Write(value); + return Task.CompletedTask; + } + [MethodImpl(MethodImplOptions.Synchronized)] public override Task WriteAsync(string? value) { @@ -1000,6 +1098,13 @@ public override Task WriteLineAsync(char value) return Task.CompletedTask; } + [MethodImpl(MethodImplOptions.Synchronized)] + public override Task WriteLineAsync(Rune value) + { + WriteLine(value); + return Task.CompletedTask; + } + [MethodImpl(MethodImplOptions.Synchronized)] public override Task WriteLineAsync() { diff --git a/src/libraries/System.Private.CoreLib/src/System/String.Comparison.cs b/src/libraries/System.Private.CoreLib/src/System/String.Comparison.cs index 458aa815cd6560..3a927ed2cfbb1a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/String.Comparison.cs +++ b/src/libraries/System.Private.CoreLib/src/System/String.Comparison.cs @@ -9,6 +9,7 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Text; using System.Text.Unicode; namespace System @@ -589,6 +590,44 @@ public bool EndsWith(char value) return ((uint)lastPos < (uint)Length) && this[lastPos] == value; } + /// + /// Determines whether the end of this string instance matches the specified character. + /// + /// The character to compare to the character at the end of this instance. + /// One of the enumeration values that specifies the rules to use in the comparison. + /// if matches the end of this instance; otherwise, . + public bool EndsWith(char value, StringComparison comparisonType) + { + // Convert value to span + ReadOnlySpan valueChars = [value]; + + return this.EndsWith(valueChars, comparisonType); + } + + /// + /// Determines whether the end of this string instance matches the specified rune using an ordinal comparison. + /// + /// The character to compare to the character at the end of this instance. + /// if matches the end of this instance; otherwise, . + public bool EndsWith(Rune value) + { + return EndsWith(value, StringComparison.Ordinal); + } + + /// + /// Determines whether the end of this string instance matches the specified rune when compared using the specified comparison option. + /// + /// The character to compare to the character at the end of this instance. + /// One of the enumeration values that specifies the rules to use in the comparison. + /// if matches the end of this instance; otherwise, . + public bool EndsWith(Rune value, StringComparison comparisonType) + { + // Convert value to span + ReadOnlySpan valueChars = value.AsSpan(stackalloc char[Rune.MaxUtf16CharsPerRune]); + + return this.EndsWith(valueChars, comparisonType); + } + // Determines whether two strings match. public override bool Equals([NotNullWhen(true)] object? obj) { @@ -1162,6 +1201,44 @@ public bool StartsWith(char value) return Length != 0 && _firstChar == value; } + /// + /// Determines whether the beginning of this string instance matches the specified character when compared using the specified comparison option. + /// + /// The character to compare. + /// One of the enumeration values that determines how this string and are compared. + /// if value matches the beginning of this string; otherwise, . + public bool StartsWith(char value, StringComparison comparisonType) + { + // Convert value to span + ReadOnlySpan valueChars = [value]; + + return this.StartsWith(valueChars, comparisonType); + } + + /// + /// Determines whether the beginning of this string instance matches the specified rune using an ordinal comparison. + /// + /// The rune to compare. + /// if value matches the beginning of this string; otherwise, . + public bool StartsWith(Rune value) + { + return StartsWith(value, StringComparison.Ordinal); + } + + /// + /// Determines whether the beginning of this string instance matches the specified rune when compared using the specified comparison option. + /// + /// The rune to compare. + /// One of the enumeration values that determines how this string and are compared. + /// if value matches the beginning of this string; otherwise, . + public bool StartsWith(Rune value, StringComparison comparisonType) + { + // Convert value to span + ReadOnlySpan valueChars = value.AsSpan(stackalloc char[Rune.MaxUtf16CharsPerRune]); + + return this.StartsWith(valueChars, comparisonType); + } + internal static void CheckStringComparison(StringComparison comparisonType) { // Single comparison to check if comparisonType is within [CurrentCulture .. OrdinalIgnoreCase] diff --git a/src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs b/src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs index 46738b2c59bafe..774f5c49d46e2f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs +++ b/src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs @@ -1447,6 +1447,29 @@ private string ReplaceHelper(int oldValueLength, string newValue, ReadOnlySpan + /// Returns a new string in which all occurrences of a specified Unicode rune in this instance are replaced with another specified Unicode rune using an ordinal comparison. + /// + /// 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() {