Skip to content

Commit

Permalink
feat: add Span overloads to complement char.Repeat/string.Repeat
Browse files Browse the repository at this point in the history
  • Loading branch information
oliverbooth committed Aug 27, 2023
1 parent 56c3f11 commit ff6b8d5
Show file tree
Hide file tree
Showing 7 changed files with 284 additions and 5 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- X10D: Added `string.ConcatIf`.
- X10D: Added `string.MDBold`, `string.MDCode`, `string.MDCodeBlock([string])`, `string.MDHeading(int)`,
`string.MDItalic`, `string.MDLink`, `string.MDStrikeOut`, and `string.MDUnderline` for Markdown formatting.
- X10D: Added Span overloads which complement `char.Repeat` and `string.Repeat`.
- X10D.Unity: Added `RaycastHit.GetComponent` and `RaycastHit.TryGetComponent`.
- X10D.Unity: Added `DebugUtility.DrawFunction`, and `DebugUtility.DrawUnjoinedPolyhedron` on which it relies.

Expand All @@ -41,6 +42,7 @@ BigEndian/LittleEndian methods.
- X10D: `Stream.GetHash<>` and `Stream.TryWriteHash<>` now throw ArgumentException in lieu of
TypeInitializationException.
- X10D: `char.IsEmoji` no longer allocates for .NET 7.
- X10D: `string.Repeat` is now more efficient.

### Removed

Expand Down
56 changes: 56 additions & 0 deletions X10D.Tests/src/Text/CharSpanTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,60 @@ public void CountSubstring_ShouldReturn0_GivenEmptyHaystack()
Assert.That(string.Empty.AsSpan().CountSubstring('\0'), Is.Zero);
Assert.That(string.Empty.AsSpan().CountSubstring(string.Empty.AsSpan(), StringComparison.OrdinalIgnoreCase), Is.Zero);
}

[Test]
public void Repeat_ShouldNotManipulateSpan_GivenCount0()
{
Span<char> destination = new char[11];
"Hello world".AsSpan().CopyTo(destination);

"a".AsSpan().Repeat(0, destination);
Assert.That(destination.ToString(), Is.EqualTo("Hello world"));
}

[Test]
public void Repeat_ShouldReturnItself_GivenCount1()
{
string repeated = "a".AsSpan().Repeat(1);
Assert.That(repeated, Has.Length.EqualTo(1));
Assert.That(repeated, Is.EqualTo("a"));
}

[Test]
public void Repeat_ShouldPopulateSpan_GivenValidSpan()
{
const string expected = "aaaaaaaaaa";
Span<char> destination = new char[10];
"a".AsSpan().Repeat(10, destination);

Assert.That(destination.ToString(), Is.EqualTo(expected));
}

[Test]
public void Repeat_ShouldReturnEmptyString_GivenCount0()
{
Assert.That("a".AsSpan().Repeat(0), Is.EqualTo(string.Empty));
}

[Test]
public void Repeat_ShouldReturnRepeatedString_GivenSpan()
{
const string expected = "aaaaaaaaaa";
string actual = "a".AsSpan().Repeat(10);

Assert.That(actual, Is.EqualTo(expected));
}

[Test]
public void Repeat_ShouldThrowArgumentException_GivenSmallSpan()
{
Assert.Throws<ArgumentException>(() => "a".AsSpan().Repeat(10, Span<char>.Empty));
}

[Test]
public void Repeat_ShouldThrowArgumentOutOfRangeException_GivenNegativeCount()
{
Assert.Throws<ArgumentOutOfRangeException>(() => _ = "a".AsSpan().Repeat(-1));
Assert.Throws<ArgumentOutOfRangeException>(() => "a".AsSpan().Repeat(-1, Span<char>.Empty));
}
}
46 changes: 45 additions & 1 deletion X10D.Tests/src/Text/CharTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,25 @@ public void IsEmoji_ShouldReturnFalse_GivenNonEmoji()
}
}

[Test]
public void Repeat_ShouldPopulateSpanWithRepeatedCharacter_GivenValidCount()
{
const string expected = "aaaaaaaaaa";
Span<char> destination = new char[10];
'a'.Repeat(10, destination);

Assert.That(destination.ToString(), Is.EqualTo(expected));
}

[Test]
public void Repeat_ShouldOnlyWriteOneCharToSpan_GivenCount1()
{
Span<char> destination = new char[10];
'a'.Repeat(1, destination);

Assert.That(destination.ToString(), Is.EqualTo("a\0\0\0\0\0\0\0\0\0"));
}

