Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

J2N.Text.StringBuilderExtensions: Added Replace() overload that accepts ReadOnlySpan<char> #87

Merged
Merged
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
99 changes: 99 additions & 0 deletions src/J2N/Text/StringBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2046,6 +2046,105 @@ public static StringBuilder Replace(this StringBuilder text, int startIndex, int
return text;
}

#if FEATURE_SPAN

/// <summary>
/// Replaces the specified subsequence in this builder with the specified
/// string, <paramref name="newValue"/>. The substring begins at the specified
/// <paramref name="startIndex"/> and ends to the character at
/// <c><paramref name="count"/> - <paramref name="startIndex"/></c> or
/// to the end of the sequence if no such character exists. First the
/// characters in the substring ar removed and then the specified
/// <paramref name="newValue"/> is inserted at <paramref name="startIndex"/>.
/// This <see cref="StringBuilder"/> will be lengthened to accommodate the
/// specified <paramref name="newValue"/> if necessary.
/// <para/>
/// IMPORTANT: This method has .NET semantics. That is, the <paramref name="count"/> parameter is a count
/// rather than an exclusive end index. To translate from Java, use <c>end - start</c>
/// to resolve the <paramref name="count"/> parameter.
/// </summary>
/// <param name="text">This <see cref="StringBuilder"/>.</param>
/// <param name="startIndex">The inclusive begin index in <paramref name="text"/>.</param>
/// <param name="count">The number of characters to replace.</param>
/// <param name="newValue">The replacement string.</param>
/// <returns>This <see cref="StringBuilder"/> builder.</returns>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="startIndex"/> or <paramref name="count"/> is less than zero.
/// <para/>
/// -or-
/// <para/>
/// Enlarging the value of this instance would exceed <see cref="StringBuilder.MaxCapacity"/>.
/// </exception>
/// <exception cref="ArgumentNullException">If <paramref name="text"/> is <c>null</c>.</exception>
public static StringBuilder Replace(this StringBuilder text, int startIndex, int count, ReadOnlySpan<char> newValue)
{
if (text is null)
throw new ArgumentNullException(nameof(text));
if (startIndex < 0)
throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_NeedNonNegNum);
if (count < 0)
throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_NeedNonNegNum);
if (text.MaxCapacity > 0 && startIndex > text.MaxCapacity - count)
throw new ArgumentOutOfRangeException(nameof(count),
$"{nameof(startIndex)}: {startIndex} + {nameof(count)}: {count} > {nameof(text.MaxCapacity)}: {text.MaxCapacity}");

int end = startIndex + count;
if (end > text.Length)
{
end = text.Length;
}
if (end > startIndex)
{
int stringLength = newValue.Length;
int diff = end - startIndex - stringLength;
if (diff > 0)
{ // replacing with fewer characters
text.Remove(startIndex, diff);
}
else if (diff < 0)
{
// replacing with more characters...need some room
text.Insert(startIndex, new char[-diff]);
}
// copy the chars based on the new length
#if FEATURE_STRINGBUILDER_GETCHUNKS
var textEnumerator = text.GetChunks();
ReadOnlyMemory<char> textChunk = default;
int lowerBound = 0, upperBound = -1;
for (int i = 0; i < stringLength; i++)
{
while (i + startIndex > upperBound)
{
lowerBound += textChunk.Length;
textEnumerator.MoveNext();
textChunk = textEnumerator.Current;
upperBound += textChunk.Length;
}
unsafe
{
using var handle = textChunk.Pin();
char* pointer = (char*)handle.Pointer;
pointer[i + startIndex - lowerBound] = newValue[i];
}
}
#else
for (int i = 0; i < stringLength; i++)
{
text[i + startIndex] = newValue[i];
}
#endif
return text;
}
if (startIndex == end)
{
text.Insert(startIndex, newValue);
return text;
}
return text;
}

#endif

#endregion Replace

