Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/libraries/System.Private.CoreLib/src/System/Char.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,13 @@ public bool Equals(char obj)
return m_value == obj;
}

public bool Equals(char right, StringComparison comparisonType)
{
ReadOnlySpan<char> leftCharsSlice = [this];
ReadOnlySpan<char> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,41 @@ internal static char ToUpperAsciiInvariant(char c)
return c;
}

public Rune ToLower(Rune r)
{
// Convert rune to span
Span<char> chars = stackalloc char[2];
int charsWritten = r.EncodeToUtf16(chars);
ReadOnlySpan<char> charsSlice = chars[..charsWritten];

// Change span to lower and convert to rune
if (charsSlice.Length == 2)
{
return new Rune(ToLower(charsSlice[0]), ToLower(charsSlice[1]));
}
else
{
return new Rune(ToLower(charsSlice[0]));
}
}
public Rune ToUpper(Rune r)
{
// Convert rune to span
Span<char> chars = stackalloc char[2];
int charsWritten = r.EncodeToUtf16(chars);
ReadOnlySpan<char> charsSlice = chars[..charsWritten];

// Change span to upper and convert to rune
if (charsSlice.Length == 2)
{
return new Rune(ToUpper(charsSlice[0]), ToUpper(charsSlice[1]));
}
else
{
return new Rune(ToUpper(charsSlice[0]));
}
}