[Test]
public void Repeat_ShouldReturnRepeatedCharacter_GivenValidCount()
{
Expand All @@ -51,9 +70,34 @@ public void Repeat_ShouldReturnEmptyString_GivenCount0()
Assert.That('a'.Repeat(0), Is.EqualTo(string.Empty));
}

[Test]
public void Repeat_ShouldNotManipulateSpan_GivenCount0()
{
Span<char> destination = new char[10];
destination.Fill(' ');
'a'.Repeat(0, destination);

const string expected = " ";
Assert.That(destination.ToString(), Is.EqualTo(expected));
}

[Test]
public void Repeat_ShouldThrowArgumentOutOfRangeException_GivenNegativeCount()
{
Assert.Throws<ArgumentOutOfRangeException>(() => _ = 'a'.Repeat(-1));
Assert.Multiple(() =>
{
Assert.Throws<ArgumentOutOfRangeException>(() => _ = 'a'.Repeat(-1));
Assert.Throws<ArgumentOutOfRangeException>(() => 'a'.Repeat(-1, Span<char>.Empty));
});
}

[Test]
public void Repeat_ShouldThrowArgumentException_GivenSmallSpan()
{
Assert.Throws<ArgumentException>(() =>
{
var destination = Span<char>.Empty;
'a'.Repeat(1, destination);
});
}
}
37 changes: 37 additions & 0 deletions X10D.Tests/src/Text/StringTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,16 @@ public void Repeat_ShouldReturnEmptyString_GivenCount0()
Assert.That("a".Repeat(0), Is.EqualTo(string.Empty));
}

[Test]
public void Repeat_ShouldNotManipulateSpan_GivenCount0()
{
Span<char> destination = new char[11];
"Hello world".AsSpan().CopyTo(destination);

"a".Repeat(0, destination);
Assert.That(destination.ToString(), Is.EqualTo("Hello world"));
}

[Test]
public void Repeat_ShouldReturnItself_GivenCount1()
{
Expand All @@ -770,17 +780,44 @@ public void Repeat_ShouldReturnItself_GivenCount1()
Assert.That(repeated, Is.EqualTo("a"));
}

[Test]
public void Repeat_ShouldThrowArgumentException_GivenSmallSpan()
{
Assert.Throws<ArgumentException>(() => "a".Repeat(10, Span<char>.Empty));
}

[Test]
public void Repeat_ShouldThrowArgumentOutOfRangeException_GivenNegativeCount()
{
Assert.Throws<ArgumentOutOfRangeException>(() => _ = "a".Repeat(-1));
Assert.Throws<ArgumentOutOfRangeException>(() => "a".Repeat(-1, Span<char>.Empty));
}

[Test]
public void Repeat_ShouldThrowArgumentNullException_GivenNull()
{
string value = null!;
Assert.Throws<ArgumentNullException>(() => _ = value.Repeat(0));
Assert.Throws<ArgumentNullException>(() => value.Repeat(0, Span<char>.Empty));
}

[Test]
public void Repeat_ShouldPopulateSpanWithRepeatedCharacter_GivenValidCount()
{
const string expected = "aaaaaaaaaa";
Span<char> destination = new char[10];
"a".Repeat(10, destination);

Assert.That(destination.ToString(), Is.EqualTo(expected));
}

[Test]
public void Repeat_ShouldOnlyWriteOneCharToSpan_GivenCount1()
{
Span<char> destination = new char[10];
"a".Repeat(1, destination);

Assert.That(destination.ToString(), Is.EqualTo("a\0\0\0\0\0\0\0\0\0"));
}

[Test]
Expand Down
30 changes: 30 additions & 0 deletions X10D/src/Text/CharExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,34 @@ public static string Repeat(this char value, int count)
_ => new string(value, count)
};
}

/// <summary>
/// Writes a character to a span of characters, repeated a specified number of times.
/// </summary>
/// <param name="value">The character to repeat.</param>
/// <param name="count">The number of times to repeat.</param>
/// <param name="destination">The span of characters into which the repeated characters will be written.</param>
[MethodImpl(CompilerResources.MethodImplOptions)]
public static void Repeat(this char value, int count, Span<char> destination)
{
if (count < 0)
{
throw new ArgumentOutOfRangeException(nameof(count), ExceptionMessages.CountMustBeGreaterThanOrEqualTo0);
}

if (count == 0)
{
return;
}

if (destination.Length < count)
{
throw new ArgumentException(ExceptionMessages.DestinationSpanLengthTooShort, nameof(destination));
}

for (var i = 0; i < count; i++)
{
destination[i] = value;
}
}
}
74 changes: 74 additions & 0 deletions X10D/src/Text/CharSpanExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
using System.Diagnostics.Contracts;
using System.Runtime.CompilerServices;
using System.Text;
using X10D.CompilerServices;

