From ff6b8d5465c34f492d97f4d69f96aebda7785ea9 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Sun, 27 Aug 2023 13:58:15 +0100 Subject: [PATCH] feat: add Span overloads to complement char.Repeat/string.Repeat --- CHANGELOG.md | 2 + X10D.Tests/src/Text/CharSpanTests.cs | 56 +++++++++++++++++++++ X10D.Tests/src/Text/CharTests.cs | 46 ++++++++++++++++- X10D.Tests/src/Text/StringTests.cs | 37 ++++++++++++++ X10D/src/Text/CharExtensions.cs | 30 +++++++++++ X10D/src/Text/CharSpanExtensions.cs | 74 ++++++++++++++++++++++++++++ X10D/src/Text/StringExtensions.cs | 44 +++++++++++++++-- 7 files changed, 284 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3d880e..b1b6ae2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. @@ -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 diff --git a/X10D.Tests/src/Text/CharSpanTests.cs b/X10D.Tests/src/Text/CharSpanTests.cs index fa02e2f..2441a58 100644 --- a/X10D.Tests/src/Text/CharSpanTests.cs +++ b/X10D.Tests/src/Text/CharSpanTests.cs @@ -60,4 +60,60 @@ internal class CharSpanTests 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 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 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(() => "a".AsSpan().Repeat(10, Span.Empty)); + } + + [Test] + public void Repeat_ShouldThrowArgumentOutOfRangeException_GivenNegativeCount() + { + Assert.Throws(() => _ = "a".AsSpan().Repeat(-1)); + Assert.Throws(() => "a".AsSpan().Repeat(-1, Span.Empty)); + } } diff --git a/X10D.Tests/src/Text/CharTests.cs b/X10D.Tests/src/Text/CharTests.cs index 3c72213..e06d3b5 100644 --- a/X10D.Tests/src/Text/CharTests.cs +++ b/X10D.Tests/src/Text/CharTests.cs @@ -28,6 +28,25 @@ internal class CharTests } } + [Test] + public void Repeat_ShouldPopulateSpanWithRepeatedCharacter_GivenValidCount() + { + const string expected = "aaaaaaaaaa"; + Span destination = new char[10]; + 'a'.Repeat(10, destination); + + Assert.That(destination.ToString(), Is.EqualTo(expected)); + } + + [Test] + public void Repeat_ShouldOnlyWriteOneCharToSpan_GivenCount1() + { + Span 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() { @@ -51,9 +70,34 @@ internal class CharTests Assert.That('a'.Repeat(0), Is.EqualTo(string.Empty)); } + [Test] + public void Repeat_ShouldNotManipulateSpan_GivenCount0() + { + Span 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(() => _ = 'a'.Repeat(-1)); + Assert.Multiple(() => + { + Assert.Throws(() => _ = 'a'.Repeat(-1)); + Assert.Throws(() => 'a'.Repeat(-1, Span.Empty)); + }); + } + + [Test] + public void Repeat_ShouldThrowArgumentException_GivenSmallSpan() + { + Assert.Throws(() => + { + var destination = Span.Empty; + 'a'.Repeat(1, destination); + }); } } diff --git a/X10D.Tests/src/Text/StringTests.cs b/X10D.Tests/src/Text/StringTests.cs index 1eecb89..060a6f7 100644 --- a/X10D.Tests/src/Text/StringTests.cs +++ b/X10D.Tests/src/Text/StringTests.cs @@ -762,6 +762,16 @@ internal class StringTests Assert.That("a".Repeat(0), Is.EqualTo(string.Empty)); } + [Test] + public void Repeat_ShouldNotManipulateSpan_GivenCount0() + { + Span 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() { @@ -770,10 +780,17 @@ internal class StringTests Assert.That(repeated, Is.EqualTo("a")); } + [Test] + public void Repeat_ShouldThrowArgumentException_GivenSmallSpan() + { + Assert.Throws(() => "a".Repeat(10, Span.Empty)); + } + [Test] public void Repeat_ShouldThrowArgumentOutOfRangeException_GivenNegativeCount() { Assert.Throws(() => _ = "a".Repeat(-1)); + Assert.Throws(() => "a".Repeat(-1, Span.Empty)); } [Test] @@ -781,6 +798,26 @@ internal class StringTests { string value = null!; Assert.Throws(() => _ = value.Repeat(0)); + Assert.Throws(() => value.Repeat(0, Span.Empty)); + } + + [Test] + public void Repeat_ShouldPopulateSpanWithRepeatedCharacter_GivenValidCount() + { + const string expected = "aaaaaaaaaa"; + Span destination = new char[10]; + "a".Repeat(10, destination); + + Assert.That(destination.ToString(), Is.EqualTo(expected)); + } + + [Test] + public void Repeat_ShouldOnlyWriteOneCharToSpan_GivenCount1() + { + Span 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] diff --git a/X10D/src/Text/CharExtensions.cs b/X10D/src/Text/CharExtensions.cs index 06ca0f7..5d27d4b 100644 --- a/X10D/src/Text/CharExtensions.cs +++ b/X10D/src/Text/CharExtensions.cs @@ -47,4 +47,34 @@ public static class CharExtensions _ => new string(value, count) }; } + + /// + /// Writes a character to a span of characters, repeated a specified number of times. + /// + /// The character to repeat. + /// The number of times to repeat. + /// The span of characters into which the repeated characters will be written. + [MethodImpl(CompilerResources.MethodImplOptions)] + public static void Repeat(this char value, int count, Span 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; + } + } } diff --git a/X10D/src/Text/CharSpanExtensions.cs b/X10D/src/Text/CharSpanExtensions.cs index 67a01ce..f9ba1d7 100644 --- a/X10D/src/Text/CharSpanExtensions.cs +++ b/X10D/src/Text/CharSpanExtensions.cs @@ -1,3 +1,8 @@ +using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; +using System.Text; +using X10D.CompilerServices; + namespace X10D.Text; /// @@ -67,4 +72,73 @@ public static class CharSpanExtensions return count; } + + /// + /// Repeats a span of characters a specified number of times. + /// + /// The string to repeat. + /// The repeat count. + /// A string containing repeated times. + /// is . + /// is less than 0. + [Pure] + [MethodImpl(CompilerResources.MethodImplOptions)] + public static string Repeat(this ReadOnlySpan 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(); + } + + /// + /// Repeats a span of character a specified number of times, writing the result to another span of characters. + /// + /// The span of characters to repeat. + /// The repeat count. + /// The destination span to write to. + /// is less than 0. + /// + /// is too short to contain the repeated string. + /// + [MethodImpl(CompilerResources.MethodImplOptions)] + public static void Repeat(this ReadOnlySpan value, int count, Span 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 slice = destination.Slice(iteration * value.Length, value.Length); + value.CopyTo(slice); + } + } } diff --git a/X10D/src/Text/StringExtensions.cs b/X10D/src/Text/StringExtensions.cs index 13f839d..c4c68cc 100644 --- a/X10D/src/Text/StringExtensions.cs +++ b/X10D/src/Text/StringExtensions.cs @@ -932,14 +932,50 @@ public static class StringExtensions return value; } - var builder = new StringBuilder(value.Length * count); + Span destination = stackalloc char[value.Length * count]; + value.Repeat(count, destination); + return new string(destination); + } - for (var i = 0; i < count; i++) + /// + /// Repeats a string a specified number of times, writing the result to a span of characters. + /// + /// The string to repeat. + /// The repeat count. + /// The destination span to write to. + /// is . + /// is less than 0. + /// + /// is too short to contain the repeated string. + /// + [MethodImpl(CompilerResources.MethodImplOptions)] + public static void Repeat(this string value, int count, Span 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 slice = destination.Slice(iteration * value.Length, value.Length); + value.AsSpan().CopyTo(slice); + } } ///