#region Reverse
Expand Down
95 changes: 92 additions & 3 deletions tests/J2N.Tests/Text/TestStringBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ public void Test_DeleteII()
* @tests java.lang.StringBuilder.replace(int, int, String)'
*/
[Test]
public void Test_ReplaceIILjava_lang_String()
public void Test_Replace_String()
{
string fixture = "0000";
StringBuilder sb = new StringBuilder(fixture);
Expand All @@ -333,7 +333,7 @@ public void Test_ReplaceIILjava_lang_String()
// FIXME Undocumented NPE in Sun's JRE 5.0_5
try
{
sb.Replace(1, 2 - 1, null); // J2N; Corrected 2nd parameter
sb.Replace(1, 2 - 1, (string)null); // J2N; Corrected 2nd parameter
fail("No NPE");
}
catch (ArgumentNullException e)
Expand Down Expand Up @@ -381,13 +381,102 @@ public void Test_ReplaceIILjava_lang_String()
}

[Test]
public void Test_Replace_Across_Chunks()
public void Test_Replace_String_Across_Chunks()
{
StringBuilder sb = new StringBuilder(2).Append("foodie").Append("guru");
sb.Replace(startIndex: 1, count: 7, "1234567");
assertEquals("f1234567ru", sb.ToString());
}

#if FEATURE_SPAN

/**
* @tests java.lang.StringBuilder.replace(int, int, String)'
*/
[Test]
public void Test_Replace_ReadOnlySpan()
{
string fixture = "0000";
StringBuilder sb = new StringBuilder(fixture);
assertSame(sb, sb.Replace(1, 3 - 1, "11".AsSpan())); // J2N; Corrected 2nd parameter
assertEquals("0110", sb.ToString());
assertEquals(4, sb.Length);

sb = new StringBuilder(fixture);
assertSame(sb, sb.Replace(1, 2 - 1, "11".AsSpan())); // J2N; Corrected 2nd parameter
assertEquals("01100", sb.ToString());
assertEquals(5, sb.Length);

sb = new StringBuilder(fixture);
assertSame(sb, sb.Replace(4, 5 - 4, "11".AsSpan())); // J2N; Corrected 2nd parameter
assertEquals("000011", sb.ToString());
assertEquals(6, sb.Length);

sb = new StringBuilder(fixture);
assertSame(sb, sb.Replace(4, 6 - 4, "11".AsSpan())); // J2N; Corrected 2nd parameter
assertEquals("000011", sb.ToString());
assertEquals(6, sb.Length);

//// FIXME Undocumented NPE in Sun's JRE 5.0_5
//try
//{
// sb.Replace(1, 2 - 1, null); // J2N; Corrected 2nd parameter
// fail("No NPE");
//}
//catch (ArgumentNullException e)
//{
// // Expected
//}

try
{
sb = new StringBuilder(fixture);
sb.Replace(-1, 2 - -1, "11".AsSpan()); // J2N; Corrected 2nd parameter
fail("No SIOOBE, negative start");
}
catch (ArgumentOutOfRangeException e)
{
// Expected
}

try
{
sb = new StringBuilder(fixture);
sb.Replace(5, 2 - 5, "11".AsSpan()); // J2N; Corrected 2nd parameter
fail("No SIOOBE, start > length");
}
catch (ArgumentOutOfRangeException e)
{
// Expected
}

try
{
sb = new StringBuilder(fixture);
sb.Replace(3, 2 - 3, "11".AsSpan()); // J2N; Corrected 2nd parameter
fail("No SIOOBE, start > end");
}
catch (ArgumentOutOfRangeException e)
{
// Expected
}

// Regression for HARMONY-348
StringBuilder buffer = new StringBuilder("1234567");
buffer.Replace(2, 6 - 2, "XXX".AsSpan()); // J2N; Corrected 2nd parameter
assertEquals("12XXX7", buffer.ToString());
}

[Test]
public void Test_Replace_ReadOnlySpan_Across_Chunks()
{
StringBuilder sb = new StringBuilder(2).Append("foodie").Append("guru");
sb.Replace(startIndex: 1, count: 7, "1234567".AsSpan());
assertEquals("f1234567ru", sb.ToString());
}

#endif

private void reverseTest(String org, String rev, String back)
{
// create non-shared StringBuilder
Expand Down
Loading