namespace X10D.Text;

/// <summary>
Expand Down Expand Up @@ -67,4 +72,73 @@ public static int CountSubstring(this ReadOnlySpan<char> haystack, ReadOnlySpan<

return count;
}

/// <summary>
/// Repeats a span of characters a specified number of times.
/// </summary>
/// <param name="value">The string to repeat.</param>
/// <param name="count">The repeat count.</param>
/// <returns>A string containing <paramref name="value" /> repeated <paramref name="count" /> times.</returns>
/// <exception cref="ArgumentNullException"><paramref name="value" /> is <see langword="null" />.</exception>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="count" /> is less than 0.</exception>
[Pure]
[MethodImpl(CompilerResources.MethodImplOptions)]
public static string Repeat(this ReadOnlySpan<char> value, int count)
{
switch (count)
{
case < 0:
throw new ArgumentOutOfRangeException(nameof(count), ExceptionMessages.CountMustBeGreaterThanOrEqualTo0);

case 0:
return string.Empty;

case 1:
return value.ToString();
}

var builder = new StringBuilder(value.Length * count);

for (var i = 0; i < count; i++)
{
builder.Append(value);
}

return builder.ToString();
}

/// <summary>
/// Repeats a span of character a specified number of times, writing the result to another span of characters.
/// </summary>
/// <param name="value">The span of characters to repeat.</param>
/// <param name="count">The repeat count.</param>
/// <param name="destination">The destination span to write to.</param>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="count" /> is less than 0.</exception>
/// <exception cref="ArgumentException">
/// <paramref name="destination" /> is too short to contain the repeated string.
/// </exception>
[MethodImpl(CompilerResources.MethodImplOptions)]
public static void Repeat(this ReadOnlySpan<char> value, int count, Span<char> destination)
{
if (count < 0)
{
throw new ArgumentOutOfRangeException(nameof(count), ExceptionMessages.CountMustBeGreaterThanOrEqualTo0);
}

if (count == 0)
{
return;
}

if (destination.Length < value.Length * count)
{
throw new ArgumentException(ExceptionMessages.DestinationSpanLengthTooShort, nameof(destination));
}

for (var iteration = 0; iteration < count; iteration++)
{
Span<char> slice = destination.Slice(iteration * value.Length, value.Length);
value.CopyTo(slice);
}
}
}
44 changes: 40 additions & 4 deletions X10D/src/Text/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -932,14 +932,50 @@ public static string Repeat(this string value, int count)
return value;
}

var builder = new StringBuilder(value.Length * count);
Span<char> destination = stackalloc char[value.Length * count];
value.Repeat(count, destination);
return new string(destination);
}

for (var i = 0; i < count; i++)
/// <summary>
/// Repeats a string a specified number of times, writing the result to a span of characters.
/// </summary>
/// <param name="value">The string to repeat.</param>
/// <param name="count">The repeat count.</param>
/// <param name="destination">The destination span to write to.</param>
/// <exception cref="ArgumentNullException"><paramref name="value" /> is <see langword="null" />.</exception>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="count" /> is less than 0.</exception>
/// <exception cref="ArgumentException">
/// <paramref name="destination" /> is too short to contain the repeated string.
/// </exception>
[MethodImpl(CompilerResources.MethodImplOptions)]
public static void Repeat(this string value, int count, Span<char> destination)
{
if (value is null)
{
builder.Append(value);
throw new ArgumentNullException(nameof(value));
}

return builder.ToString();
if (count < 0)
{
throw new ArgumentOutOfRangeException(nameof(count), ExceptionMessages.CountMustBeGreaterThanOrEqualTo0);
}

if (count == 0)
{
return;
}

if (destination.Length < value.Length * count)
{
throw new ArgumentException(ExceptionMessages.DestinationSpanLengthTooShort, nameof(destination));
}

for (var iteration = 0; iteration < count; iteration++)
{
Span<char> slice = destination.Slice(iteration * value.Length, value.Length);
value.AsSpan().CopyTo(slice);
}
}

/// <summary>
Expand Down

0 comments on commit ff6b8d5

Please sign in to comment.