private bool IsAsciiCasingSameAsInvariant
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
Expand Down
68 changes: 68 additions & 0 deletions src/libraries/System.Private.CoreLib/src/System/IO/TextWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,74 @@ public virtual void WriteLine([StringSyntax(StringSyntaxAttribute.CompositeForma
WriteLine(string.Format(FormatProvider, format, arg));
}

public virtual void Write(Rune value)
{
// Convert value to span
Span<char> chars = stackalloc char[2];
int charsWritten = value.EncodeToUtf16(chars);
ReadOnlySpan<char> charsSlice = chars[..charsWritten];

// Write span
Write(charsSlice);
}

public virtual Task WriteAsync(Rune value)
{
// Convert value to span
Span<char> chars = stackalloc char[2];
int charsWritten = value.EncodeToUtf16(chars);
ReadOnlySpan<char> charsSlice = chars[..charsWritten];

// Write chars individually (can't use span with async)
if (charsSlice.Length == 2)
{
async Task WriteAsyncPair(char highSurrogate, char lowSurrogate)
{
await WriteAsync(highSurrogate).ConfigureAwait(false);
await WriteAsync(lowSurrogate).ConfigureAwait(false);
}
return WriteAsyncPair(charsSlice[0], charsSlice[1]);
}
else
{
return WriteAsync(charsSlice[0]);
}
}

public virtual void WriteLine(Rune value)
{
// Convert value to span
Span<char> chars = stackalloc char[2];
int charsWritten = value.EncodeToUtf16(chars);
ReadOnlySpan<char> charsSlice = chars[..charsWritten];

// Write span
WriteLine(charsSlice);
}

public virtual Task WriteLineAsync(Rune value)
{
// Convert value to span
Span<char> chars = stackalloc char[2];
int charsWritten = value.EncodeToUtf16(chars);
ReadOnlySpan<char> charsSlice = chars[..charsWritten];

// Write chars individually (can't use span with async)
if (charsSlice.Length == 2)
{
async Task WriteLineAsyncPair(char highSurrogate, char lowSurrogate)
{
await WriteAsync(highSurrogate).ConfigureAwait(false);
await WriteLineAsync(lowSurrogate).ConfigureAwait(false);
}
return WriteLineAsyncPair(charsSlice[0], charsSlice[1]);
}
else
{
return WriteLineAsync(charsSlice[0]);
}
}

#region Task based Async APIs
public virtual Task WriteAsync(char value) =>
Task.Factory.StartNew(static state =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Unicode;

namespace System
Expand Down Expand Up @@ -609,6 +610,29 @@ public bool EndsWith(char value)
return ((uint)lastPos < (uint)Length) && this[lastPos] == value;
}

public bool EndsWith(Rune value)
{
return EndsWith(value, StringComparison.Ordinal);
}

public bool EndsWith(Rune value, StringComparison comparisonType)
{
if (Rune.DecodeLastFromUtf16(this, out Rune result, out _) is OperationStatus.Done)
{
return result.Equals(value, comparisonType);
}
return false;
}

public bool EndsWith(char value, StringComparison comparisonType)
{
if (Length == 0)
{
return false;
}
return this[^1].Equals(value, comparisonType);
}

// Determines whether two strings match.
public override bool Equals([NotNullWhen(true)] object? obj)
{
Expand Down Expand Up @@ -1182,6 +1206,29 @@ public bool StartsWith(char value)
return Length != 0 && _firstChar == value;
}

public bool StartsWith(Rune value)
{
return StartsWith(value, StringComparison.Ordinal);
}

public bool StartsWith(Rune value, StringComparison comparisonType)
{
if (Rune.DecodeFromUtf16(this, out Rune result, out _) is OperationStatus.Done)
{
return result.Equals(value, comparisonType);
}
return false;
}

public bool StartsWith(char value, StringComparison comparisonType)
{
if (this.Length == 0)
{
return false;
}
return this[0].Equals(value, comparisonType);
}

internal static void CheckStringComparison(StringComparison comparisonType)
{
// Single comparison to check if comparisonType is within [CurrentCulture .. OrdinalIgnoreCase]
Expand Down
156 changes: 156 additions & 0 deletions src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1447,6 +1447,35 @@ private string ReplaceHelper(int oldValueLength, string newValue, ReadOnlySpan<i
return dst;
}

public string Replace(Rune oldRune, Rune newRune)
{
return Replace(oldRune, newRune, StringComparison.Ordinal);
}

public string Replace(Rune oldRune, Rune newRune, StringComparison stringComparison)
{
if (Length == 0)
{
return this;
}

Span<char> oldChars = stackalloc char[2];
int oldCharsWritten = oldRune.EncodeToUtf16(oldChars);
ReadOnlySpan<char> oldCharsSlice = oldChars[..oldCharsWritten];

Span<char> newChars = stackalloc char[2];
int newCharsWritten = newRune.EncodeToUtf16(newChars);
ReadOnlySpan<char> newCharsSlice = newChars[..newCharsWritten];

// TODO:
// We need an overload for string.Replace(ReadOnlySpan<char> oldValue, ReadOnlySpan<char> newValue) to avoid allocations.
// The overload is already available internally through ReplaceCore but not exposed publicly.
// When added, this method can look like the StringBuilder.Replace(Rune, Rune).
// For now we'll just convert the runes to strings.

return Replace(oldRune.ToString(), newRune.ToString(), stringComparison); // return Replace(oldCharsSlice, newCharsSlice, stringComparison);
}

/// <summary>
/// Replaces all newline sequences in the current string with <see cref="Environment.NewLine"/>.
/// </summary>
Expand Down Expand Up @@ -1755,6 +1784,30 @@ public string[] Split(string[]? separator, int count, StringSplitOptions options
return SplitInternal(null, separator, count, options);
}

public string[] Split(Rune separator, StringSplitOptions options = StringSplitOptions.None)
{
return Split(separator, int.MaxValue, options);
}

public string[] Split(Rune separator, int count, StringSplitOptions options = StringSplitOptions.None)
{
if (Length == 0)
{
return [];
}

Span<char> separatorChars = stackalloc char[2];
int separatorCharsWritten = separator.EncodeToUtf16(separatorChars);
ReadOnlySpan<char> separatorCharsSlice = separatorChars[..separatorCharsWritten];

// TODO:
// We need an overload for string.Split(ReadOnlySpan<char>, int, StringSplitOptions) to avoid copying the separator to a string.
// The overload is already available internally through SplitInternal but not exposed publicly.
// For now we'll just convert the separator to a string.

return Split(separator.ToString(), count, options); // return Split(separatorCharsSlice, count, options);
}

private string[] SplitInternal(string? separator, string?[]? separators, int count, StringSplitOptions options)
{
ArgumentOutOfRangeException.ThrowIfNegative(count);
Expand Down Expand Up @@ -2379,6 +2432,109 @@ public unsafe string Trim(params ReadOnlySpan<char> trimChars)
}
}

public string Trim(Rune trimRune)
{
if (Length == 0)
{
return this;
}

// Convert trimRune to span
Span<char> trimChars = stackalloc char[2];
int trimCharsWritten = trimRune.EncodeToUtf16(trimChars);
ReadOnlySpan<char> trimCharsSlice = trimChars[..trimCharsWritten];

// Trim start
int index = 0;
while (true)
{
if (index > Length)
{
return string.Empty;
}
if (!this.AsSpan(index).StartsWith(trimCharsSlice))
{
break;
}
index += trimCharsSlice.Length;
}

// Trim end
int endIndex = Length - 1;
while (true)
{
if (endIndex < index)
{
return string.Empty;
}
if (!this.AsSpan(..endIndex).EndsWith(trimCharsSlice))
{
break;
}
endIndex -= trimCharsSlice.Length;
}

return this[index..endIndex];
}

public string TrimStart(Rune trimRune)
{
if (Length == 0)
{
return this;
}

// Convert trimRune to span
Span<char> trimChars = stackalloc char[2];
int trimCharsWritten = trimRune.EncodeToUtf16(trimChars);
ReadOnlySpan<char> trimCharsSlice = trimChars[..trimCharsWritten];

// Trim start
int index = 0;
while (true)
{
if (index > Length)
{
return string.Empty;
}
if (!this.AsSpan(index).StartsWith(trimCharsSlice))
{
break;
}
index += trimCharsSlice.Length;
}
return this[index..];
}

public string TrimEnd(Rune trimRune)
{
if (Length == 0)
{
return this;
}

// Convert trimRune to span
Span<char> trimChars = stackalloc char[2];
int trimCharsWritten = trimRune.EncodeToUtf16(trimChars);
ReadOnlySpan<char> trimCharsSlice = trimChars[..trimCharsWritten];

// Trim end
int endIndex = Length - 1;
while (true)
{
if (endIndex < 0)
{
return string.Empty;
}
if (!this.AsSpan(..endIndex).EndsWith(trimCharsSlice))
{
break;
}
endIndex -= trimCharsSlice.Length;
}
return this[..endIndex];
}

// Removes a set of characters from the beginning of this string.
public string TrimStart() => TrimWhiteSpaceHelper(TrimType.Head);

Expand Down
Loading
Loading