Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 3628544

Browse files
TheMaximumMax Klaversmaadamsitnik
authored
StringBuilder.Replace with ReadOnlySpan<char> (#93938)
Fixes #77837 Co-authored-by: Max Klaversma <[email protected]> Co-authored-by: Adam Sitnik <[email protected]>
1 parent ddd663d commit 3628544

File tree

6 files changed

+163
-89
lines changed

6 files changed

+163
-89
lines changed

src/libraries/System.Private.CoreLib/src/System/Text/StringBuilder.cs

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1841,6 +1841,17 @@ private StringBuilder AppendFormat<TArg0, TArg1, TArg2>(IFormatProvider? provide
18411841
/// </remarks>
18421842
public StringBuilder Replace(string oldValue, string? newValue) => Replace(oldValue, newValue, 0, Length);
18431843

1844+
/// <summary>
1845+
/// Replaces all instances of one read-only character span with another in this builder.
1846+
/// </summary>
1847+
/// <param name="oldValue">The read-only character span to replace.</param>
1848+
/// <param name="newValue">The read-only character span to replace <paramref name="oldValue"/> with.</param>
1849+
/// <remarks>
1850+
/// If <paramref name="newValue"/> is empty, instances of <paramref name="oldValue"/>
1851+
/// are removed from this builder.
1852+
/// </remarks>
1853+
public StringBuilder Replace(ReadOnlySpan<char> oldValue, ReadOnlySpan<char> newValue) => Replace(oldValue, newValue, 0, Length);
1854+
18441855
/// <summary>
18451856
/// Determines if the contents of this builder are equal to the contents of another builder.
18461857
/// </summary>
@@ -1950,6 +1961,23 @@ public bool Equals(ReadOnlySpan<char> span)
19501961
/// are removed from this builder.
19511962
/// </remarks>
19521963
public StringBuilder Replace(string oldValue, string? newValue, int startIndex, int count)
1964+
{
1965+
ArgumentException.ThrowIfNullOrEmpty(oldValue);
1966+
return Replace(oldValue.AsSpan(), newValue.AsSpan(), startIndex, count);
1967+
}
1968+
1969+
/// <summary>
1970+
/// Replaces all instances of one read-only character span with another in part of this builder.
1971+
/// </summary>
1972+
/// <param name="oldValue">The read-only character span to replace.</param>
1973+
/// <param name="newValue">The read-only character span to replace <paramref name="oldValue"/> with.</param>
1974+
/// <param name="startIndex">The index to start in this builder.</param>
1975+
/// <param name="count">The number of characters to read in this builder.</param>
1976+
/// <remarks>
1977+
/// If <paramref name="newValue"/> is empty, instances of <paramref name="oldValue"/>
1978+
/// are removed from this builder.
1979+
/// </remarks>
1980+
public StringBuilder Replace(ReadOnlySpan<char> oldValue, ReadOnlySpan<char> newValue, int startIndex, int count)
19531981
{
19541982
int currentLength = Length;
19551983
if ((uint)startIndex > (uint)currentLength)
@@ -1960,9 +1988,10 @@ public StringBuilder Replace(string oldValue, string? newValue, int startIndex,
19601988
{
19611989
throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_IndexMustBeLessOrEqual);
19621990
}
1963-
ArgumentException.ThrowIfNullOrEmpty(oldValue);
1964-
1965-
newValue ??= string.Empty;
1991+
if (oldValue.Length == 0)
1992+
{
1993+
throw new ArgumentException(SR.Arg_EmptySpan, nameof(oldValue));
1994+
}
19661995

19671996
var replacements = new ValueListBuilder<int>(stackalloc int[128]); // A list of replacement positions in a chunk to apply
19681997

@@ -2225,7 +2254,7 @@ private void Insert(int index, ref char value, int valueCount)
22252254
/// <remarks>
22262255
/// This routine is very efficient because it does replacements in bulk.
22272256
/// </remarks>
2228-
private void ReplaceAllInChunk(ReadOnlySpan<int> replacements, StringBuilder sourceChunk, int removeCount, string value)
2257+
private void ReplaceAllInChunk(ReadOnlySpan<int> replacements, StringBuilder sourceChunk, int removeCount, ReadOnlySpan<char> value)
22292258
{
22302259
Debug.Assert(!replacements.IsEmpty);
22312260

@@ -2251,7 +2280,7 @@ private void ReplaceAllInChunk(ReadOnlySpan<int> replacements, StringBuilder sou
22512280
while (true)
22522281
{
22532282
// Copy in the new string for the ith replacement
2254-
ReplaceInPlaceAtChunk(ref targetChunk!, ref targetIndexInChunk, ref value.GetRawStringData(), value.Length);
2283+
ReplaceInPlaceAtChunk(ref targetChunk!, ref targetIndexInChunk, ref MemoryMarshal.GetReference<char>(value), value.Length);
22552284
int gapStart = replacements[i] + removeCount;
22562285
i++;
22572286
if ((uint)i >= replacements.Length)
@@ -2289,7 +2318,7 @@ private void ReplaceAllInChunk(ReadOnlySpan<int> replacements, StringBuilder sou
22892318
/// <param name="indexInChunk">The index in <paramref name="chunk"/> at which the substring starts.</param>
22902319
/// <param name="count">The logical count of the substring.</param>
22912320
/// <param name="value">The prefix.</param>
2292-
private bool StartsWith(StringBuilder chunk, int indexInChunk, int count, string value)
2321+
private bool StartsWith(StringBuilder chunk, int indexInChunk, int count, ReadOnlySpan<char> value)
22932322
{
22942323
for (int i = 0; i < value.Length; i++)
22952324
{

src/libraries/System.Runtime/ref/System.Runtime.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14936,6 +14936,8 @@ public void CopyTo(int sourceIndex, System.Span<char> destination, int count) {
1493614936
public System.Text.StringBuilder Replace(char oldChar, char newChar, int startIndex, int count) { throw null; }
1493714937
public System.Text.StringBuilder Replace(string oldValue, string? newValue) { throw null; }
1493814938
public System.Text.StringBuilder Replace(string oldValue, string? newValue, int startIndex, int count) { throw null; }
14939+
public System.Text.StringBuilder Replace(ReadOnlySpan<char> oldValue, ReadOnlySpan<char> newValue) { throw null; }
14940+
public System.Text.StringBuilder Replace(ReadOnlySpan<char> oldValue, ReadOnlySpan<char> newValue, int startIndex, int count) { throw null; }
1493914941
void System.Runtime.Serialization.ISerializable.GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { }
1494014942
public override string ToString() { throw null; }
1494114943
public string ToString(int startIndex, int length) { throw null; }

src/libraries/System.Runtime/tests/System.Runtime.Tests/NlsTests/System.Runtime.Nls.Tests.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
Link="System\Text\RuneTests.TestData.cs" />
3131
<Compile Include="..\System\Text\StringBuilderTests.cs"
3232
Link="System\Text\StringBuilderTests.cs" />
33+
<Compile Include="..\System\Text\StringBuilderReplaceTests.cs"
34+
Link="System\Text\StringBuilderReplaceTests.cs" />
3335
<Compile Include="..\System\Uri.CreateStringTests.cs"
3436
Link="System\Uri.CreateStringTests.cs" />
3537
<Compile Include="..\System\Uri.CreateUriTests.cs"

src/libraries/System.Runtime/tests/System.Runtime.Tests/System.Runtime.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@
153153
<Compile Include="System\StringTests.cs" />
154154
<Compile Include="System\SystemExceptionTests.cs" />
155155
<Compile Include="System\Text\CompositeFormatTests.cs" />
156+
<Compile Include="System\Text\StringBuilderReplaceTests.cs" />
156157
<Compile Include="System\TimeOnlyTests.cs" />
157158
<Compile Include="System\TimeoutExceptionTests.cs" />
158159
<Compile Include="System\TimeSpanTests.cs" />
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Xunit;
5+
6+
namespace System.Text.Tests
7+
{
8+
public abstract class StringBuilderReplaceTests
9+
{
10+
[Theory]
11+
[InlineData("", "a", "!", 0, 0, "")]
12+
[InlineData("aaaabbbbccccdddd", "a", "!", 0, 16, "!!!!bbbbccccdddd")]
13+
[InlineData("aaaabbbbccccdddd", "a", "!", 2, 3, "aa!!bbbbccccdddd")]
14+
[InlineData("aaaabbbbccccdddd", "a", "!", 4, 1, "aaaabbbbccccdddd")]
15+
[InlineData("aaaabbbbccccdddd", "aab", "!", 2, 2, "aaaabbbbccccdddd")]
16+
[InlineData("aaaabbbbccccdddd", "aab", "!", 2, 3, "aa!bbbccccdddd")]
17+
[InlineData("aaaabbbbccccdddd", "aa", "!", 0, 16, "!!bbbbccccdddd")]
18+
[InlineData("aaaabbbbccccdddd", "aa", "$!", 0, 16, "$!$!bbbbccccdddd")]
19+
[InlineData("aaaabbbbccccdddd", "aa", "$!$", 0, 16, "$!$$!$bbbbccccdddd")]
20+
[InlineData("aaaabbbbccccdddd", "aaaa", "!", 0, 16, "!bbbbccccdddd")]
21+
[InlineData("aaaabbbbccccdddd", "aaaa", "$!", 0, 16, "$!bbbbccccdddd")]
22+
[InlineData("aaaabbbbccccdddd", "a", "", 0, 16, "bbbbccccdddd")]
23+
[InlineData("aaaabbbbccccdddd", "b", null, 0, 16, "aaaaccccdddd")]
24+
[InlineData("aaaabbbbccccdddd", "aaaabbbbccccdddd", "", 0, 16, "")]
25+
[InlineData("aaaabbbbccccdddd", "aaaabbbbccccdddd", "", 16, 0, "aaaabbbbccccdddd")]
26+
[InlineData("aaaabbbbccccdddd", "aaaabbbbccccdddde", "", 0, 16, "aaaabbbbccccdddd")]
27+
[InlineData("aaaaaaaaaaaaaaaa", "a", "b", 0, 16, "bbbbbbbbbbbbbbbb")]
28+
public void Replace_StringBuilder(string value, string oldValue, string newValue, int startIndex, int count, string expected)
29+
{
30+
StringBuilder builder;
31+
if (startIndex == 0 && count == value.Length)
32+
{
33+
// Use Replace(string, string) / Replace(ReadOnlySpan<char>, ReadOnlySpan<char>)
34+
builder = new StringBuilder(value);
35+
Replace(builder, oldValue, newValue);
36+
Assert.Equal(expected, builder.ToString());
37+
}
38+
39+
// Use Replace(string, string, int, int) / Replace(ReadOnlySpan<char>, ReadOnlySpan<char>, int, int)
40+
builder = new StringBuilder(value);
41+
Replace(builder, oldValue, newValue, startIndex, count);
42+
Assert.Equal(expected, builder.ToString());
43+
}
44+
45+
[Fact]
46+
public void Replace_StringBuilderWithMultipleChunks()
47+
{
48+
StringBuilder builder = StringBuilderTests.StringBuilderWithMultipleChunks();
49+
Replace(builder, "a", "b", builder.Length - 10, 10);
50+
Assert.Equal(new string('a', builder.Length - 10) + new string('b', 10), builder.ToString());
51+
}
52+
53+
[Fact]
54+
public void Replace_StringBuilderWithMultipleChunks_WholeString()
55+
{
56+
StringBuilder builder = StringBuilderTests.StringBuilderWithMultipleChunks();
57+
Replace(builder, builder.ToString(), "");
58+
Assert.Same(string.Empty, builder.ToString());
59+
}
60+
61+
[Fact]
62+
public void Replace_StringBuilderWithMultipleChunks_LongString()
63+
{
64+
StringBuilder builder = StringBuilderTests.StringBuilderWithMultipleChunks();
65+
Replace(builder, builder.ToString() + "b", "");
66+
Assert.Equal(StringBuilderTests.s_chunkSplitSource, builder.ToString());
67+
}
68+
69+
[Fact]
70+
public void Replace_Invalid()
71+
{
72+
var builder = new StringBuilder(0, 5);
73+
builder.Append("Hello");
74+
75+
AssertExtensions.Throws<ArgumentException>("oldValue", () => Replace(builder, "", "a")); // Old value is empty
76+
AssertExtensions.Throws<ArgumentException>("oldValue", () => Replace(builder, "", "a", 0, 0)); // Old value is empty
77+
78+
AssertExtensions.Throws<ArgumentOutOfRangeException>("requiredLength", () => Replace(builder, "o", "oo")); // New length > builder.MaxCapacity
79+
AssertExtensions.Throws<ArgumentOutOfRangeException>("requiredLength", () => Replace(builder, "o", "oo", 0, 5)); // New length > builder.MaxCapacity
80+
81+
AssertExtensions.Throws<ArgumentOutOfRangeException>("startIndex", () => Replace(builder, "a", "b", -1, 0)); // Start index < 0
82+
AssertExtensions.Throws<ArgumentOutOfRangeException>("count", () => Replace(builder, "a", "b", 0, -1)); // Count < 0
83+
84+
AssertExtensions.Throws<ArgumentOutOfRangeException>("startIndex", () => Replace(builder, "a", "b", 6, 0)); // Count + start index > builder.Length
85+
AssertExtensions.Throws<ArgumentOutOfRangeException>("count", () => Replace(builder, "a", "b", 5, 1)); // Count + start index > builder.Length
86+
AssertExtensions.Throws<ArgumentOutOfRangeException>("count", () => Replace(builder, "a", "b", 4, 2)); // Count + start index > builder.Length
87+
}
88+
89+
protected abstract StringBuilder Replace(StringBuilder builder, string oldValue, string newValue);
90+
91+
protected abstract StringBuilder Replace(StringBuilder builder, string oldValue, string newValue, int startIndex, int count);
92+
}
93+
94+
public class StringBuilderReplaceTests_String : StringBuilderReplaceTests
95+
{
96+
[Fact]
97+
public void Replace_String_Invalid()
98+
{
99+
var builder = new StringBuilder(0, 5);
100+
builder.Append("Hello");
101+
102+
AssertExtensions.Throws<ArgumentNullException>("oldValue", () => Replace(builder, null, "")); // Old value is null
103+
AssertExtensions.Throws<ArgumentNullException>("oldValue", () => Replace(builder, null, "a", 0, 0)); // Old value is null
104+
}
105+
106+
protected override StringBuilder Replace(StringBuilder builder, string oldValue, string newValue)
107+
=> builder.Replace(oldValue, newValue);
108+
109+
protected override StringBuilder Replace(StringBuilder builder, string oldValue, string newValue, int startIndex, int count)
110+
=> builder.Replace(oldValue, newValue, startIndex, count);
111+
}
112+
113+
public class StringBuilderReplaceTests_Span : StringBuilderReplaceTests
114+
{
115+
protected override StringBuilder Replace(StringBuilder builder, string oldValue, string newValue)
116+
=> builder.Replace(oldValue.AsSpan(), newValue.AsSpan());
117+
118+
protected override StringBuilder Replace(StringBuilder builder, string oldValue, string newValue, int startIndex, int count)
119+
=> builder.Replace(oldValue.AsSpan(), newValue.AsSpan(), startIndex, count);
120+
}
121+
}

src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Text/StringBuilderTests.cs

Lines changed: 2 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ namespace System.Text.Tests
1313
{
1414
public partial class StringBuilderTests
1515
{
16-
private static readonly string s_chunkSplitSource = new string('a', 30);
1716
private static readonly string s_noCapacityParamName = "valueCount";
1817

19-
private static StringBuilder StringBuilderWithMultipleChunks() => new StringBuilder(20).Append(s_chunkSplitSource);
18+
internal static readonly string s_chunkSplitSource = new string('a', 30);
19+
internal static StringBuilder StringBuilderWithMultipleChunks() => new StringBuilder(20).Append(s_chunkSplitSource);
2020

2121
[Fact]
2222
public static void Ctor_Empty()
@@ -1694,87 +1694,6 @@ public static void Replace_Char_Invalid()
16941694
AssertExtensions.Throws<ArgumentOutOfRangeException>("count", () => builder.Replace('a', 'b', 4, 2)); // Count + start index > builder.Length
16951695
}
16961696

1697-
[Theory]
1698-
[InlineData("", "a", "!", 0, 0, "")]
1699-
[InlineData("aaaabbbbccccdddd", "a", "!", 0, 16, "!!!!bbbbccccdddd")]
1700-
[InlineData("aaaabbbbccccdddd", "a", "!", 2, 3, "aa!!bbbbccccdddd")]
1701-
[InlineData("aaaabbbbccccdddd", "a", "!", 4, 1, "aaaabbbbccccdddd")]
1702-
[InlineData("aaaabbbbccccdddd", "aab", "!", 2, 2, "aaaabbbbccccdddd")]
1703-
[InlineData("aaaabbbbccccdddd", "aab", "!", 2, 3, "aa!bbbccccdddd")]
1704-
[InlineData("aaaabbbbccccdddd", "aa", "!", 0, 16, "!!bbbbccccdddd")]
1705-
[InlineData("aaaabbbbccccdddd", "aa", "$!", 0, 16, "$!$!bbbbccccdddd")]
1706-
[InlineData("aaaabbbbccccdddd", "aa", "$!$", 0, 16, "$!$$!$bbbbccccdddd")]
1707-
[InlineData("aaaabbbbccccdddd", "aaaa", "!", 0, 16, "!bbbbccccdddd")]
1708-
[InlineData("aaaabbbbccccdddd", "aaaa", "$!", 0, 16, "$!bbbbccccdddd")]
1709-
[InlineData("aaaabbbbccccdddd", "a", "", 0, 16, "bbbbccccdddd")]
1710-
[InlineData("aaaabbbbccccdddd", "b", null, 0, 16, "aaaaccccdddd")]
1711-
[InlineData("aaaabbbbccccdddd", "aaaabbbbccccdddd", "", 0, 16, "")]
1712-
[InlineData("aaaabbbbccccdddd", "aaaabbbbccccdddd", "", 16, 0, "aaaabbbbccccdddd")]
1713-
[InlineData("aaaabbbbccccdddd", "aaaabbbbccccdddde", "", 0, 16, "aaaabbbbccccdddd")]
1714-
[InlineData("aaaaaaaaaaaaaaaa", "a", "b", 0, 16, "bbbbbbbbbbbbbbbb")]
1715-
public static void Replace_String(string value, string oldValue, string newValue, int startIndex, int count, string expected)
1716-
{
1717-
StringBuilder builder;
1718-
if (startIndex == 0 && count == value.Length)
1719-
{
1720-
// Use Replace(string, string)
1721-
builder = new StringBuilder(value);
1722-
builder.Replace(oldValue, newValue);
1723-
Assert.Equal(expected, builder.ToString());
1724-
}
1725-
// Use Replace(string, string, int, int)
1726-
builder = new StringBuilder(value);
1727-
builder.Replace(oldValue, newValue, startIndex, count);
1728-
Assert.Equal(expected, builder.ToString());
1729-
}
1730-
1731-
[Fact]
1732-
public static void Replace_String_StringBuilderWithMultipleChunks()
1733-
{
1734-
StringBuilder builder = StringBuilderWithMultipleChunks();
1735-
builder.Replace("a", "b", builder.Length - 10, 10);
1736-
Assert.Equal(new string('a', builder.Length - 10) + new string('b', 10), builder.ToString());
1737-
}
1738-
1739-
[Fact]
1740-
public static void Replace_String_StringBuilderWithMultipleChunks_WholeString()
1741-
{
1742-
StringBuilder builder = StringBuilderWithMultipleChunks();
1743-
builder.Replace(builder.ToString(), "");
1744-
Assert.Same(string.Empty, builder.ToString());
1745-
}
1746-
1747-
[Fact]
1748-
public static void Replace_String_StringBuilderWithMultipleChunks_LongString()
1749-
{
1750-
StringBuilder builder = StringBuilderWithMultipleChunks();
1751-
builder.Replace(builder.ToString() + "b", "");
1752-
Assert.Equal(s_chunkSplitSource, builder.ToString());
1753-
}
1754-
1755-
[Fact]
1756-
public static void Replace_String_Invalid()
1757-
{
1758-
var builder = new StringBuilder(0, 5);
1759-
builder.Append("Hello");
1760-
1761-
AssertExtensions.Throws<ArgumentNullException>("oldValue", () => builder.Replace(null, "")); // Old value is null
1762-
AssertExtensions.Throws<ArgumentNullException>("oldValue", () => builder.Replace(null, "a", 0, 0)); // Old value is null
1763-
1764-
AssertExtensions.Throws<ArgumentException>("oldValue", () => builder.Replace("", "a")); // Old value is empty
1765-
AssertExtensions.Throws<ArgumentException>("oldValue", () => builder.Replace("", "a", 0, 0)); // Old value is empty
1766-
1767-
AssertExtensions.Throws<ArgumentOutOfRangeException>("requiredLength", () => builder.Replace("o", "oo")); // New length > builder.MaxCapacity
1768-
AssertExtensions.Throws<ArgumentOutOfRangeException>("requiredLength", () => builder.Replace("o", "oo", 0, 5)); // New length > builder.MaxCapacity
1769-
1770-
AssertExtensions.Throws<ArgumentOutOfRangeException>("startIndex", () => builder.Replace("a", "b", -1, 0)); // Start index < 0
1771-
AssertExtensions.Throws<ArgumentOutOfRangeException>("count", () => builder.Replace("a", "b", 0, -1)); // Count < 0
1772-
1773-
AssertExtensions.Throws<ArgumentOutOfRangeException>("startIndex", () => builder.Replace("a", "b", 6, 0)); // Count + start index > builder.Length
1774-
AssertExtensions.Throws<ArgumentOutOfRangeException>("count", () => builder.Replace("a", "b", 5, 1)); // Count + start index > builder.Length
1775-
AssertExtensions.Throws<ArgumentOutOfRangeException>("count", () => builder.Replace("a", "b", 4, 2)); // Count + start index > builder.Length
1776-
}
1777-
17781697
[Theory]
17791698
[InlineData("Hello", 0, 5, "Hello")]
17801699
[InlineData("Hello", 2, 3, "llo")]

0 commit comments

Comments
 (0)