From 2997dfd2a054894a38843f453f4aec66430a05f7 Mon Sep 17 00:00:00 2001
From: Corentin Giaufer Saubert <43623834+CorentinGS@users.noreply.github.com>
Date: Thu, 7 Aug 2025 22:08:13 +0200
Subject: [PATCH 01/13] feat: Enhance ZaSpanStringBuilder.Append method to
support custom format providers and use InvariantCulture by default
---
.../ZaSpanStringBuilderExtensions.cs | 23 ++++++++++++++++---
1 file changed, 20 insertions(+), 3 deletions(-)
diff --git a/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs b/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs
index 2dac637..6ad7a0a 100644
--- a/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs
+++ b/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs
@@ -1,3 +1,4 @@
+using System.Globalization;
using ZaString.Core;
namespace ZaString.Extensions;
@@ -85,10 +86,26 @@ public static ref ZaSpanStringBuilder Append(ref this ZaSpanStringBuilder builde
/// Thrown if the value cannot be formatted correctly.
public static ref ZaSpanStringBuilder Append(ref this ZaSpanStringBuilder builder, T value, ReadOnlySpan format = default) where T : ISpanFormattable
{
- if (!value.TryFormat(builder.RemainingSpan, out var charsWritten, format, null))
+ return ref builder.Append(value, format, CultureInfo.InvariantCulture);
+ }
+
+ ///
+ /// Appends a value of a type that implements using the specified format provider.
+ ///
+ /// The type of the value, which must implement ISpanFormattable.
+ /// The builder instance.
+ /// The value to format and append.
+ /// An optional format string for the value.
+ /// Format provider to use. If null, is used.
+ /// A reference to the builder to allow for method chaining.
+ /// Thrown if the buffer is too small to hold the formatted value.
+ /// Thrown if the value cannot be formatted correctly.
+ public static ref ZaSpanStringBuilder Append(ref this ZaSpanStringBuilder builder, T value, ReadOnlySpan format, IFormatProvider? provider) where T : ISpanFormattable
+ {
+ provider ??= CultureInfo.InvariantCulture;
+
+ if (!value.TryFormat(builder.RemainingSpan, out var charsWritten, format, provider))
{
- // This can mean two things: buffer is too small, or format is invalid.
- // We throw for "out of range" as it's the most common and actionable reason.
ThrowOutOfRangeException();
}
From 8193ea16fbe3dd065c3ffbc912ff934f6ab4ad71 Mon Sep 17 00:00:00 2001
From: Corentin Giaufer Saubert <43623834+CorentinGS@users.noreply.github.com>
Date: Thu, 7 Aug 2025 22:13:18 +0200
Subject: [PATCH 02/13] feat: Add TryAppend methods to ZaSpanStringBuilder for
safe appending of spans, strings, characters, and formatted values
---
.../ZaSpanStringBuilderExtensions.cs | 108 ++++++++++
.../ZaSpanStringBuilderTryAppendTests.cs | 185 ++++++++++++++++++
2 files changed, 293 insertions(+)
create mode 100644 tests/ZaString.Tests/ZaSpanStringBuilderTryAppendTests.cs
diff --git a/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs b/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs
index 6ad7a0a..038bb50 100644
--- a/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs
+++ b/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs
@@ -8,6 +8,114 @@ namespace ZaString.Extensions;
///
public static class ZaSpanStringBuilderExtensions
{
+ ///
+ /// Attempts to append a read-only span of characters to the builder without throwing.
+ ///
+ /// The builder instance.
+ /// The span of characters to append.
+ /// true if the value was appended; otherwise false if there was not enough capacity.
+ public static bool TryAppend(ref this ZaSpanStringBuilder builder, ReadOnlySpan value)
+ {
+ if (value.Length > builder.RemainingSpan.Length)
+ {
+ return false;
+ }
+
+ value.CopyTo(builder.RemainingSpan);
+ builder.Advance(value.Length);
+ return true;
+ }
+
+ ///
+ /// Attempts to append a string to the builder without throwing.
+ ///
+ /// The builder instance.
+ /// The string to append. If null, this is a no-op and returns true.
+ /// true if the value was appended; otherwise false if there was not enough capacity.
+ public static bool TryAppend(ref this ZaSpanStringBuilder builder, string? value)
+ {
+ return value is null || builder.TryAppend(value.AsSpan());
+ }
+
+ ///
+ /// Attempts to append a single character to the builder without throwing.
+ ///
+ /// The builder instance.
+ /// The character to append.
+ /// true if the value was appended; otherwise false if there was not enough capacity.
+ public static bool TryAppend(ref this ZaSpanStringBuilder builder, char value)
+ {
+ if (builder.RemainingSpan.Length < 1)
+ {
+ return false;
+ }
+
+ builder.RemainingSpan[0] = value;
+ builder.Advance(1);
+ return true;
+ }
+
+ ///
+ /// Attempts to append a value of a type that implements without throwing.
+ ///
+ /// The type of the value, which must implement ISpanFormattable.
+ /// The builder instance.
+ /// The value to format and append.
+ /// An optional format string for the value.
+ /// Format provider to use. If null, is used.
+ /// true if the value was formatted and appended; otherwise false if there was not enough capacity or formatting failed.
+ public static bool TryAppend(ref this ZaSpanStringBuilder builder, T value, ReadOnlySpan format = default, IFormatProvider? provider = null) where T : ISpanFormattable
+ {
+ provider ??= CultureInfo.InvariantCulture;
+
+ if (!value.TryFormat(builder.RemainingSpan, out var charsWritten, format, provider))
+ {
+ return false;
+ }
+
+ builder.Advance(charsWritten);
+ return true;
+ }
+
+ ///
+ /// Attempts to append the default line terminator to the builder without throwing.
+ ///
+ /// The builder instance.
+ /// true if the newline was appended; otherwise false if there was not enough capacity.
+ public static bool TryAppendLine(ref this ZaSpanStringBuilder builder)
+ {
+ var newline = Environment.NewLine.AsSpan();
+ return builder.TryAppend(newline);
+ }
+
+ ///
+ /// Attempts to append a string followed by the default line terminator to the builder without throwing.
+ /// The operation is atomic with respect to capacity: if there is not enough space for both, nothing is written.
+ ///
+ /// The builder instance.
+ /// The string to append. If null, only the newline is appended.
+ /// true if the string and newline were appended; otherwise false if there was not enough capacity.
+ public static bool TryAppendLine(ref this ZaSpanStringBuilder builder, string? value)
+ {
+ var valueLength = value?.Length ?? 0;
+ var newlineLength = Environment.NewLine.Length;
+ var required = valueLength + newlineLength;
+
+ if (required > builder.RemainingSpan.Length)
+ {
+ return false;
+ }
+
+ if (valueLength > 0)
+ {
+ value!.AsSpan().CopyTo(builder.RemainingSpan);
+ builder.Advance(valueLength);
+ }
+
+ Environment.NewLine.AsSpan().CopyTo(builder.RemainingSpan);
+ builder.Advance(newlineLength);
+ return true;
+ }
///
/// Appends a read-only span of characters to the builder.
///
diff --git a/tests/ZaString.Tests/ZaSpanStringBuilderTryAppendTests.cs b/tests/ZaString.Tests/ZaSpanStringBuilderTryAppendTests.cs
new file mode 100644
index 0000000..abaaab0
--- /dev/null
+++ b/tests/ZaString.Tests/ZaSpanStringBuilderTryAppendTests.cs
@@ -0,0 +1,185 @@
+using System.Globalization;
+using ZaString.Core;
+using ZaString.Extensions;
+
+namespace ZaString.Tests;
+
+public class ZaSpanStringBuilderTryAppendTests
+{
+ [Fact]
+ public void TryAppend_ReadOnlySpan_Succeeds()
+ {
+ Span buffer = stackalloc char[10];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+
+ var ok = builder.TryAppend("Hello".AsSpan());
+
+ Assert.True(ok);
+ Assert.Equal(5, builder.Length);
+ Assert.Equal("Hello", builder.AsSpan());
+ }
+
+ [Fact]
+ public void TryAppend_ReadOnlySpan_InsufficientCapacity_ReturnsFalse_NoChange()
+ {
+ Span buffer = stackalloc char[3];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+
+ var ok = builder.TryAppend("Hello".AsSpan());
+
+ Assert.False(ok);
+ Assert.Equal(0, builder.Length);
+ Assert.Equal("", builder.AsSpan());
+ }
+
+ [Fact]
+ public void TryAppend_String_Null_ReturnsTrue_NoChange()
+ {
+ Span buffer = stackalloc char[3];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+
+ var ok = builder.TryAppend((string?)null);
+
+ Assert.True(ok);
+ Assert.Equal(0, builder.Length);
+ Assert.Equal("", builder.AsSpan());
+ }
+
+ [Fact]
+ public void TryAppend_String_Succeeds()
+ {
+ Span buffer = stackalloc char[5];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+
+ var ok = builder.TryAppend("Hello");
+
+ Assert.True(ok);
+ Assert.Equal("Hello", builder.AsSpan());
+ }
+
+ [Fact]
+ public void TryAppend_Char_Succeeds()
+ {
+ Span buffer = stackalloc char[1];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+
+ var ok = builder.TryAppend('A');
+
+ Assert.True(ok);
+ Assert.Equal("A", builder.AsSpan());
+ Assert.Equal(1, builder.Length);
+ }
+
+ [Fact]
+ public void TryAppend_Char_Insufficient_ReturnsFalse_NoChange()
+ {
+ var builder = ZaSpanStringBuilder.Create(Span.Empty);
+
+ var ok = builder.TryAppend('A');
+
+ Assert.False(ok);
+ Assert.Equal(0, builder.Length);
+ Assert.Equal("", builder.AsSpan());
+ }
+
+ [Fact]
+ public void TryAppend_ISpanFormattable_Double_DefaultProvider_UsesInvariant()
+ {
+ var originalCulture = CultureInfo.CurrentCulture;
+ try
+ {
+ // Set a culture with comma decimal separator to ensure provider default is invariant
+ CultureInfo.CurrentCulture = new CultureInfo("fr-FR");
+
+ Span buffer = stackalloc char[10];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+
+ var ok = builder.TryAppend(1.5);
+
+ Assert.True(ok);
+ Assert.Equal("1.5", builder.AsSpan());
+ }
+ finally
+ {
+ CultureInfo.CurrentCulture = originalCulture;
+ }
+ }
+
+ [Fact]
+ public void TryAppend_ISpanFormattable_Double_CustomProvider_RespectsProvider()
+ {
+ var fr = new CultureInfo("fr-FR");
+ Span buffer = stackalloc char[10];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+
+ var ok = builder.TryAppend(1.5, provider: fr);
+
+ Assert.True(ok);
+ Assert.Equal("1,5", builder.AsSpan());
+ }
+
+ [Fact]
+ public void TryAppendLine_OnlyNewline_Succeeds_WhenCapacitySufficient()
+ {
+ var newlineLen = Environment.NewLine.Length;
+ Span buffer = stackalloc char[newlineLen];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+
+ var ok = builder.TryAppendLine();
+
+ Assert.True(ok);
+ Assert.Equal(Environment.NewLine, builder.AsSpan());
+ Assert.Equal(newlineLen, builder.Length);
+ }
+
+ [Fact]
+ public void TryAppendLine_OnlyNewline_InsufficientCapacity_ReturnsFalse_NoChange()
+ {
+ var newlineLen = Environment.NewLine.Length;
+ var capacity = Math.Max(0, newlineLen - 1);
+ Span buffer = capacity == 0 ? Span.Empty : stackalloc char[capacity];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+
+ var ok = builder.TryAppendLine();
+
+ Assert.False(ok);
+ Assert.Equal(0, builder.Length);
+ Assert.Equal("", builder.AsSpan());
+ }
+
+ [Fact]
+ public void TryAppendLine_String_AtomicFailure_NoPartialWrite()
+ {
+ var value = "Hi"; // length 2
+ var newlineLen = Environment.NewLine.Length;
+ var required = value.Length + newlineLen;
+ var capacity = Math.Max(value.Length, required - 1); // ensure not enough for both, but possibly enough for value
+
+ Span buffer = stackalloc char[capacity];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+
+ var ok = builder.TryAppendLine(value);
+
+ Assert.False(ok);
+ Assert.Equal(0, builder.Length);
+ Assert.Equal("", builder.AsSpan());
+ }
+
+ [Fact]
+ public void TryAppendLine_String_Succeeds_WhenCapacitySufficient()
+ {
+ var value = "Hello";
+ var newlineLen = Environment.NewLine.Length;
+ var required = value.Length + newlineLen;
+
+ Span buffer = stackalloc char[required];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+
+ var ok = builder.TryAppendLine(value);
+
+ Assert.True(ok);
+ Assert.Equal(value + Environment.NewLine, builder.AsSpan());
+ Assert.Equal(required, builder.Length);
+ }
+}
+
From 67cd48b2f5c999eaeabafec0864dce0514847a81 Mon Sep 17 00:00:00 2001
From: Corentin Giaufer Saubert <43623834+CorentinGS@users.noreply.github.com>
Date: Thu, 7 Aug 2025 22:21:36 +0200
Subject: [PATCH 03/13] feat: Introduce AppendRepeat and AppendJoin methods to
ZaSpanStringBuilder for enhanced string manipulation capabilities
---
samples/ZaString.Demo/Program.cs | 59 ++++++++++++
.../ZaSpanStringBuilderExtensions.cs | 93 +++++++++++++++++++
.../ZaSpanStringBuilderAppendHelpersTests.cs | 82 ++++++++++++++++
3 files changed, 234 insertions(+)
create mode 100644 tests/ZaString.Tests/ZaSpanStringBuilderAppendHelpersTests.cs
diff --git a/samples/ZaString.Demo/Program.cs b/samples/ZaString.Demo/Program.cs
index 60ec05c..b4640b4 100644
--- a/samples/ZaString.Demo/Program.cs
+++ b/samples/ZaString.Demo/Program.cs
@@ -1,5 +1,6 @@
using System.Diagnostics;
using System.Text;
+using System.Globalization;
using ZaString.Core;
using ZaString.Extensions;
@@ -37,9 +38,67 @@ public static void Main()
CharacterModificationDemo();
Console.WriteLine();
+ TryAppendDemo();
+ Console.WriteLine();
+
+ AppendHelpersDemo();
+ Console.WriteLine();
+
Console.WriteLine("Demo complete!");
}
+ private static void TryAppendDemo()
+ {
+ Console.WriteLine("--- TryAppend ---");
+
+ // Small buffer: demonstrate non-throwing false return
+ Span small = stackalloc char[3];
+ var builder = ZaSpanStringBuilder.Create(small);
+ var ok = builder.TryAppend("Hello");
+ Console.WriteLine($"TryAppend(\"Hello\") ok: {ok}, Length: {builder.Length}");
+ ok = builder.TryAppend('A');
+ Console.WriteLine($"TryAppend('A') ok: {ok}, Text: '{builder.AsSpan()}'");
+
+ // Sufficient buffer: show newline and culture-safe formatting
+ Span buffer = stackalloc char[32];
+ builder = ZaSpanStringBuilder.Create(buffer);
+ builder.TryAppend("Hi");
+ builder.TryAppend(' ');
+ builder.TryAppendLine();
+ builder.TryAppend(1.5); // uses InvariantCulture by default => "1.5"
+ Console.WriteLine($"With newline + double (invariant): '{builder.AsSpan()}'");
+
+ // Custom provider formatting via TryAppend
+ Span bufferFr = stackalloc char[16];
+ var fr = new CultureInfo("fr-FR");
+ builder = ZaSpanStringBuilder.Create(bufferFr);
+ builder.TryAppend(1.5, provider: fr);
+ Console.WriteLine($"Double with fr-FR: '{builder.AsSpan()}'");
+ }
+
+ private static void AppendHelpersDemo()
+ {
+ Console.WriteLine("--- Append Helpers (Repeat/Join) ---");
+
+ Span buffer = stackalloc char[128];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+
+ // AppendRepeat and TryAppendRepeat
+ builder.AppendRepeat('-', 10).AppendLine();
+ var repeatedOk = builder.TryAppendRepeat('*', 5);
+ builder.AppendLine();
+ Console.WriteLine($"Repeat appended ok: {repeatedOk}");
+
+ // AppendJoin with strings (null treated as empty)
+ builder.AppendJoin(", ".AsSpan(), "a", null, "c").AppendLine();
+
+ // AppendJoin with ISpanFormattable values and culture
+ var values = new double[] { 1.5, 2.5, 3.5 };
+ builder.AppendJoin("; ".AsSpan(), values, provider: new CultureInfo("fr-FR"));
+
+ Console.WriteLine(builder.AsSpan().ToString());
+ }
+
private static void BasicUsageDemo()
{
Console.WriteLine("--- Basic Usage ---");
diff --git a/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs b/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs
index 038bb50..1432ec0 100644
--- a/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs
+++ b/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs
@@ -8,6 +8,99 @@ namespace ZaString.Extensions;
///
public static class ZaSpanStringBuilderExtensions
{
+ ///
+ /// Appends the specified character repeated times.
+ ///
+ /// Thrown if count is negative or buffer is too small.
+ public static ref ZaSpanStringBuilder AppendRepeat(ref this ZaSpanStringBuilder builder, char value, int count)
+ {
+ if (count < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(count));
+ }
+
+ if (count == 0)
+ {
+ return ref builder;
+ }
+
+ if (builder.RemainingSpan.Length < count)
+ {
+ ThrowOutOfRangeException();
+ }
+
+ builder.RemainingSpan[..count].Fill(value);
+ builder.Advance(count);
+ return ref builder;
+ }
+
+ ///
+ /// Attempts to append the specified character repeated times without throwing.
+ ///
+ /// true if appended; otherwise false when capacity is insufficient.
+ public static bool TryAppendRepeat(ref this ZaSpanStringBuilder builder, char value, int count)
+ {
+ if (count < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(count));
+ }
+
+ if (count == 0)
+ {
+ return true;
+ }
+
+ if (builder.RemainingSpan.Length < count)
+ {
+ return false;
+ }
+
+ builder.RemainingSpan[..count].Fill(value);
+ builder.Advance(count);
+ return true;
+ }
+
+ ///
+ /// Appends the elements separated by .
+ /// Null elements are treated as empty strings.
+ ///
+ public static ref ZaSpanStringBuilder AppendJoin(ref this ZaSpanStringBuilder builder, ReadOnlySpan separator, params string?[] values)
+ {
+ for (var i = 0; i < values.Length; i++)
+ {
+ if (i > 0)
+ {
+ builder.Append(separator);
+ }
+
+ var s = values[i];
+ if (s is not null)
+ {
+ builder.Append(s);
+ }
+ }
+
+ return ref builder;
+ }
+
+ ///
+ /// Appends the elements of separated by .
+ ///
+ public static ref ZaSpanStringBuilder AppendJoin(ref this ZaSpanStringBuilder builder, ReadOnlySpan separator, ReadOnlySpan values, ReadOnlySpan format = default, IFormatProvider? provider = null)
+ where T : ISpanFormattable
+ {
+ for (var i = 0; i < values.Length; i++)
+ {
+ if (i > 0)
+ {
+ builder.Append(separator);
+ }
+
+ builder.Append(values[i], format, provider);
+ }
+
+ return ref builder;
+ }
///
/// Attempts to append a read-only span of characters to the builder without throwing.
///
diff --git a/tests/ZaString.Tests/ZaSpanStringBuilderAppendHelpersTests.cs b/tests/ZaString.Tests/ZaSpanStringBuilderAppendHelpersTests.cs
new file mode 100644
index 0000000..b2fc737
--- /dev/null
+++ b/tests/ZaString.Tests/ZaSpanStringBuilderAppendHelpersTests.cs
@@ -0,0 +1,82 @@
+using System.Globalization;
+using ZaString.Core;
+using ZaString.Extensions;
+
+namespace ZaString.Tests;
+
+public class ZaSpanStringBuilderAppendHelpersTests
+{
+ [Fact]
+ public void AppendRepeat_AppendsCorrectly()
+ {
+ Span buffer = stackalloc char[5];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+
+ builder.AppendRepeat('x', 5);
+
+ Assert.Equal("xxxxx", builder.AsSpan());
+ Assert.Equal(5, builder.Length);
+ }
+
+ [Fact]
+ public void AppendRepeat_Zero_NoChange()
+ {
+ Span buffer = stackalloc char[3];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+
+ builder.AppendRepeat('x', 0);
+
+ Assert.Equal(0, builder.Length);
+ Assert.Equal("", builder.AsSpan());
+ }
+
+ [Fact]
+ public void TryAppendRepeat_Succeeds_WhenCapacitySufficient()
+ {
+ Span buffer = stackalloc char[4];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+
+ var ok = builder.TryAppendRepeat('a', 4);
+
+ Assert.True(ok);
+ Assert.Equal("aaaa", builder.AsSpan());
+ }
+
+ [Fact]
+ public void TryAppendRepeat_InsufficientCapacity_ReturnsFalse_NoChange()
+ {
+ Span buffer = stackalloc char[3];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+
+ var ok = builder.TryAppendRepeat('a', 4);
+
+ Assert.False(ok);
+ Assert.Equal(0, builder.Length);
+ Assert.Equal("", builder.AsSpan());
+ }
+
+ [Fact]
+ public void AppendJoin_Strings_Works()
+ {
+ Span buffer = stackalloc char[20];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+
+ builder.AppendJoin(", ".AsSpan(), "a", null, "c");
+
+ Assert.Equal("a, , c", builder.AsSpan());
+ }
+
+ [Fact]
+ public void AppendJoin_ISpanFormattable_Works_WithProvider()
+ {
+ var fr = new CultureInfo("fr-FR");
+ Span buffer = stackalloc char[20];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+
+ var values = new[] { 1.5, 2.5 };
+ builder.AppendJoin("; ".AsSpan(), values, provider: fr);
+
+ Assert.Equal("1,5; 2,5", builder.AsSpan());
+ }
+}
+
From 0ddb7474c16ceb5306aaecc1d1d8903be732fd77 Mon Sep 17 00:00:00 2001
From: Corentin Giaufer Saubert <43623834+CorentinGS@users.noreply.github.com>
Date: Thu, 7 Aug 2025 22:27:47 +0200
Subject: [PATCH 04/13] feat: Add SetLength and RemoveLast methods to
ZaSpanStringBuilder for enhanced string manipulation, along with extension
methods for safe usage
---
src/ZaString/Core/ZaSpanString.cs | 32 ++++++-
.../ZaSpanStringBuilderExtensions.cs | 90 +++++++++++++++++++
...ZaSpanStringBuilderMutationHelpersTests.cs | 59 ++++++++++++
3 files changed, 180 insertions(+), 1 deletion(-)
create mode 100644 tests/ZaString.Tests/ZaSpanStringBuilderMutationHelpersTests.cs
diff --git a/src/ZaString/Core/ZaSpanString.cs b/src/ZaString/Core/ZaSpanString.cs
index c3ece7d..0c2c9da 100644
--- a/src/ZaString/Core/ZaSpanString.cs
+++ b/src/ZaString/Core/ZaSpanString.cs
@@ -1,4 +1,6 @@
-namespace ZaString.Core;
+using System.Diagnostics;
+
+namespace ZaString.Core;
///
/// A zero-allocation string builder that writes directly to a provided Span
@@ -87,6 +89,8 @@ public static ZaSpanStringBuilder Create(Span buffer)
/// The number of characters written.
public void Advance(int count)
{
+ Debug.Assert(count >= 0, "Advance count must be non-negative.");
+ Debug.Assert(Length + count <= Capacity, "Advance would exceed capacity.");
Length += count;
}
@@ -99,6 +103,32 @@ public void Clear()
Length = 0;
}
+ ///
+ /// Reduces the current length to the specified value. Only truncation is allowed.
+ ///
+ /// The new length, which must be between 0 and the current Length.
+ /// Thrown if newLength is negative or greater than the current Length.
+ public void SetLength(int newLength)
+ {
+ if ((uint)newLength > (uint)Length)
+ throw new ArgumentOutOfRangeException(nameof(newLength));
+
+ Length = newLength;
+ }
+
+ ///
+ /// Removes the last characters from the written span.
+ ///
+ /// Number of characters to remove. Must be between 0 and current Length.
+ /// Thrown if count is negative or greater than current Length.
+ public void RemoveLast(int count)
+ {
+ if ((uint)count > (uint)Length)
+ throw new ArgumentOutOfRangeException(nameof(count));
+
+ Length -= count;
+ }
+
///
/// Returns the built string as a .
///
diff --git a/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs b/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs
index 1432ec0..917c7c3 100644
--- a/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs
+++ b/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs
@@ -8,6 +8,96 @@ namespace ZaString.Extensions;
///
public static class ZaSpanStringBuilderExtensions
{
+ ///
+ /// Attempts to reserve a writable span of the specified size without throwing.
+ /// On success, caller must write up to characters and then call with the actual number written.
+ ///
+ /// The builder.
+ /// Requested size to reserve.
+ /// The span the caller can write into.
+ /// true if reserved; false if capacity is insufficient.
+ public static bool TryGetAppendSpan(ref this ZaSpanStringBuilder builder, int size, out Span writeSpan)
+ {
+ if (size < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(size));
+ }
+
+ if (size == 0)
+ {
+ writeSpan = Span.Empty;
+ return true;
+ }
+
+ if (builder.RemainingSpan.Length < size)
+ {
+ writeSpan = Span.Empty;
+ return false;
+ }
+
+ writeSpan = builder.RemainingSpan[..size];
+ return true;
+ }
+
+ ///
+ /// Reserves a writable span of the specified size or throws if the capacity is insufficient.
+ /// On success, caller must write up to characters and then call with the actual number written.
+ ///
+ public static ref ZaSpanStringBuilder GetAppendSpan(ref this ZaSpanStringBuilder builder, int size, out Span writeSpan)
+ {
+ if (!TryGetAppendSpan(ref builder, size, out writeSpan))
+ {
+ ThrowOutOfRangeException();
+ }
+
+ return ref builder;
+ }
+
+ ///
+ /// Removes the last characters from the written span.
+ ///
+ public static ref ZaSpanStringBuilder RemoveLast(ref this ZaSpanStringBuilder builder, int count)
+ {
+ if (count < 0 || count > builder.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(count));
+ }
+
+ if (count > 0)
+ {
+ builder.RemoveLast(count);
+ }
+
+ return ref builder;
+ }
+
+ ///
+ /// Sets the current length to . Must be between 0 and Capacity.
+ /// If is less than the current Length, the content is logically truncated.
+ ///
+ public static ref ZaSpanStringBuilder SetLength(ref this ZaSpanStringBuilder builder, int newLength)
+ {
+ if (newLength < 0 || newLength > builder.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(newLength));
+ }
+
+ builder.SetLength(newLength);
+ return ref builder;
+ }
+
+ ///
+ /// Ensures the written span ends with the specified character; appends it if needed.
+ ///
+ public static ref ZaSpanStringBuilder EnsureEndsWith(ref this ZaSpanStringBuilder builder, char value)
+ {
+ if (builder.Length == 0 || builder[builder.Length - 1] != value)
+ {
+ builder.Append(value);
+ }
+
+ return ref builder;
+ }
///
/// Appends the specified character repeated times.
///
diff --git a/tests/ZaString.Tests/ZaSpanStringBuilderMutationHelpersTests.cs b/tests/ZaString.Tests/ZaSpanStringBuilderMutationHelpersTests.cs
new file mode 100644
index 0000000..51a5b0d
--- /dev/null
+++ b/tests/ZaString.Tests/ZaSpanStringBuilderMutationHelpersTests.cs
@@ -0,0 +1,59 @@
+using ZaString.Core;
+using ZaString.Extensions;
+
+namespace ZaString.Tests;
+
+public class ZaSpanStringBuilderMutationHelpersTests
+{
+ [Fact]
+ public void SetLength_Truncates()
+ {
+ Span buffer = stackalloc char[16];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+ builder.Append("abcdef");
+
+ builder.SetLength(3);
+
+ Assert.Equal(3, builder.Length);
+ Assert.Equal("abc", builder.AsSpan());
+ }
+
+ [Fact]
+ public void RemoveLast_RemovesCorrectly()
+ {
+ Span buffer = stackalloc char[16];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+ builder.Append("abcdef");
+
+ builder.RemoveLast(2);
+
+ Assert.Equal("abcd", builder.AsSpan());
+ Assert.Equal(4, builder.Length);
+ }
+
+ [Fact]
+ public void EnsureEndsWith_AppendsWhenMissing()
+ {
+ Span buffer = stackalloc char[10];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+ builder.Append("abc");
+
+ builder.EnsureEndsWith('x');
+
+ Assert.Equal("abcx", builder.AsSpan());
+ Assert.Equal('x', builder[builder.Length - 1]);
+ }
+
+ [Fact]
+ public void EnsureEndsWith_NoOpWhenAlreadyEnding()
+ {
+ Span buffer = stackalloc char[10];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+ builder.Append("abcx");
+
+ builder.EnsureEndsWith('x');
+
+ Assert.Equal("abcx", builder.AsSpan());
+ }
+}
+
From e7d0cb0ed2d55b03ec45bf913aac4a057ca99492 Mon Sep 17 00:00:00 2001
From: Corentin Giaufer Saubert <43623834+CorentinGS@users.noreply.github.com>
Date: Thu, 7 Aug 2025 22:36:04 +0200
Subject: [PATCH 05/13] feat: Implement ZaInterpolatedStringHandler for
enhanced string interpolation support in ZaSpanStringBuilder, including new
extension methods for appending interpolated strings
---
samples/ZaString.Demo/Program.cs | 22 +++++++
.../Extensions/ZaInterpolatedStringHandler.cs | 57 +++++++++++++++++++
.../ZaSpanStringBuilderExtensions.cs | 21 +++++++
.../ZaSpanStringBuilderInterpolationTests.cs | 47 +++++++++++++++
4 files changed, 147 insertions(+)
create mode 100644 src/ZaString/Extensions/ZaInterpolatedStringHandler.cs
create mode 100644 tests/ZaString.Tests/ZaSpanStringBuilderInterpolationTests.cs
diff --git a/samples/ZaString.Demo/Program.cs b/samples/ZaString.Demo/Program.cs
index b4640b4..2a6499c 100644
--- a/samples/ZaString.Demo/Program.cs
+++ b/samples/ZaString.Demo/Program.cs
@@ -44,6 +44,9 @@ public static void Main()
AppendHelpersDemo();
Console.WriteLine();
+ InterpolationDemo();
+ Console.WriteLine();
+
Console.WriteLine("Demo complete!");
}
@@ -99,6 +102,25 @@ private static void AppendHelpersDemo()
Console.WriteLine(builder.AsSpan().ToString());
}
+ private static void InterpolationDemo()
+ {
+ Console.WriteLine("--- Interpolated String Handler ---");
+
+ Span buffer = stackalloc char[128];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+
+ var name = "Alice";
+ var age = 30;
+ var pi = Math.PI;
+
+ builder.Append($"User: {name}, Age: {age}, Pi: {pi:F2}");
+ Console.WriteLine(builder.AsSpan().ToString());
+
+ builder.Clear();
+ builder.AppendLine($"Line: {42}");
+ Console.WriteLine(builder.AsSpan().ToString());
+ }
+
private static void BasicUsageDemo()
{
Console.WriteLine("--- Basic Usage ---");
diff --git a/src/ZaString/Extensions/ZaInterpolatedStringHandler.cs b/src/ZaString/Extensions/ZaInterpolatedStringHandler.cs
new file mode 100644
index 0000000..2414729
--- /dev/null
+++ b/src/ZaString/Extensions/ZaInterpolatedStringHandler.cs
@@ -0,0 +1,57 @@
+using System.Globalization;
+using System.Runtime.CompilerServices;
+using ZaString.Core;
+
+namespace ZaString.Extensions;
+
+[InterpolatedStringHandler]
+public ref struct ZaInterpolatedStringHandler
+{
+ private ZaSpanStringBuilder _builder;
+ private readonly IFormatProvider? _provider;
+
+ public ZaInterpolatedStringHandler(int literalLength, int formattedCount, ref ZaSpanStringBuilder builder)
+ {
+ _builder = builder;
+ _provider = CultureInfo.InvariantCulture;
+ }
+
+ public ZaInterpolatedStringHandler(int literalLength, int formattedCount, ref ZaSpanStringBuilder builder, IFormatProvider? provider)
+ {
+ _builder = builder;
+ _provider = provider ?? CultureInfo.InvariantCulture;
+ }
+
+ public void AppendLiteral(string value)
+ {
+ _builder.Append(value);
+ }
+
+ public void AppendFormatted(string? value)
+ {
+ _builder.Append(value);
+ }
+
+ public void AppendFormatted(ReadOnlySpan value)
+ {
+ _builder.Append(value);
+ }
+
+ public void AppendFormatted(char value)
+ {
+ _builder.Append(value);
+ }
+
+ public void AppendFormatted(T value) where T : ISpanFormattable
+ {
+ _builder.Append(value, default, _provider);
+ }
+
+ public void AppendFormatted(T value, string? format) where T : ISpanFormattable
+ {
+ _builder.Append(value, format, _provider);
+ }
+
+ public ZaSpanStringBuilder GetResult() => _builder;
+}
+
diff --git a/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs b/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs
index 917c7c3..b389bb6 100644
--- a/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs
+++ b/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs
@@ -1,4 +1,5 @@
using System.Globalization;
+using System.Runtime.CompilerServices;
using ZaString.Core;
namespace ZaString.Extensions;
@@ -8,6 +9,26 @@ namespace ZaString.Extensions;
///
public static class ZaSpanStringBuilderExtensions
{
+ ///
+ /// Appends an interpolated string using a custom handler that writes directly into the builder.
+ ///
+ public static ref ZaSpanStringBuilder Append(ref this ZaSpanStringBuilder builder,
+ [InterpolatedStringHandlerArgument("builder")] ZaInterpolatedStringHandler handler)
+ {
+ builder = handler.GetResult();
+ return ref builder;
+ }
+
+ ///
+ /// Appends an interpolated string followed by the default line terminator.
+ ///
+ public static ref ZaSpanStringBuilder AppendLine(ref this ZaSpanStringBuilder builder,
+ [InterpolatedStringHandlerArgument("builder")] ZaInterpolatedStringHandler handler)
+ {
+ builder = handler.GetResult();
+ return ref builder.AppendLine();
+ }
+
///
/// Attempts to reserve a writable span of the specified size without throwing.
/// On success, caller must write up to characters and then call with the actual number written.
diff --git a/tests/ZaString.Tests/ZaSpanStringBuilderInterpolationTests.cs b/tests/ZaString.Tests/ZaSpanStringBuilderInterpolationTests.cs
new file mode 100644
index 0000000..3cd9339
--- /dev/null
+++ b/tests/ZaString.Tests/ZaSpanStringBuilderInterpolationTests.cs
@@ -0,0 +1,47 @@
+using System.Globalization;
+using ZaString.Core;
+using ZaString.Extensions;
+
+namespace ZaString.Tests;
+
+public class ZaSpanStringBuilderInterpolationTests
+{
+ [Fact]
+ public void Append_InterpolatedString_WritesLiteralAndFormatted()
+ {
+ Span buffer = stackalloc char[64];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+
+ builder.Append($"Hello {{world}}: {123} {1.5}");
+
+ Assert.Equal("Hello {world}: 123 1.5", builder.AsSpan());
+ }
+
+ [Fact]
+ public void Append_InterpolatedString_WithFormat_AndCulture()
+ {
+ Span buffer = stackalloc char[64];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+
+ builder.Append($"Hex: {255:X2}");
+ Assert.Equal("Hex: FF", builder.AsSpan());
+
+ builder.Clear();
+
+ var fr = new CultureInfo("fr-FR");
+ builder.Append($"FR: {1.5}"); // handler defaults to invariant; test explicit provider
+ Assert.Equal("FR: 1.5", builder.AsSpan());
+ }
+
+ [Fact]
+ public void AppendLine_InterpolatedString_AppendsNewline()
+ {
+ Span buffer = stackalloc char[64];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+
+ builder.AppendLine($"Line: {42}");
+
+ Assert.Equal($"Line: 42{Environment.NewLine}", builder.AsSpan());
+ }
+}
+
From f56221585168bc0910de49b10102643478eb9f5c Mon Sep 17 00:00:00 2001
From: Corentin Giaufer Saubert <43623834+CorentinGS@users.noreply.github.com>
Date: Thu, 7 Aug 2025 22:44:35 +0200
Subject: [PATCH 06/13] feat: Add JSON, HTML, and CSV escaping methods to
ZaSpanStringBuilder with corresponding demo and tests
---
samples/ZaString.Demo/Program.cs | 36 ++++
.../ZaSpanStringBuilderExtensions.cs | 203 ++++++++++++++++++
.../ZaSpanStringBuilderEscapingTests.cs | 52 +++++
3 files changed, 291 insertions(+)
create mode 100644 tests/ZaString.Tests/ZaSpanStringBuilderEscapingTests.cs
diff --git a/samples/ZaString.Demo/Program.cs b/samples/ZaString.Demo/Program.cs
index 2a6499c..55b3e27 100644
--- a/samples/ZaString.Demo/Program.cs
+++ b/samples/ZaString.Demo/Program.cs
@@ -47,6 +47,9 @@ public static void Main()
InterpolationDemo();
Console.WriteLine();
+ JsonEscapingDemo();
+ Console.WriteLine();
+
Console.WriteLine("Demo complete!");
}
@@ -121,6 +124,39 @@ private static void InterpolationDemo()
Console.WriteLine(builder.AsSpan().ToString());
}
+ private static void JsonEscapingDemo()
+ {
+ Console.WriteLine("--- JSON Escaping ---");
+
+ // Build a small JSON object with escaped string values
+ Span buffer = stackalloc char[256];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+
+ var name = "Alice \"A\"\n\t";
+ var message = "Line1\r\nLine2\t\"Quote\"";
+
+ builder.Append('{')
+ .Append("\"name\":\"")
+ .AppendJsonEscaped(name)
+ .Append("\",\"message\":\"")
+ .AppendJsonEscaped(message)
+ .Append("\"}");
+
+ Console.WriteLine(builder.AsSpan().ToString());
+
+ // Demonstrate non-throwing variant
+ Span tiny = stackalloc char[10];
+ var b2 = ZaSpanStringBuilder.Create(tiny);
+ var ok = b2.TryAppendJsonEscaped(message);
+ Console.WriteLine($"TryAppendJsonEscaped ok={ok}, len={b2.Length}");
+
+ // Success case: allocate a conservatively sized buffer and retry
+ Span big = stackalloc char[message.Length * 6]; // safe upper-bound for escaping
+ var b3 = ZaSpanStringBuilder.Create(big);
+ var ok2 = b3.TryAppendJsonEscaped(message);
+ Console.WriteLine($"TryAppendJsonEscaped ok={ok2}, value='{b3.AsSpan().ToString()}'");
+ }
+
private static void BasicUsageDemo()
{
Console.WriteLine("--- Basic Usage ---");
diff --git a/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs b/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs
index b389bb6..f6ccc8f 100644
--- a/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs
+++ b/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs
@@ -535,4 +535,207 @@ private static void ThrowOutOfRangeException()
{
throw new ArgumentOutOfRangeException("value", "The destination buffer is too small.");
}
+
+ // Escaping helpers
+
+ public static ref ZaSpanStringBuilder AppendJsonEscaped(ref this ZaSpanStringBuilder builder, ReadOnlySpan value)
+ {
+ if (!TryAppendJsonEscaped(ref builder, value))
+ {
+ ThrowOutOfRangeException();
+ }
+ return ref builder;
+ }
+
+ public static bool TryAppendJsonEscaped(ref this ZaSpanStringBuilder builder, ReadOnlySpan value)
+ {
+ var required = GetJsonEscapedLength(value);
+ if (required > builder.RemainingSpan.Length)
+ {
+ return false;
+ }
+ if (required == value.Length)
+ {
+ value.CopyTo(builder.RemainingSpan);
+ builder.Advance(value.Length);
+ return true;
+ }
+ var dest = builder.RemainingSpan;
+ var w = 0;
+ for (var i = 0; i < value.Length; i++)
+ {
+ var c = value[i];
+ switch (c)
+ {
+ case '"': dest[w++] = '\\'; dest[w++] = '"'; break;
+ case '\\': dest[w++] = '\\'; dest[w++] = '\\'; break;
+ case '\b': dest[w++] = '\\'; dest[w++] = 'b'; break;
+ case '\f': dest[w++] = '\\'; dest[w++] = 'f'; break;
+ case '\n': dest[w++] = '\\'; dest[w++] = 'n'; break;
+ case '\r': dest[w++] = '\\'; dest[w++] = 'r'; break;
+ case '\t': dest[w++] = '\\'; dest[w++] = 't'; break;
+ default:
+ if (c < ' ')
+ {
+ dest[w++] = '\\';
+ dest[w++] = 'u';
+ dest[w++] = '0';
+ dest[w++] = '0';
+ WriteHexByte((byte)c, dest.Slice(w, 2));
+ w += 2;
+ }
+ else
+ {
+ dest[w++] = c;
+ }
+ break;
+ }
+ }
+ builder.Advance(required);
+ return true;
+ }
+
+ private static int GetJsonEscapedLength(ReadOnlySpan value)
+ {
+ var extra = 0;
+ for (var i = 0; i < value.Length; i++)
+ {
+ var c = value[i];
+ switch (c)
+ {
+ case '"':
+ case '\\':
+ case '\b':
+ case '\f':
+ case '\n':
+ case '\r':
+ case '\t':
+ extra += 1; // becomes two chars instead of one
+ break;
+ default:
+ if (c < ' ')
+ {
+ extra += 5; // \u00XX adds 5 extra over the original 1
+ }
+ break;
+ }
+ }
+ return value.Length + extra;
+ }
+
+ private static void WriteHexByte(byte b, Span dest)
+ {
+ const string hex = "0123456789ABCDEF";
+ dest[0] = hex[(b >> 4) & 0xF];
+ dest[1] = hex[b & 0xF];
+ }
+
+ public static ref ZaSpanStringBuilder AppendHtmlEscaped(ref this ZaSpanStringBuilder builder, ReadOnlySpan value)
+ {
+ if (!TryAppendHtmlEscaped(ref builder, value))
+ {
+ ThrowOutOfRangeException();
+ }
+ return ref builder;
+ }
+
+ public static bool TryAppendHtmlEscaped(ref this ZaSpanStringBuilder builder, ReadOnlySpan value)
+ {
+ var required = GetHtmlEscapedLength(value);
+ if (required > builder.RemainingSpan.Length)
+ {
+ return false;
+ }
+ if (required == value.Length)
+ {
+ value.CopyTo(builder.RemainingSpan);
+ builder.Advance(value.Length);
+ return true;
+ }
+ var dest = builder.RemainingSpan;
+ var w = 0;
+ for (var i = 0; i < value.Length; i++)
+ {
+ switch (value[i])
+ {
+ case '&': dest[w++] = '&'; dest[w++] = 'a'; dest[w++] = 'm'; dest[w++] = 'p'; dest[w++] = ';'; break; // &
+ case '<': dest[w++] = '&'; dest[w++] = 'l'; dest[w++] = 't'; dest[w++] = ';'; break; // <
+ case '>': dest[w++] = '&'; dest[w++] = 'g'; dest[w++] = 't'; dest[w++] = ';'; break; // >
+ case '"': dest[w++] = '&'; dest[w++] = 'q'; dest[w++] = 'u'; dest[w++] = 'o'; dest[w++] = 't'; dest[w++] = ';'; break; // "
+ case '\'': dest[w++] = '&'; dest[w++] = '#'; dest[w++] = '3'; dest[w++] = '9'; dest[w++] = ';'; break; // '
+ default: dest[w++] = value[i]; break;
+ }
+ }
+ builder.Advance(required);
+ return true;
+ }
+
+ private static int GetHtmlEscapedLength(ReadOnlySpan value)
+ {
+ var extra = 0;
+ for (var i = 0; i < value.Length; i++)
+ {
+ switch (value[i])
+ {
+ case '&': extra += 4; break; // & (5) - 1 original = +4
+ case '<':
+ case '>': extra += 3; break; // < or > (4) -1 = +3
+ case '"': extra += 5; break; // " (6) -1 = +5
+ case '\'': extra += 4; break; // ' (5) -1 = +4
+ }
+ }
+ return value.Length + extra;
+ }
+
+ public static ref ZaSpanStringBuilder AppendCsvEscaped(ref this ZaSpanStringBuilder builder, ReadOnlySpan value)
+ {
+ if (!TryAppendCsvEscaped(ref builder, value))
+ {
+ ThrowOutOfRangeException();
+ }
+ return ref builder;
+ }
+
+ public static bool TryAppendCsvEscaped(ref this ZaSpanStringBuilder builder, ReadOnlySpan value)
+ {
+ var needsQuote = NeedsCsvQuoting(value);
+ if (!needsQuote)
+ {
+ return builder.TryAppend(value);
+ }
+ var quoteCount = 0;
+ for (var i = 0; i < value.Length; i++) if (value[i] == '"') quoteCount++;
+ var required = value.Length + quoteCount + 2;
+ if (required > builder.RemainingSpan.Length)
+ {
+ return false;
+ }
+ var dest = builder.RemainingSpan;
+ var w = 0;
+ dest[w++] = '"';
+ for (var i = 0; i < value.Length; i++)
+ {
+ var c = value[i];
+ dest[w++] = c;
+ if (c == '"')
+ {
+ dest[w++] = '"';
+ }
+ }
+ dest[w++] = '"';
+ builder.Advance(required);
+ return true;
+ }
+
+ private static bool NeedsCsvQuoting(ReadOnlySpan value)
+ {
+ if (value.Length == 0) return false;
+ if (char.IsWhiteSpace(value[0]) || char.IsWhiteSpace(value[^1])) return true;
+ for (var i = 0; i < value.Length; i++)
+ {
+ var c = value[i];
+ if (c == ',' || c == '"' || c == '\n' || c == '\r') return true;
+ }
+ return false;
+ }
}
\ No newline at end of file
diff --git a/tests/ZaString.Tests/ZaSpanStringBuilderEscapingTests.cs b/tests/ZaString.Tests/ZaSpanStringBuilderEscapingTests.cs
new file mode 100644
index 0000000..42c4aa0
--- /dev/null
+++ b/tests/ZaString.Tests/ZaSpanStringBuilderEscapingTests.cs
@@ -0,0 +1,52 @@
+using ZaString.Core;
+using ZaString.Extensions;
+
+namespace ZaString.Tests;
+
+public class ZaSpanStringBuilderEscapingTests
+{
+ [Fact]
+ public void AppendJsonEscaped_Basic()
+ {
+ Span buffer = stackalloc char[64];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+
+ builder.AppendJsonEscaped("\"Hello\n\tWorld\"");
+
+ Assert.Equal("\\\"Hello\\n\\tWorld\\\"", builder.AsSpan());
+ }
+
+ [Fact]
+ public void TryAppendJsonEscaped_ControlChars_Unicode()
+ {
+ Span buffer = stackalloc char[64];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+
+ var ok = builder.TryAppendJsonEscaped("A\u0001B");
+ Assert.True(ok);
+ Assert.Equal("A\\u0001B", builder.AsSpan());
+ }
+
+ [Fact]
+ public void AppendHtmlEscaped_Basic()
+ {
+ Span buffer = stackalloc char[64];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+
+ builder.AppendHtmlEscaped("'x' & y");
+
+ Assert.Equal("<a href="#">'x' & y</a>", builder.AsSpan());
+ }
+
+ [Fact]
+ public void TryAppendCsvEscaped_Quotes_Commas_Newlines()
+ {
+ Span buffer = stackalloc char[128];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+
+ var ok = builder.TryAppendCsvEscaped(" a,\"b\"\n");
+ Assert.True(ok);
+ Assert.Equal("\" a,\"\"b\"\"\n\"", builder.AsSpan());
+ }
+}
+
From 196f38a5cf8f2d5cb5e187e510a44d4b0a156a4b Mon Sep 17 00:00:00 2001
From: Corentin Giaufer Saubert <43623834+CorentinGS@users.noreply.github.com>
Date: Thu, 7 Aug 2025 22:50:27 +0200
Subject: [PATCH 07/13] feat: Add URL encoding and path/query parameter helpers
to ZaSpanStringBuilder with corresponding demo and tests
---
samples/ZaString.Demo/Program.cs | 18 ++
.../ZaSpanStringBuilderExtensions.cs | 165 ++++++++++++++++++
.../ZaSpanStringBuilderUrlHelpersTests.cs | 55 ++++++
3 files changed, 238 insertions(+)
create mode 100644 tests/ZaString.Tests/ZaSpanStringBuilderUrlHelpersTests.cs
diff --git a/samples/ZaString.Demo/Program.cs b/samples/ZaString.Demo/Program.cs
index 55b3e27..815b2df 100644
--- a/samples/ZaString.Demo/Program.cs
+++ b/samples/ZaString.Demo/Program.cs
@@ -50,6 +50,9 @@ public static void Main()
JsonEscapingDemo();
Console.WriteLine();
+ UrlHelpersDemo();
+ Console.WriteLine();
+
Console.WriteLine("Demo complete!");
}
@@ -157,6 +160,21 @@ private static void JsonEscapingDemo()
Console.WriteLine($"TryAppendJsonEscaped ok={ok2}, value='{b3.AsSpan().ToString()}'");
}
+ private static void UrlHelpersDemo()
+ {
+ Console.WriteLine("--- URL Helpers ---");
+
+ Span buffer = stackalloc char[256];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+
+ // Path composition ensures single separators
+ builder.AppendPathSegment("api").AppendPathSegment("/v1/").AppendPathSegment("users");
+ builder.AppendQueryParam("q", "a b", isFirst: true);
+ builder.AppendQueryParam("tag", "c#");
+
+ Console.WriteLine(builder.AsSpan().ToString()); // api/v1/users?q=a%20b&tag=c%23
+ }
+
private static void BasicUsageDemo()
{
Console.WriteLine("--- Basic Usage ---");
diff --git a/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs b/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs
index f6ccc8f..3165f57 100644
--- a/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs
+++ b/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs
@@ -1,6 +1,7 @@
using System.Globalization;
using System.Runtime.CompilerServices;
using ZaString.Core;
+using System.Text;
namespace ZaString.Extensions;
@@ -738,4 +739,168 @@ private static bool NeedsCsvQuoting(ReadOnlySpan value)
}
return false;
}
+
+ // URL encoding and composition helpers
+
+ private static bool IsUnreservedAscii(char c)
+ {
+ return (c >= 'A' && c <= 'Z') ||
+ (c >= 'a' && c <= 'z') ||
+ (c >= '0' && c <= '9') ||
+ c is '-' or '_' or '.' or '~';
+ }
+
+ public static ref ZaSpanStringBuilder AppendUrlEncoded(ref this ZaSpanStringBuilder builder, ReadOnlySpan value)
+ {
+ if (!TryAppendUrlEncoded(ref builder, value))
+ {
+ ThrowOutOfRangeException();
+ }
+ return ref builder;
+ }
+
+ public static bool TryAppendUrlEncoded(ref this ZaSpanStringBuilder builder, ReadOnlySpan value)
+ {
+ var required = GetUrlEncodedLength(value);
+ if (required > builder.RemainingSpan.Length)
+ {
+ return false;
+ }
+
+ var dest = builder.RemainingSpan;
+ var w = 0;
+ for (int i = 0; i < value.Length; i++)
+ {
+ var c = value[i];
+ if (c <= 0x7F)
+ {
+ if (IsUnreservedAscii(c))
+ {
+ dest[w++] = c;
+ }
+ else
+ {
+ dest[w++] = '%';
+ WriteHexByte((byte)c, dest.Slice(w, 2));
+ w += 2;
+ }
+ }
+ else if (char.IsHighSurrogate(c) && i + 1 < value.Length && char.IsLowSurrogate(value[i + 1]))
+ {
+ var high = c;
+ var low = value[++i];
+ var codePoint = 0x10000 + (((high - 0xD800) << 10) | (low - 0xDC00));
+ w += PercentEncodeUtf8FromCodePoint(codePoint, dest.Slice(w));
+ }
+ else
+ {
+ var codePoint = (int)c;
+ w += PercentEncodeUtf8FromCodePoint(codePoint, dest.Slice(w));
+ }
+ }
+
+ builder.Advance(required);
+ return true;
+ }
+
+ private static int GetUrlEncodedLength(ReadOnlySpan value)
+ {
+ var length = 0;
+ for (int i = 0; i < value.Length; i++)
+ {
+ var c = value[i];
+ if (c <= 0x7F)
+ {
+ length += IsUnreservedAscii(c) ? 1 : 3;
+ }
+ else if (char.IsHighSurrogate(c) && i + 1 < value.Length && char.IsLowSurrogate(value[i + 1]))
+ {
+ length += 4 * 3; // 4 UTF-8 bytes -> %HH %HH %HH %HH
+ i++; // consume low surrogate
+ }
+ else
+ {
+ // Non-surrogate BMP char: 0x80..0x7FF => 2 bytes; 0x800..0xFFFF => 3 bytes
+ length += (c <= 0x7FF) ? 2 * 3 : 3 * 3;
+ }
+ }
+ return length;
+ }
+
+ private static int PercentEncodeUtf8FromCodePoint(int codePoint, Span dest)
+ {
+ // Returns number of chars written to dest (multiple of 3)
+ if (codePoint <= 0x7F)
+ {
+ dest[0] = '%';
+ WriteHexByte((byte)codePoint, dest.Slice(1, 2));
+ return 3;
+ }
+ if (codePoint <= 0x7FF)
+ {
+ var b1 = (byte)(0b1100_0000 | (codePoint >> 6));
+ var b2 = (byte)(0b1000_0000 | (codePoint & 0b0011_1111));
+ dest[0] = '%'; WriteHexByte(b1, dest.Slice(1, 2));
+ dest[3] = '%'; WriteHexByte(b2, dest.Slice(4, 2));
+ return 6;
+ }
+ if (codePoint <= 0xFFFF)
+ {
+ var b1 = (byte)(0b1110_0000 | (codePoint >> 12));
+ var b2 = (byte)(0b1000_0000 | ((codePoint >> 6) & 0b0011_1111));
+ var b3 = (byte)(0b1000_0000 | (codePoint & 0b0011_1111));
+ dest[0] = '%'; WriteHexByte(b1, dest.Slice(1, 2));
+ dest[3] = '%'; WriteHexByte(b2, dest.Slice(4, 2));
+ dest[6] = '%'; WriteHexByte(b3, dest.Slice(7, 2));
+ return 9;
+ }
+ else
+ {
+ var b1 = (byte)(0b1111_0000 | (codePoint >> 18));
+ var b2 = (byte)(0b1000_0000 | ((codePoint >> 12) & 0b0011_1111));
+ var b3 = (byte)(0b1000_0000 | ((codePoint >> 6) & 0b0011_1111));
+ var b4 = (byte)(0b1000_0000 | (codePoint & 0b0011_1111));
+ dest[0] = '%'; WriteHexByte(b1, dest.Slice(1, 2));
+ dest[3] = '%'; WriteHexByte(b2, dest.Slice(4, 2));
+ dest[6] = '%'; WriteHexByte(b3, dest.Slice(7, 2));
+ dest[9] = '%'; WriteHexByte(b4, dest.Slice(10, 2));
+ return 12;
+ }
+ }
+
+ public static ref ZaSpanStringBuilder AppendPathSegment(ref this ZaSpanStringBuilder builder, ReadOnlySpan segment, char separator = '/')
+ {
+ if (builder.Length > 0 && builder[builder.Length - 1] != separator)
+ {
+ builder.Append(separator);
+ }
+
+ // Trim leading separators in segment
+ int start = 0;
+ while (start < segment.Length && segment[start] == separator) start++;
+ if (start < segment.Length)
+ {
+ builder.Append(segment[start..]);
+ }
+ return ref builder;
+ }
+
+ public static ref ZaSpanStringBuilder AppendQueryParam(ref this ZaSpanStringBuilder builder, ReadOnlySpan key, ReadOnlySpan value, bool urlEncode = true, bool isFirst = false)
+ {
+ builder.Append(isFirst ? '?' : '&');
+ if (urlEncode)
+ {
+ builder.AppendUrlEncoded(key);
+ builder.Append('=');
+ builder.AppendUrlEncoded(value);
+ }
+ else
+ {
+ builder.Append(key);
+ builder.Append('=');
+ builder.Append(value);
+ }
+
+ return ref builder;
+ }
}
\ No newline at end of file
diff --git a/tests/ZaString.Tests/ZaSpanStringBuilderUrlHelpersTests.cs b/tests/ZaString.Tests/ZaSpanStringBuilderUrlHelpersTests.cs
new file mode 100644
index 0000000..8eb9e03
--- /dev/null
+++ b/tests/ZaString.Tests/ZaSpanStringBuilderUrlHelpersTests.cs
@@ -0,0 +1,55 @@
+using ZaString.Core;
+using ZaString.Extensions;
+
+namespace ZaString.Tests;
+
+public class ZaSpanStringBuilderUrlHelpersTests
+{
+ [Fact]
+ public void AppendUrlEncoded_Ascii_Unreserved_Untouched()
+ {
+ Span buffer = stackalloc char[32];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+
+ builder.AppendUrlEncoded("abc-_.~123");
+
+ Assert.Equal("abc-_.~123", builder.AsSpan());
+ }
+
+ [Fact]
+ public void AppendUrlEncoded_Reserved_And_NonAscii_PercentEncoded()
+ {
+ Span buffer = stackalloc char[64];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+
+ builder.AppendUrlEncoded("a b/€");
+
+ // space -> %20, slash -> %2F, Euro (UTF-8: E2 82 AC) -> %E2%82%AC
+ Assert.Equal("a%20b%2F%E2%82%AC", builder.AsSpan());
+ }
+
+ [Fact]
+ public void AppendPathSegment_Joins_With_Single_Separator()
+ {
+ Span buffer = stackalloc char[32];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+
+ builder.AppendPathSegment("api").AppendPathSegment("/v1/").AppendPathSegment("users");
+
+ Assert.Equal("api/v1/users", builder.AsSpan());
+ }
+
+ [Fact]
+ public void AppendQueryParam_Encodes_And_Uses_Correct_Delimiters()
+ {
+ Span buffer = stackalloc char[64];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+
+ builder.Append("/search")
+ .AppendQueryParam("q", "a b", urlEncode: true, isFirst: true)
+ .AppendQueryParam("page", "1", urlEncode: false);
+
+ Assert.Equal("/search?q=a%20b&page=1", builder.AsSpan());
+ }
+}
+
From 70d85f35abfd61547041ab3c6f4db335e162fa1e Mon Sep 17 00:00:00 2001
From: Corentin Giaufer Saubert <43623834+CorentinGS@users.noreply.github.com>
Date: Thu, 7 Aug 2025 22:56:02 +0200
Subject: [PATCH 08/13] feat: Introduce ZaPooledStringBuilder for efficient
string building with pooled memory management, including demo and tests
---
samples/ZaString.Demo/Program.cs | 13 ++
src/ZaString/Core/ZaPooledStringBuilder.cs | 123 ++++++++++++++++++
.../ZaPooledStringBuilderTests.cs | 34 +++++
3 files changed, 170 insertions(+)
create mode 100644 src/ZaString/Core/ZaPooledStringBuilder.cs
create mode 100644 tests/ZaString.Tests/ZaPooledStringBuilderTests.cs
diff --git a/samples/ZaString.Demo/Program.cs b/samples/ZaString.Demo/Program.cs
index 815b2df..e3d86da 100644
--- a/samples/ZaString.Demo/Program.cs
+++ b/samples/ZaString.Demo/Program.cs
@@ -53,6 +53,9 @@ public static void Main()
UrlHelpersDemo();
Console.WriteLine();
+ PooledBuilderDemo();
+ Console.WriteLine();
+
Console.WriteLine("Demo complete!");
}
@@ -175,6 +178,16 @@ private static void UrlHelpersDemo()
Console.WriteLine(builder.AsSpan().ToString()); // api/v1/users?q=a%20b&tag=c%23
}
+ private static void PooledBuilderDemo()
+ {
+ Console.WriteLine("--- Pooled Builder ---");
+
+ using var b = ZaPooledStringBuilder.Rent(initialCapacity: 8);
+ b.Append("Hello").Append(", ").Append("World!").Append(' ').Append(123);
+ Console.WriteLine(b.AsSpan().ToString());
+ Console.WriteLine(b.ToString());
+ }
+
private static void BasicUsageDemo()
{
Console.WriteLine("--- Basic Usage ---");
diff --git a/src/ZaString/Core/ZaPooledStringBuilder.cs b/src/ZaString/Core/ZaPooledStringBuilder.cs
new file mode 100644
index 0000000..83c6ea6
--- /dev/null
+++ b/src/ZaString/Core/ZaPooledStringBuilder.cs
@@ -0,0 +1,123 @@
+using System.Buffers;
+using System.Globalization;
+
+namespace ZaString.Core;
+
+///
+/// A growable, pooled string builder that minimizes allocations by renting buffers from ArrayPool.
+///
+public sealed class ZaPooledStringBuilder : IDisposable
+{
+ private char[] _buffer;
+ private int _length;
+ private readonly ArrayPool _pool;
+ private bool _disposed;
+
+ private ZaPooledStringBuilder(ArrayPool pool, int initialCapacity)
+ {
+ _pool = pool;
+ _buffer = pool.Rent(Math.Max(1, initialCapacity));
+ _length = 0;
+ }
+
+ public static ZaPooledStringBuilder Rent(int initialCapacity = 256, ArrayPool? pool = null)
+ {
+ return new ZaPooledStringBuilder(pool ?? ArrayPool.Shared, initialCapacity);
+ }
+
+ public int Length => _length;
+ public int Capacity => _buffer.Length;
+
+ public ReadOnlySpan AsSpan() => _buffer.AsSpan(0, _length);
+
+ public override string ToString() => new string(_buffer, 0, _length);
+
+ public void Clear() => _length = 0;
+
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+ var buf = _buffer;
+ _buffer = Array.Empty();
+ _pool.Return(buf);
+ }
+
+ private void EnsureCapacity(int additionalRequired)
+ {
+ if (additionalRequired < 0) throw new ArgumentOutOfRangeException(nameof(additionalRequired));
+ var required = _length + additionalRequired;
+ if (required <= _buffer.Length) return;
+
+ var newCapacity = _buffer.Length;
+ if (newCapacity == 0) newCapacity = 1;
+ while (newCapacity < required)
+ {
+ newCapacity = newCapacity * 2;
+ }
+
+ var newBuffer = _pool.Rent(newCapacity);
+ _buffer.AsSpan(0, _length).CopyTo(newBuffer);
+ _pool.Return(_buffer);
+ _buffer = newBuffer;
+ }
+
+ public ZaPooledStringBuilder Append(ReadOnlySpan value)
+ {
+ EnsureCapacity(value.Length);
+ value.CopyTo(_buffer.AsSpan(_length));
+ _length += value.Length;
+ return this;
+ }
+
+ public ZaPooledStringBuilder Append(string? value)
+ {
+ if (!string.IsNullOrEmpty(value))
+ {
+ Append(value.AsSpan());
+ }
+ return this;
+ }
+
+ public ZaPooledStringBuilder Append(char value)
+ {
+ EnsureCapacity(1);
+ _buffer[_length++] = value;
+ return this;
+ }
+
+ public ZaPooledStringBuilder Append(bool value)
+ {
+ return Append(value ? "true" : "false");
+ }
+
+ public ZaPooledStringBuilder Append(T value, ReadOnlySpan format = default, IFormatProvider? provider = null) where T : ISpanFormattable
+ {
+ provider ??= CultureInfo.InvariantCulture;
+ while (true)
+ {
+ if (value.TryFormat(_buffer.AsSpan(_length), out var written, format, provider))
+ {
+ _length += written;
+ return this;
+ }
+ // Grow and retry
+ EnsureCapacity(Math.Max(1, _buffer.Length));
+ }
+ }
+
+ public ZaPooledStringBuilder AppendLine()
+ {
+ return Append(Environment.NewLine);
+ }
+
+ public ZaPooledStringBuilder AppendLine(string? value)
+ {
+ if (value is not null)
+ {
+ Append(value);
+ }
+ return AppendLine();
+ }
+}
+
diff --git a/tests/ZaString.Tests/ZaPooledStringBuilderTests.cs b/tests/ZaString.Tests/ZaPooledStringBuilderTests.cs
new file mode 100644
index 0000000..1bfbf8c
--- /dev/null
+++ b/tests/ZaString.Tests/ZaPooledStringBuilderTests.cs
@@ -0,0 +1,34 @@
+using ZaString.Core;
+
+namespace ZaString.Tests;
+
+public class ZaPooledStringBuilderTests
+{
+ [Fact]
+ public void Append_GrowsAndBuilds()
+ {
+ using var b = ZaPooledStringBuilder.Rent(initialCapacity: 4);
+ b.Append("Hello").Append(", ").Append("World!");
+ Assert.Equal("Hello, World!", b.AsSpan());
+ Assert.Equal("Hello, World!", b.ToString());
+ }
+
+ [Fact]
+ public void Append_Format_Primitives_Invariant()
+ {
+ using var b = ZaPooledStringBuilder.Rent(4);
+ b.Append(255, "X2").Append(' ').Append(1.5);
+ Assert.Equal("FF 1.5", b.AsSpan());
+ }
+
+ [Fact]
+ public void Clear_ResetsLength()
+ {
+ using var b = ZaPooledStringBuilder.Rent(4);
+ b.Append("abc");
+ b.Clear();
+ Assert.Equal(0, b.Length);
+ Assert.Equal("", b.AsSpan());
+ }
+}
+
From 54b55bb56c0c5feed23509e0e40edbad1991e59e Mon Sep 17 00:00:00 2001
From: Corentin Giaufer Saubert <43623834+CorentinGS@users.noreply.github.com>
Date: Thu, 7 Aug 2025 23:05:13 +0200
Subject: [PATCH 09/13] feat: Implement ZaUtf8SpanWriter for zero-allocation
UTF-8 writing with extension methods for various data types, including tests
and demo
---
samples/ZaString.Demo/Program.cs | 30 ++++
src/ZaString/Core/ZaUtf8SpanWriter.cs | 39 +++++
.../Extensions/ZaUtf8SpanWriterExtensions.cs | 163 ++++++++++++++++++
tests/ZaString.Tests/ZaUtf8SpanWriterTests.cs | 83 +++++++++
4 files changed, 315 insertions(+)
create mode 100644 src/ZaString/Core/ZaUtf8SpanWriter.cs
create mode 100644 src/ZaString/Extensions/ZaUtf8SpanWriterExtensions.cs
create mode 100644 tests/ZaString.Tests/ZaUtf8SpanWriterTests.cs
diff --git a/samples/ZaString.Demo/Program.cs b/samples/ZaString.Demo/Program.cs
index e3d86da..d3ddd2e 100644
--- a/samples/ZaString.Demo/Program.cs
+++ b/samples/ZaString.Demo/Program.cs
@@ -56,6 +56,9 @@ public static void Main()
PooledBuilderDemo();
Console.WriteLine();
+ Utf8WriterDemo();
+ Console.WriteLine();
+
Console.WriteLine("Demo complete!");
}
@@ -444,4 +447,31 @@ private static void CharacterModificationDemo()
Console.WriteLine("✓ IndexOutOfRangeException caught for negative index");
}
}
+
+ private static void Utf8WriterDemo()
+ {
+ Console.WriteLine("--- UTF-8 Writer Demo ---");
+
+ // Create a buffer for UTF-8 bytes
+ Span utf8Buffer = stackalloc byte[1024];
+ var writer = ZaUtf8SpanWriter.Create(utf8Buffer);
+
+ // Write some text
+ writer.Append("Hello, ");
+ writer.Append("World!");
+ writer.Append(" This is a test of ");
+ writer.Append(123);
+ writer.Append(" bytes.");
+
+ // Access the written bytes
+ Console.WriteLine($"Written UTF-8 bytes: {writer.Length}");
+ Console.WriteLine(Encoding.UTF8.GetString(writer.AsSpan()));
+
+ // Demonstrate hex and base64
+ writer.Clear();
+ var data = new byte[] { 0x01, 0x02, 0x03, 0x04 };
+ writer.Append("Hex: ").AppendHex(data, uppercase: true);
+ writer.Append(", Base64: ").AppendBase64(data);
+ Console.WriteLine(Encoding.UTF8.GetString(writer.AsSpan()));
+ }
}
\ No newline at end of file
diff --git a/src/ZaString/Core/ZaUtf8SpanWriter.cs b/src/ZaString/Core/ZaUtf8SpanWriter.cs
new file mode 100644
index 0000000..4f6e6f8
--- /dev/null
+++ b/src/ZaString/Core/ZaUtf8SpanWriter.cs
@@ -0,0 +1,39 @@
+using System.Buffers;
+using System.Globalization;
+using System.Text;
+
+namespace ZaString.Core;
+
+///
+/// A zero-allocation UTF-8 writer that writes directly to a provided Span<byte>.
+/// This is a ref struct to ensure it is only allocated on the stack.
+///
+public ref struct ZaUtf8SpanWriter
+{
+ private readonly Span _buffer;
+ public int Length { get; private set; }
+ public int Capacity => _buffer.Length;
+ public ReadOnlySpan WrittenSpan => _buffer[..Length];
+ public Span RemainingSpan => _buffer[Length..];
+
+ private ZaUtf8SpanWriter(Span buffer)
+ {
+ _buffer = buffer;
+ Length = 0;
+ }
+
+ public static ZaUtf8SpanWriter Create(Span buffer) => new(buffer);
+
+ public void Advance(int count)
+ {
+ System.Diagnostics.Debug.Assert(count >= 0, "Advance count must be non-negative.");
+ System.Diagnostics.Debug.Assert(Length + count <= Capacity, "Advance would exceed capacity.");
+ Length += count;
+ }
+
+ public void Clear() => Length = 0;
+
+ public ReadOnlySpan AsSpan() => WrittenSpan;
+
+ public override string ToString() => Encoding.UTF8.GetString(WrittenSpan);
+}
\ No newline at end of file
diff --git a/src/ZaString/Extensions/ZaUtf8SpanWriterExtensions.cs b/src/ZaString/Extensions/ZaUtf8SpanWriterExtensions.cs
new file mode 100644
index 0000000..b0c344e
--- /dev/null
+++ b/src/ZaString/Extensions/ZaUtf8SpanWriterExtensions.cs
@@ -0,0 +1,163 @@
+using System.Buffers;
+using System.Globalization;
+using System.Text;
+using System.Buffers.Text;
+using ZaString.Core;
+
+namespace ZaString.Extensions;
+
+///
+/// Provides extension methods for the .
+///
+public static class ZaUtf8SpanWriterExtensions
+{
+ public static ref ZaUtf8SpanWriter Append(ref this ZaUtf8SpanWriter writer, ReadOnlySpan value)
+ {
+ if (value.Length > writer.RemainingSpan.Length)
+ {
+ ThrowOutOfRangeException();
+ }
+
+ value.CopyTo(writer.RemainingSpan);
+ writer.Advance(value.Length);
+ return ref writer;
+ }
+
+ public static ref ZaUtf8SpanWriter Append(ref this ZaUtf8SpanWriter writer, string? value)
+ {
+ if (value is not null)
+ {
+ var bytes = Encoding.UTF8.GetByteCount(value);
+ if (bytes > writer.RemainingSpan.Length)
+ {
+ ThrowOutOfRangeException();
+ }
+
+ var written = Encoding.UTF8.GetBytes(value, writer.RemainingSpan);
+ writer.Advance(written);
+ }
+
+ return ref writer;
+ }
+
+ public static ref ZaUtf8SpanWriter Append(ref this ZaUtf8SpanWriter writer, char value)
+ {
+ var bytes = Encoding.UTF8.GetByteCount(stackalloc char[1] { value });
+ if (bytes > writer.RemainingSpan.Length)
+ {
+ ThrowOutOfRangeException();
+ }
+
+ var written = Encoding.UTF8.GetBytes(stackalloc char[1] { value }, writer.RemainingSpan);
+ writer.Advance(written);
+ return ref writer;
+ }
+
+ public static ref ZaUtf8SpanWriter Append(ref this ZaUtf8SpanWriter writer, int value, ReadOnlySpan format = default)
+ {
+ var standardFormat = format.Length > 0 ? new StandardFormat(format[0]) : default;
+ if (!Utf8Formatter.TryFormat(value, writer.RemainingSpan, out var bytesWritten, standardFormat))
+ {
+ ThrowOutOfRangeException();
+ }
+
+ writer.Advance(bytesWritten);
+ return ref writer;
+ }
+
+ public static ref ZaUtf8SpanWriter Append(ref this ZaUtf8SpanWriter writer, long value, ReadOnlySpan format = default)
+ {
+ var standardFormat = format.Length > 0 ? new StandardFormat(format[0]) : default;
+ if (!Utf8Formatter.TryFormat(value, writer.RemainingSpan, out var bytesWritten, standardFormat))
+ {
+ ThrowOutOfRangeException();
+ }
+
+ writer.Advance(bytesWritten);
+ return ref writer;
+ }
+
+ public static ref ZaUtf8SpanWriter Append(ref this ZaUtf8SpanWriter writer, double value, ReadOnlySpan format = default)
+ {
+ var standardFormat = format.Length > 0 ? new StandardFormat(format[0]) : default;
+ if (!Utf8Formatter.TryFormat(value, writer.RemainingSpan, out var bytesWritten, standardFormat))
+ {
+ ThrowOutOfRangeException();
+ }
+
+ writer.Advance(bytesWritten);
+ return ref writer;
+ }
+
+ public static ref ZaUtf8SpanWriter Append(ref this ZaUtf8SpanWriter writer, DateTime value, ReadOnlySpan format = default)
+ {
+ var standardFormat = format.Length > 0 ? new StandardFormat(format[0]) : default;
+ if (!Utf8Formatter.TryFormat(value, writer.RemainingSpan, out var bytesWritten, standardFormat))
+ {
+ ThrowOutOfRangeException();
+ }
+
+ writer.Advance(bytesWritten);
+ return ref writer;
+ }
+
+ public static ref ZaUtf8SpanWriter AppendLine(ref this ZaUtf8SpanWriter writer)
+ {
+ return ref writer.Append(Encoding.UTF8.GetBytes(Environment.NewLine));
+ }
+
+ public static ref ZaUtf8SpanWriter AppendLine(ref this ZaUtf8SpanWriter writer, string? value)
+ {
+ if (value is not null)
+ {
+ writer.Append(value);
+ }
+ return ref writer.AppendLine();
+ }
+
+ public static ref ZaUtf8SpanWriter AppendHex(ref this ZaUtf8SpanWriter writer, ReadOnlySpan data, bool uppercase = false)
+ {
+ var required = data.Length * 2;
+ if (required > writer.RemainingSpan.Length)
+ {
+ ThrowOutOfRangeException();
+ }
+
+ var dest = writer.RemainingSpan;
+ var hex = uppercase ? "0123456789ABCDEF" : "0123456789abcdef";
+ var w = 0;
+
+ for (var i = 0; i < data.Length; i++)
+ {
+ var b = data[i];
+ dest[w++] = (byte)hex[(b >> 4) & 0xF];
+ dest[w++] = (byte)hex[b & 0xF];
+ }
+
+ writer.Advance(required);
+ return ref writer;
+ }
+
+ public static ref ZaUtf8SpanWriter AppendBase64(ref this ZaUtf8SpanWriter writer, ReadOnlySpan data)
+ {
+ var required = Base64.GetMaxEncodedToUtf8Length(data.Length);
+ if (required > writer.RemainingSpan.Length)
+ {
+ ThrowOutOfRangeException();
+ }
+
+ var status = Base64.EncodeToUtf8(data, writer.RemainingSpan, out var bytesConsumed, out var bytesWritten);
+ if (status != OperationStatus.Done)
+ {
+ ThrowOutOfRangeException();
+ }
+
+ writer.Advance(bytesWritten);
+ return ref writer;
+ }
+
+ private static void ThrowOutOfRangeException()
+ {
+ throw new ArgumentOutOfRangeException("value", "The destination buffer is too small.");
+ }
+}
\ No newline at end of file
diff --git a/tests/ZaString.Tests/ZaUtf8SpanWriterTests.cs b/tests/ZaString.Tests/ZaUtf8SpanWriterTests.cs
new file mode 100644
index 0000000..93d889d
--- /dev/null
+++ b/tests/ZaString.Tests/ZaUtf8SpanWriterTests.cs
@@ -0,0 +1,83 @@
+using System.Text;
+using ZaString.Core;
+using ZaString.Extensions;
+
+namespace ZaString.Tests;
+
+public class ZaUtf8SpanWriterTests
+{
+ [Fact]
+ public void Append_String_EncodesToUtf8()
+ {
+ Span buffer = stackalloc byte[32];
+ var writer = ZaUtf8SpanWriter.Create(buffer);
+
+ writer.Append("Hello");
+
+ var expected = Encoding.UTF8.GetBytes("Hello");
+ Assert.True(writer.AsSpan().SequenceEqual(expected));
+ }
+
+ [Fact]
+ public void Append_Char_EncodesToUtf8()
+ {
+ Span buffer = stackalloc byte[8];
+ var writer = ZaUtf8SpanWriter.Create(buffer);
+
+ writer.Append('A');
+
+ var expected = Encoding.UTF8.GetBytes("A");
+ Assert.True(writer.AsSpan().SequenceEqual(expected));
+ }
+
+ [Fact]
+ public void Append_Int_FormatsToUtf8()
+ {
+ Span buffer = stackalloc byte[16];
+ var writer = ZaUtf8SpanWriter.Create(buffer);
+
+ writer.Append(123);
+
+ var expected = Encoding.UTF8.GetBytes("123");
+ Assert.True(writer.AsSpan().SequenceEqual(expected));
+ }
+
+ [Fact]
+ public void Append_Double_FormatsToUtf8()
+ {
+ Span buffer = stackalloc byte[16];
+ var writer = ZaUtf8SpanWriter.Create(buffer);
+
+ writer.Append(3.14);
+
+ var expected = Encoding.UTF8.GetBytes("3.14");
+ Assert.True(writer.AsSpan().SequenceEqual(expected));
+ }
+
+ [Fact]
+ public void AppendHex_FormatsBytesAsHex()
+ {
+ Span buffer = stackalloc byte[16];
+ var writer = ZaUtf8SpanWriter.Create(buffer);
+
+ var data = new byte[] { 0xAB, 0xCD, 0xEF };
+ writer.AppendHex(data, uppercase: true);
+
+ var expected = Encoding.UTF8.GetBytes("ABCDEF");
+ Assert.True(writer.AsSpan().SequenceEqual(expected));
+ }
+
+ [Fact]
+ public void AppendBase64_EncodesBytes()
+ {
+ Span buffer = stackalloc byte[32];
+ var writer = ZaUtf8SpanWriter.Create(buffer);
+
+ var data = new byte[] { 0x01, 0x02, 0x03 };
+ writer.AppendBase64(data);
+
+ var expected = Convert.ToBase64String(data);
+ var expectedBytes = Encoding.UTF8.GetBytes(expected);
+ Assert.True(writer.AsSpan().SequenceEqual(expectedBytes));
+ }
+}
\ No newline at end of file
From c07aefb1869506011089b898ece39e22838f0276 Mon Sep 17 00:00:00 2001
From: Corentin Giaufer Saubert <43623834+CorentinGS@users.noreply.github.com>
Date: Thu, 7 Aug 2025 23:15:45 +0200
Subject: [PATCH 10/13] feat: Add composite formatting support to
ZaSpanStringBuilder with new AppendFormat methods and corresponding demo and
tests
---
samples/ZaString.Demo/Program.cs | 35 ++++++++++++
.../ZaSpanStringBuilderExtensions.cs | 11 ++++
.../ZaSpanStringBuilderFormatTests.cs | 57 +++++++++++++++++++
3 files changed, 103 insertions(+)
create mode 100644 tests/ZaString.Tests/ZaSpanStringBuilderFormatTests.cs
diff --git a/samples/ZaString.Demo/Program.cs b/samples/ZaString.Demo/Program.cs
index d3ddd2e..c25d866 100644
--- a/samples/ZaString.Demo/Program.cs
+++ b/samples/ZaString.Demo/Program.cs
@@ -59,6 +59,9 @@ public static void Main()
Utf8WriterDemo();
Console.WriteLine();
+ FormatDemo();
+ Console.WriteLine();
+
Console.WriteLine("Demo complete!");
}
@@ -474,4 +477,36 @@ private static void Utf8WriterDemo()
writer.Append(", Base64: ").AppendBase64(data);
Console.WriteLine(Encoding.UTF8.GetString(writer.AsSpan()));
}
+
+ private static void FormatDemo()
+ {
+ Console.WriteLine("--- Composite Formatting ---");
+
+ Span buffer = stackalloc char[128];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+
+ var name = "Alice";
+ var age = 30;
+ var pi = Math.PI;
+
+ // Composite formatting using AppendFormat
+ builder.AppendFormat("User: {0}, Age: {1}, Pi: {2:F2}", name, age, pi);
+ Console.WriteLine(builder.AsSpan().ToString());
+
+ // Clear and demonstrate AppendFormat with culture-specific formatting
+ builder.Clear();
+ var fr = new CultureInfo("fr-FR");
+ builder.AppendFormat(fr, "User: {0}, Age: {1}, Pi: {2:F2}", name, age, pi);
+ Console.WriteLine(builder.AsSpan().ToString());
+
+ // Clear and demonstrate AppendFormat with custom formatting
+ builder.Clear();
+ builder.AppendFormat(fr, "Currency: {0:C}", 1234.56);
+ Console.WriteLine(builder.AsSpan().ToString());
+
+ // Clear and demonstrate AppendFormat with multiple arguments
+ builder.Clear();
+ builder.AppendFormat(fr, "User: {0}, Age: {1}, Pi: {2:F2}, Currency: {3:C}", name, age, pi, 1234.56);
+ Console.WriteLine(builder.AsSpan().ToString());
+ }
}
\ No newline at end of file
diff --git a/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs b/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs
index 3165f57..f90c37b 100644
--- a/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs
+++ b/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs
@@ -903,4 +903,15 @@ public static ref ZaSpanStringBuilder AppendQueryParam(ref this ZaSpanStringBuil
return ref builder;
}
+
+ public static ref ZaSpanStringBuilder AppendFormat(ref this ZaSpanStringBuilder builder, string format, params object?[] args)
+ {
+ return ref builder.AppendFormat(CultureInfo.InvariantCulture, format, args);
+ }
+
+ public static ref ZaSpanStringBuilder AppendFormat(ref this ZaSpanStringBuilder builder, IFormatProvider? formatProvider, string format, params object?[] args)
+ {
+ var formatted = string.Format(formatProvider, format, args);
+ return ref builder.Append(formatted.AsSpan());
+ }
}
\ No newline at end of file
diff --git a/tests/ZaString.Tests/ZaSpanStringBuilderFormatTests.cs b/tests/ZaString.Tests/ZaSpanStringBuilderFormatTests.cs
new file mode 100644
index 0000000..a2dfa2b
--- /dev/null
+++ b/tests/ZaString.Tests/ZaSpanStringBuilderFormatTests.cs
@@ -0,0 +1,57 @@
+using System.Globalization;
+using ZaString.Core;
+using ZaString.Extensions;
+
+namespace ZaString.Tests;
+
+public class ZaSpanStringBuilderFormatTests
+{
+ [Fact]
+ public void AppendFormat_WithParameters_FormatsCorrectly()
+ {
+ Span buffer = stackalloc char[256];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+
+ builder.AppendFormat("User: {0}, Balance: {1:C}, Active: {2}", "John", 1234.56, true);
+
+ var expected = string.Format(CultureInfo.InvariantCulture, "User: {0}, Balance: {1:C}, Active: {2}", "John", 1234.56, true);
+ Assert.Equal(expected, builder.AsSpan().ToString());
+ }
+
+ [Fact]
+ public void AppendFormat_WithCulture_FormatsWithCulture()
+ {
+ Span buffer = stackalloc char[256];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+
+ var culture = new CultureInfo("fr-FR");
+ builder.AppendFormat(culture, "Balance: {0:C}", 1234.56);
+
+ var expected = string.Format(culture, "Balance: {0:C}", 1234.56);
+ Assert.Equal(expected, builder.AsSpan().ToString());
+ }
+
+ [Fact]
+ public void AppendFormat_WithNullValues_HandlesNulls()
+ {
+ Span buffer = stackalloc char[256];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+
+ builder.AppendFormat("Null: {0}, Text: {1}, Another null: {2}", null, "test", null);
+
+ var expected = string.Format("Null: {0}, Text: {1}, Another null: {2}", null, "test", null);
+ Assert.Equal(expected, builder.AsSpan().ToString());
+ }
+
+ [Fact]
+ public void AppendFormat_WithISpanFormattable_UsesSpanFormattable()
+ {
+ Span buffer = stackalloc char[256];
+ var builder = ZaSpanStringBuilder.Create(buffer);
+
+ builder.AppendFormat("Int: {0:N0}, Double: {1:F2}", 123, 456.789);
+
+ var expected = string.Format("Int: {0:N0}, Double: {1:F2}", 123, 456.789);
+ Assert.Equal(expected, builder.AsSpan().ToString());
+ }
+}
\ No newline at end of file
From ee7439494b6b4b04b8e62ef2b1b1b9713e61ed47 Mon Sep 17 00:00:00 2001
From: Corentin Giaufer Saubert <43623834+CorentinGS@users.noreply.github.com>
Date: Thu, 7 Aug 2025 23:26:17 +0200
Subject: [PATCH 11/13] refactor: Improve ZaPooledStringBuilder and
ZaSpanStringBuilder with code cleanup, enhanced readability, and consistent
formatting across methods
---
samples/ZaString.Demo/Program.cs | 31 +-
src/ZaString/Core/ZaPooledStringBuilder.cs | 69 +-
src/ZaString/Core/ZaSpanString.cs | 2 +-
src/ZaString/Core/ZaUtf8SpanWriter.cs | 51 +-
.../Extensions/ZaInterpolatedStringHandler.cs | 8 +-
.../ZaSpanStringBuilderExtensions.cs | 1147 +++++++++--------
.../Extensions/ZaUtf8SpanWriterExtensions.cs | 20 +-
.../ZaPooledStringBuilderTests.cs | 5 +-
.../ZaSpanStringBuilderAppendHelpersTests.cs | 9 +-
.../ZaSpanStringBuilderBasicTests.cs | 2 +-
.../ZaSpanStringBuilderEdgeCasesTests.cs | 4 +-
.../ZaSpanStringBuilderEscapingTests.cs | 3 +-
.../ZaSpanStringBuilderFormatTests.cs | 2 +-
.../ZaSpanStringBuilderInterpolationTests.cs | 3 +-
...ZaSpanStringBuilderMutationHelpersTests.cs | 3 +-
.../ZaSpanStringBuilderTryAppendTests.cs | 7 +-
.../ZaSpanStringBuilderUrlHelpersTests.cs | 7 +-
tests/ZaString.Tests/ZaUtf8SpanWriterTests.cs | 18 +-
18 files changed, 769 insertions(+), 622 deletions(-)
diff --git a/samples/ZaString.Demo/Program.cs b/samples/ZaString.Demo/Program.cs
index c25d866..6e18e22 100644
--- a/samples/ZaString.Demo/Program.cs
+++ b/samples/ZaString.Demo/Program.cs
@@ -1,6 +1,6 @@
using System.Diagnostics;
-using System.Text;
using System.Globalization;
+using System.Text;
using ZaString.Core;
using ZaString.Extensions;
@@ -111,7 +111,12 @@ private static void AppendHelpersDemo()
builder.AppendJoin(", ".AsSpan(), "a", null, "c").AppendLine();
// AppendJoin with ISpanFormattable values and culture
- var values = new double[] { 1.5, 2.5, 3.5 };
+ var values = new[]
+ {
+ 1.5,
+ 2.5,
+ 3.5
+ };
builder.AppendJoin("; ".AsSpan(), values, provider: new CultureInfo("fr-FR"));
Console.WriteLine(builder.AsSpan().ToString());
@@ -148,11 +153,11 @@ private static void JsonEscapingDemo()
var message = "Line1\r\nLine2\t\"Quote\"";
builder.Append('{')
- .Append("\"name\":\"")
- .AppendJsonEscaped(name)
- .Append("\",\"message\":\"")
- .AppendJsonEscaped(message)
- .Append("\"}");
+ .Append("\"name\":\"")
+ .AppendJsonEscaped(name)
+ .Append("\",\"message\":\"")
+ .AppendJsonEscaped(message)
+ .Append("\"}");
Console.WriteLine(builder.AsSpan().ToString());
@@ -188,7 +193,7 @@ private static void PooledBuilderDemo()
{
Console.WriteLine("--- Pooled Builder ---");
- using var b = ZaPooledStringBuilder.Rent(initialCapacity: 8);
+ using var b = ZaPooledStringBuilder.Rent(8);
b.Append("Hello").Append(", ").Append("World!").Append(' ').Append(123);
Console.WriteLine(b.AsSpan().ToString());
Console.WriteLine(b.ToString());
@@ -472,8 +477,14 @@ private static void Utf8WriterDemo()
// Demonstrate hex and base64
writer.Clear();
- var data = new byte[] { 0x01, 0x02, 0x03, 0x04 };
- writer.Append("Hex: ").AppendHex(data, uppercase: true);
+ var data = new byte[]
+ {
+ 0x01,
+ 0x02,
+ 0x03,
+ 0x04
+ };
+ writer.Append("Hex: ").AppendHex(data, true);
writer.Append(", Base64: ").AppendBase64(data);
Console.WriteLine(Encoding.UTF8.GetString(writer.AsSpan()));
}
diff --git a/src/ZaString/Core/ZaPooledStringBuilder.cs b/src/ZaString/Core/ZaPooledStringBuilder.cs
index 83c6ea6..7f873ab 100644
--- a/src/ZaString/Core/ZaPooledStringBuilder.cs
+++ b/src/ZaString/Core/ZaPooledStringBuilder.cs
@@ -4,60 +4,73 @@
namespace ZaString.Core;
///
-/// A growable, pooled string builder that minimizes allocations by renting buffers from ArrayPool.
+/// A growable, pooled string builder that minimizes allocations by renting buffers from ArrayPool.
///
public sealed class ZaPooledStringBuilder : IDisposable
{
- private char[] _buffer;
- private int _length;
private readonly ArrayPool _pool;
+ private char[] _buffer;
private bool _disposed;
private ZaPooledStringBuilder(ArrayPool pool, int initialCapacity)
{
_pool = pool;
_buffer = pool.Rent(Math.Max(1, initialCapacity));
- _length = 0;
+ Length = 0;
}
- public static ZaPooledStringBuilder Rent(int initialCapacity = 256, ArrayPool? pool = null)
+ public int Length { get; private set; }
+
+ public int Capacity
{
- return new ZaPooledStringBuilder(pool ?? ArrayPool.Shared, initialCapacity);
+ get => _buffer.Length;
}
- public int Length => _length;
- public int Capacity => _buffer.Length;
-
- public ReadOnlySpan AsSpan() => _buffer.AsSpan(0, _length);
-
- public override string ToString() => new string(_buffer, 0, _length);
-
- public void Clear() => _length = 0;
-
public void Dispose()
{
if (_disposed) return;
_disposed = true;
var buf = _buffer;
- _buffer = Array.Empty();
+ _buffer = [];
_pool.Return(buf);
}
+ public static ZaPooledStringBuilder Rent(int initialCapacity = 256, ArrayPool? pool = null)
+ {
+ return new ZaPooledStringBuilder(pool ?? ArrayPool.Shared, initialCapacity);
+ }
+
+ public ReadOnlySpan AsSpan()
+ {
+ return _buffer.AsSpan(0, Length);
+ }
+
+ public override string ToString()
+ {
+ return new string(_buffer, 0, Length);
+ }
+
+ public void Clear()
+ {
+ Length = 0;
+ }
+
private void EnsureCapacity(int additionalRequired)
{
- if (additionalRequired < 0) throw new ArgumentOutOfRangeException(nameof(additionalRequired));
- var required = _length + additionalRequired;
+ ArgumentOutOfRangeException.ThrowIfNegative(additionalRequired);
+
+ var required = Length + additionalRequired;
if (required <= _buffer.Length) return;
var newCapacity = _buffer.Length;
if (newCapacity == 0) newCapacity = 1;
while (newCapacity < required)
{
- newCapacity = newCapacity * 2;
+ newCapacity *= 2;
}
var newBuffer = _pool.Rent(newCapacity);
- _buffer.AsSpan(0, _length).CopyTo(newBuffer);
+ _buffer.AsSpan(0, Length).CopyTo(newBuffer);
_pool.Return(_buffer);
_buffer = newBuffer;
}
@@ -65,8 +78,8 @@ private void EnsureCapacity(int additionalRequired)
public ZaPooledStringBuilder Append(ReadOnlySpan value)
{
EnsureCapacity(value.Length);
- value.CopyTo(_buffer.AsSpan(_length));
- _length += value.Length;
+ value.CopyTo(_buffer.AsSpan(Length));
+ Length += value.Length;
return this;
}
@@ -76,13 +89,14 @@ public ZaPooledStringBuilder Append(string? value)
{
Append(value.AsSpan());
}
+
return this;
}
public ZaPooledStringBuilder Append(char value)
{
EnsureCapacity(1);
- _buffer[_length++] = value;
+ _buffer[Length++] = value;
return this;
}
@@ -96,11 +110,12 @@ public ZaPooledStringBuilder Append(T value, ReadOnlySpan format = defa
provider ??= CultureInfo.InvariantCulture;
while (true)
{
- if (value.TryFormat(_buffer.AsSpan(_length), out var written, format, provider))
+ if (value.TryFormat(_buffer.AsSpan(Length), out var written, format, provider))
{
- _length += written;
+ Length += written;
return this;
}
+
// Grow and retry
EnsureCapacity(Math.Max(1, _buffer.Length));
}
@@ -117,7 +132,7 @@ public ZaPooledStringBuilder AppendLine(string? value)
{
Append(value);
}
+
return AppendLine();
}
-}
-
+}
\ No newline at end of file
diff --git a/src/ZaString/Core/ZaSpanString.cs b/src/ZaString/Core/ZaSpanString.cs
index 0c2c9da..ffeb077 100644
--- a/src/ZaString/Core/ZaSpanString.cs
+++ b/src/ZaString/Core/ZaSpanString.cs
@@ -117,7 +117,7 @@ public void SetLength(int newLength)
}
///
- /// Removes the last characters from the written span.
+ /// Removes the last characters from the written span.
///
/// Number of characters to remove. Must be between 0 and current Length.
/// Thrown if count is negative or greater than current Length.
diff --git a/src/ZaString/Core/ZaUtf8SpanWriter.cs b/src/ZaString/Core/ZaUtf8SpanWriter.cs
index 4f6e6f8..54f56f8 100644
--- a/src/ZaString/Core/ZaUtf8SpanWriter.cs
+++ b/src/ZaString/Core/ZaUtf8SpanWriter.cs
@@ -1,20 +1,31 @@
-using System.Buffers;
-using System.Globalization;
+using System.Diagnostics;
using System.Text;
namespace ZaString.Core;
///
-/// A zero-allocation UTF-8 writer that writes directly to a provided Span<byte>.
-/// This is a ref struct to ensure it is only allocated on the stack.
+/// A zero-allocation UTF-8 writer that writes directly to a provided Span<byte>.
+/// This is a ref struct to ensure it is only allocated on the stack.
///
public ref struct ZaUtf8SpanWriter
{
private readonly Span _buffer;
public int Length { get; private set; }
- public int Capacity => _buffer.Length;
- public ReadOnlySpan WrittenSpan => _buffer[..Length];
- public Span RemainingSpan => _buffer[Length..];
+
+ private int Capacity
+ {
+ get => _buffer.Length;
+ }
+
+ public ReadOnlySpan WrittenSpan
+ {
+ get => _buffer[..Length];
+ }
+
+ public Span RemainingSpan
+ {
+ get => _buffer[Length..];
+ }
private ZaUtf8SpanWriter(Span buffer)
{
@@ -22,18 +33,30 @@ private ZaUtf8SpanWriter(Span buffer)
Length = 0;
}
- public static ZaUtf8SpanWriter Create(Span buffer) => new(buffer);
+ public static ZaUtf8SpanWriter Create(Span buffer)
+ {
+ return new ZaUtf8SpanWriter(buffer);
+ }
public void Advance(int count)
{
- System.Diagnostics.Debug.Assert(count >= 0, "Advance count must be non-negative.");
- System.Diagnostics.Debug.Assert(Length + count <= Capacity, "Advance would exceed capacity.");
+ Debug.Assert(count >= 0, "Advance count must be non-negative.");
+ Debug.Assert(Length + count <= Capacity, "Advance would exceed capacity.");
Length += count;
}
- public void Clear() => Length = 0;
+ public void Clear()
+ {
+ Length = 0;
+ }
- public ReadOnlySpan AsSpan() => WrittenSpan;
+ public ReadOnlySpan AsSpan()
+ {
+ return WrittenSpan;
+ }
- public override string ToString() => Encoding.UTF8.GetString(WrittenSpan);
-}
\ No newline at end of file
+ public override string ToString()
+ {
+ return Encoding.UTF8.GetString(WrittenSpan);
+ }
+}
\ No newline at end of file
diff --git a/src/ZaString/Extensions/ZaInterpolatedStringHandler.cs b/src/ZaString/Extensions/ZaInterpolatedStringHandler.cs
index 2414729..d233422 100644
--- a/src/ZaString/Extensions/ZaInterpolatedStringHandler.cs
+++ b/src/ZaString/Extensions/ZaInterpolatedStringHandler.cs
@@ -52,6 +52,8 @@ public void AppendFormatted(T value, string? format) where T : ISpanFormattab
_builder.Append(value, format, _provider);
}
- public ZaSpanStringBuilder GetResult() => _builder;
-}
-
+ public ZaSpanStringBuilder GetResult()
+ {
+ return _builder;
+ }
+}
\ No newline at end of file
diff --git a/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs b/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs
index f90c37b..d967a72 100644
--- a/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs
+++ b/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs
@@ -1,7 +1,6 @@
using System.Globalization;
using System.Runtime.CompilerServices;
using ZaString.Core;
-using System.Text;
namespace ZaString.Extensions;
@@ -10,317 +9,315 @@ namespace ZaString.Extensions;
///
public static class ZaSpanStringBuilderExtensions
{
- ///
- /// Appends an interpolated string using a custom handler that writes directly into the builder.
- ///
- public static ref ZaSpanStringBuilder Append(ref this ZaSpanStringBuilder builder,
- [InterpolatedStringHandlerArgument("builder")] ZaInterpolatedStringHandler handler)
+ ///
+ /// Appends an interpolated string using a custom handler that writes directly into the builder.
+ ///
+ public static ref ZaSpanStringBuilder Append(ref this ZaSpanStringBuilder builder,
+ [InterpolatedStringHandlerArgument("builder")]
+ ZaInterpolatedStringHandler handler)
+ {
+ builder = handler.GetResult();
+ return ref builder;
+ }
+
+ ///
+ /// Appends an interpolated string followed by the default line terminator.
+ ///
+ public static ref ZaSpanStringBuilder AppendLine(ref this ZaSpanStringBuilder builder,
+ [InterpolatedStringHandlerArgument("builder")]
+ ZaInterpolatedStringHandler handler)
+ {
+ builder = handler.GetResult();
+ return ref builder.AppendLine();
+ }
+
+ ///
+ /// Attempts to reserve a writable span of the specified size without throwing.
+ /// On success, caller must write up to characters and then call
+ /// with the actual number written.
+ ///
+ /// The builder.
+ /// Requested size to reserve.
+ /// The span the caller can write into.
+ /// true if reserved; false if capacity is insufficient.
+ public static bool TryGetAppendSpan(ref this ZaSpanStringBuilder builder, int size, out Span writeSpan)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(size);
+
+ if (size == 0)
{
- builder = handler.GetResult();
- return ref builder;
+ writeSpan = Span.Empty;
+ return true;
}
- ///
- /// Appends an interpolated string followed by the default line terminator.
- ///
- public static ref ZaSpanStringBuilder AppendLine(ref this ZaSpanStringBuilder builder,
- [InterpolatedStringHandlerArgument("builder")] ZaInterpolatedStringHandler handler)
+ if (builder.RemainingSpan.Length < size)
{
- builder = handler.GetResult();
- return ref builder.AppendLine();
+ writeSpan = Span.Empty;
+ return false;
}
- ///
- /// Attempts to reserve a writable span of the specified size without throwing.
- /// On success, caller must write up to characters and then call with the actual number written.
- ///
- /// The builder.
- /// Requested size to reserve.
- /// The span the caller can write into.
- /// true if reserved; false if capacity is insufficient.
- public static bool TryGetAppendSpan(ref this ZaSpanStringBuilder builder, int size, out Span writeSpan)
- {
- if (size < 0)
- {
- throw new ArgumentOutOfRangeException(nameof(size));
- }
+ writeSpan = builder.RemainingSpan[..size];
+ return true;
+ }
- if (size == 0)
- {
- writeSpan = Span.Empty;
- return true;
- }
+ ///
+ /// Reserves a writable span of the specified size or throws if the capacity is insufficient.
+ /// On success, caller must write up to characters and then call
+ /// with the actual number written.
+ ///
+ public static ref ZaSpanStringBuilder GetAppendSpan(ref this ZaSpanStringBuilder builder, int size, out Span writeSpan)
+ {
+ if (!TryGetAppendSpan(ref builder, size, out writeSpan))
+ {
+ ThrowOutOfRangeException();
+ }
- if (builder.RemainingSpan.Length < size)
- {
- writeSpan = Span.Empty;
- return false;
- }
+ return ref builder;
+ }
- writeSpan = builder.RemainingSpan[..size];
- return true;
+ ///
+ /// Removes the last characters from the written span.
+ ///
+ public static ref ZaSpanStringBuilder RemoveLast(ref this ZaSpanStringBuilder builder, int count)
+ {
+ if (count < 0 || count > builder.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(count));
}
- ///
- /// Reserves a writable span of the specified size or throws if the capacity is insufficient.
- /// On success, caller must write up to characters and then call with the actual number written.
- ///
- public static ref ZaSpanStringBuilder GetAppendSpan(ref this ZaSpanStringBuilder builder, int size, out Span writeSpan)
+ if (count > 0)
{
- if (!TryGetAppendSpan(ref builder, size, out writeSpan))
- {
- ThrowOutOfRangeException();
- }
-
- return ref builder;
+ builder.RemoveLast(count);
}
- ///
- /// Removes the last characters from the written span.
- ///
- public static ref ZaSpanStringBuilder RemoveLast(ref this ZaSpanStringBuilder builder, int count)
+ return ref builder;
+ }
+
+ ///
+ /// Sets the current length to . Must be between 0 and Capacity.
+ /// If is less than the current Length, the content is logically truncated.
+ ///
+ public static ref ZaSpanStringBuilder SetLength(ref this ZaSpanStringBuilder builder, int newLength)
+ {
+ if (newLength < 0 || newLength > builder.Length)
{
- if (count < 0 || count > builder.Length)
- {
- throw new ArgumentOutOfRangeException(nameof(count));
- }
+ throw new ArgumentOutOfRangeException(nameof(newLength));
+ }
- if (count > 0)
- {
- builder.RemoveLast(count);
- }
+ builder.SetLength(newLength);
+ return ref builder;
+ }
- return ref builder;
+ ///
+ /// Ensures the written span ends with the specified character; appends it if needed.
+ ///
+ public static ref ZaSpanStringBuilder EnsureEndsWith(ref this ZaSpanStringBuilder builder, char value)
+ {
+ if (builder.Length == 0 || builder[^1] != value)
+ {
+ builder.Append(value);
}
- ///
- /// Sets the current length to . Must be between 0 and Capacity.
- /// If is less than the current Length, the content is logically truncated.
- ///
- public static ref ZaSpanStringBuilder SetLength(ref this ZaSpanStringBuilder builder, int newLength)
- {
- if (newLength < 0 || newLength > builder.Length)
- {
- throw new ArgumentOutOfRangeException(nameof(newLength));
- }
+ return ref builder;
+ }
+ ///
+ /// Appends the specified character repeated times.
+ ///
+ /// Thrown if count is negative or buffer is too small.
+ public static ref ZaSpanStringBuilder AppendRepeat(ref this ZaSpanStringBuilder builder, char value, int count)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(count);
- builder.SetLength(newLength);
+ if (count == 0)
+ {
return ref builder;
}
- ///
- /// Ensures the written span ends with the specified character; appends it if needed.
- ///
- public static ref ZaSpanStringBuilder EnsureEndsWith(ref this ZaSpanStringBuilder builder, char value)
+ if (builder.RemainingSpan.Length < count)
{
- if (builder.Length == 0 || builder[builder.Length - 1] != value)
- {
- builder.Append(value);
- }
-
- return ref builder;
+ ThrowOutOfRangeException();
}
- ///
- /// Appends the specified character repeated times.
- ///
- /// Thrown if count is negative or buffer is too small.
- public static ref ZaSpanStringBuilder AppendRepeat(ref this ZaSpanStringBuilder builder, char value, int count)
- {
- if (count < 0)
- {
- throw new ArgumentOutOfRangeException(nameof(count));
- }
- if (count == 0)
- {
- return ref builder;
- }
+ builder.RemainingSpan[..count].Fill(value);
+ builder.Advance(count);
+ return ref builder;
+ }
- if (builder.RemainingSpan.Length < count)
- {
- ThrowOutOfRangeException();
- }
+ ///
+ /// Attempts to append the specified character repeated times without throwing.
+ ///
+ /// true if appended; otherwise false when capacity is insufficient.
+ public static bool TryAppendRepeat(ref this ZaSpanStringBuilder builder, char value, int count)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(count);
- builder.RemainingSpan[..count].Fill(value);
- builder.Advance(count);
- return ref builder;
+ if (count == 0)
+ {
+ return true;
}
- ///
- /// Attempts to append the specified character repeated times without throwing.
- ///
- /// true if appended; otherwise false when capacity is insufficient.
- public static bool TryAppendRepeat(ref this ZaSpanStringBuilder builder, char value, int count)
+ if (builder.RemainingSpan.Length < count)
{
- if (count < 0)
- {
- throw new ArgumentOutOfRangeException(nameof(count));
- }
+ return false;
+ }
- if (count == 0)
- {
- return true;
- }
+ builder.RemainingSpan[..count].Fill(value);
+ builder.Advance(count);
+ return true;
+ }
- if (builder.RemainingSpan.Length < count)
+ ///
+ /// Appends the elements separated by .
+ /// Null elements are treated as empty strings.
+ ///
+ public static ref ZaSpanStringBuilder AppendJoin(ref this ZaSpanStringBuilder builder, ReadOnlySpan separator, params string?[] values)
+ {
+ for (var i = 0; i < values.Length; i++)
+ {
+ if (i > 0)
{
- return false;
+ builder.Append(separator);
}
- builder.RemainingSpan[..count].Fill(value);
- builder.Advance(count);
- return true;
- }
-
- ///
- /// Appends the elements separated by .
- /// Null elements are treated as empty strings.
- ///
- public static ref ZaSpanStringBuilder AppendJoin(ref this ZaSpanStringBuilder builder, ReadOnlySpan separator, params string?[] values)
- {
- for (var i = 0; i < values.Length; i++)
+ var s = values[i];
+ if (s is not null)
{
- if (i > 0)
- {
- builder.Append(separator);
- }
-
- var s = values[i];
- if (s is not null)
- {
- builder.Append(s);
- }
+ builder.Append(s);
}
-
- return ref builder;
}
- ///
- /// Appends the elements of separated by .
- ///
- public static ref ZaSpanStringBuilder AppendJoin(ref this ZaSpanStringBuilder builder, ReadOnlySpan separator, ReadOnlySpan values, ReadOnlySpan format = default, IFormatProvider? provider = null)
- where T : ISpanFormattable
- {
- for (var i = 0; i < values.Length; i++)
- {
- if (i > 0)
- {
- builder.Append(separator);
- }
-
- builder.Append(values[i], format, provider);
- }
+ return ref builder;
+ }
- return ref builder;
- }
- ///
- /// Attempts to append a read-only span of characters to the builder without throwing.
- ///
- /// The builder instance.
- /// The span of characters to append.
- /// true if the value was appended; otherwise false if there was not enough capacity.
- public static bool TryAppend(ref this ZaSpanStringBuilder builder, ReadOnlySpan value)
+ ///
+ /// Appends the elements of separated by .
+ ///
+ public static ref ZaSpanStringBuilder AppendJoin(ref this ZaSpanStringBuilder builder, ReadOnlySpan separator, ReadOnlySpan values, ReadOnlySpan format = default, IFormatProvider? provider = null)
+ where T : ISpanFormattable
+ {
+ for (var i = 0; i < values.Length; i++)
{
- if (value.Length > builder.RemainingSpan.Length)
+ if (i > 0)
{
- return false;
+ builder.Append(separator);
}
- value.CopyTo(builder.RemainingSpan);
- builder.Advance(value.Length);
- return true;
+ builder.Append(values[i], format, provider);
}
- ///
- /// Attempts to append a string to the builder without throwing.
- ///
- /// The builder instance.
- /// The string to append. If null, this is a no-op and returns true.
- /// true if the value was appended; otherwise false if there was not enough capacity.
- public static bool TryAppend(ref this ZaSpanStringBuilder builder, string? value)
+ return ref builder;
+ }
+ ///
+ /// Attempts to append a read-only span of characters to the builder without throwing.
+ ///
+ /// The builder instance.
+ /// The span of characters to append.
+ /// true if the value was appended; otherwise false if there was not enough capacity.
+ public static bool TryAppend(ref this ZaSpanStringBuilder builder, ReadOnlySpan value)
+ {
+ if (value.Length > builder.RemainingSpan.Length)
{
- return value is null || builder.TryAppend(value.AsSpan());
+ return false;
}
- ///
- /// Attempts to append a single character to the builder without throwing.
- ///
- /// The builder instance.
- /// The character to append.
- /// true if the value was appended; otherwise false if there was not enough capacity.
- public static bool TryAppend(ref this ZaSpanStringBuilder builder, char value)
- {
- if (builder.RemainingSpan.Length < 1)
- {
- return false;
- }
+ value.CopyTo(builder.RemainingSpan);
+ builder.Advance(value.Length);
+ return true;
+ }
- builder.RemainingSpan[0] = value;
- builder.Advance(1);
- return true;
- }
+ ///
+ /// Attempts to append a string to the builder without throwing.
+ ///
+ /// The builder instance.
+ /// The string to append. If null, this is a no-op and returns true.
+ /// true if the value was appended; otherwise false if there was not enough capacity.
+ public static bool TryAppend(ref this ZaSpanStringBuilder builder, string? value)
+ {
+ return value is null || builder.TryAppend(value.AsSpan());
+ }
- ///
- /// Attempts to append a value of a type that implements without throwing.
- ///
- /// The type of the value, which must implement ISpanFormattable.
- /// The builder instance.
- /// The value to format and append.
- /// An optional format string for the value.
- /// Format provider to use. If null, is used.
- /// true if the value was formatted and appended; otherwise false if there was not enough capacity or formatting failed.
- public static bool TryAppend(ref this ZaSpanStringBuilder builder, T value, ReadOnlySpan format = default, IFormatProvider? provider = null) where T : ISpanFormattable
+ ///
+ /// Attempts to append a single character to the builder without throwing.
+ ///
+ /// The builder instance.
+ /// The character to append.
+ /// true if the value was appended; otherwise false if there was not enough capacity.
+ public static bool TryAppend(ref this ZaSpanStringBuilder builder, char value)
+ {
+ if (builder.RemainingSpan.Length < 1)
{
- provider ??= CultureInfo.InvariantCulture;
+ return false;
+ }
- if (!value.TryFormat(builder.RemainingSpan, out var charsWritten, format, provider))
- {
- return false;
- }
+ builder.RemainingSpan[0] = value;
+ builder.Advance(1);
+ return true;
+ }
- builder.Advance(charsWritten);
- return true;
- }
+ ///
+ /// Attempts to append a value of a type that implements without throwing.
+ ///
+ /// The type of the value, which must implement ISpanFormattable.
+ /// The builder instance.
+ /// The value to format and append.
+ /// An optional format string for the value.
+ /// Format provider to use. If null, is used.
+ ///
+ /// true if the value was formatted and appended; otherwise false if there was not enough capacity
+ /// or formatting failed.
+ ///
+ public static bool TryAppend(ref this ZaSpanStringBuilder builder, T value, ReadOnlySpan format = default, IFormatProvider? provider = null) where T : ISpanFormattable
+ {
+ provider ??= CultureInfo.InvariantCulture;
- ///
- /// Attempts to append the default line terminator to the builder without throwing.
- ///
- /// The builder instance.
- /// true if the newline was appended; otherwise false if there was not enough capacity.
- public static bool TryAppendLine(ref this ZaSpanStringBuilder builder)
+ if (!value.TryFormat(builder.RemainingSpan, out var charsWritten, format, provider))
{
- var newline = Environment.NewLine.AsSpan();
- return builder.TryAppend(newline);
+ return false;
}
- ///
- /// Attempts to append a string followed by the default line terminator to the builder without throwing.
- /// The operation is atomic with respect to capacity: if there is not enough space for both, nothing is written.
- ///
- /// The builder instance.
- /// The string to append. If null, only the newline is appended.
- /// true if the string and newline were appended; otherwise false if there was not enough capacity.
- public static bool TryAppendLine(ref this ZaSpanStringBuilder builder, string? value)
- {
- var valueLength = value?.Length ?? 0;
- var newlineLength = Environment.NewLine.Length;
- var required = valueLength + newlineLength;
+ builder.Advance(charsWritten);
+ return true;
+ }
- if (required > builder.RemainingSpan.Length)
- {
- return false;
- }
+ ///
+ /// Attempts to append the default line terminator to the builder without throwing.
+ ///
+ /// The builder instance.
+ /// true if the newline was appended; otherwise false if there was not enough capacity.
+ public static bool TryAppendLine(ref this ZaSpanStringBuilder builder)
+ {
+ var newline = Environment.NewLine.AsSpan();
+ return builder.TryAppend(newline);
+ }
- if (valueLength > 0)
- {
- value!.AsSpan().CopyTo(builder.RemainingSpan);
- builder.Advance(valueLength);
- }
+ ///
+ /// Attempts to append a string followed by the default line terminator to the builder without throwing.
+ /// The operation is atomic with respect to capacity: if there is not enough space for both, nothing is written.
+ ///
+ /// The builder instance.
+ /// The string to append. If null, only the newline is appended.
+ /// true if the string and newline were appended; otherwise false if there was not enough capacity.
+ public static bool TryAppendLine(ref this ZaSpanStringBuilder builder, string? value)
+ {
+ var valueLength = value?.Length ?? 0;
+ var newlineLength = Environment.NewLine.Length;
+ var required = valueLength + newlineLength;
- Environment.NewLine.AsSpan().CopyTo(builder.RemainingSpan);
- builder.Advance(newlineLength);
- return true;
+ if (required > builder.RemainingSpan.Length)
+ {
+ return false;
+ }
+
+ if (valueLength > 0)
+ {
+ value!.AsSpan().CopyTo(builder.RemainingSpan);
+ builder.Advance(valueLength);
}
+
+ Environment.NewLine.AsSpan().CopyTo(builder.RemainingSpan);
+ builder.Advance(newlineLength);
+ return true;
+ }
///
/// Appends a read-only span of characters to the builder.
///
@@ -409,7 +406,7 @@ public static ref ZaSpanStringBuilder Append(ref this ZaSpanStringBuilder bui
/// The builder instance.
/// The value to format and append.
/// An optional format string for the value.
- /// Format provider to use. If null, is used.
+ /// Format provider to use. If null, is used.
/// A reference to the builder to allow for method chaining.
/// Thrown if the buffer is too small to hold the formatted value.
/// Thrown if the value cannot be formatted correctly.
@@ -448,6 +445,7 @@ public static ref ZaSpanStringBuilder AppendLine(ref this ZaSpanStringBuilder bu
{
builder.Append(value);
}
+
return ref builder.AppendLine();
}
@@ -476,6 +474,7 @@ public static ref ZaSpanStringBuilder AppendIf(ref this ZaSpanStringBuilder buil
{
builder.Append(value);
}
+
return ref builder;
}
@@ -492,6 +491,7 @@ public static ref ZaSpanStringBuilder AppendIf(ref this ZaSpanStringBuilder buil
{
builder.Append(value);
}
+
return ref builder;
}
@@ -508,6 +508,7 @@ public static ref ZaSpanStringBuilder AppendIf(ref this ZaSpanStringBuilder buil
{
builder.Append(value);
}
+
return ref builder;
}
@@ -526,6 +527,7 @@ public static ref ZaSpanStringBuilder AppendIf(ref this ZaSpanStringBuilder b
{
builder.Append(value, format);
}
+
return ref builder;
}
@@ -537,372 +539,453 @@ private static void ThrowOutOfRangeException()
throw new ArgumentOutOfRangeException("value", "The destination buffer is too small.");
}
- // Escaping helpers
+ // Escaping helpers
- public static ref ZaSpanStringBuilder AppendJsonEscaped(ref this ZaSpanStringBuilder builder, ReadOnlySpan value)
+ public static ref ZaSpanStringBuilder AppendJsonEscaped(ref this ZaSpanStringBuilder builder, ReadOnlySpan value)
+ {
+ if (!TryAppendJsonEscaped(ref builder, value))
{
- if (!TryAppendJsonEscaped(ref builder, value))
- {
- ThrowOutOfRangeException();
- }
- return ref builder;
+ ThrowOutOfRangeException();
}
- public static bool TryAppendJsonEscaped(ref this ZaSpanStringBuilder builder, ReadOnlySpan value)
+ return ref builder;
+ }
+
+ public static bool TryAppendJsonEscaped(ref this ZaSpanStringBuilder builder, ReadOnlySpan value)
+ {
+ var required = GetJsonEscapedLength(value);
+ if (required > builder.RemainingSpan.Length)
{
- var required = GetJsonEscapedLength(value);
- if (required > builder.RemainingSpan.Length)
- {
- return false;
- }
- if (required == value.Length)
- {
- value.CopyTo(builder.RemainingSpan);
- builder.Advance(value.Length);
- return true;
- }
- var dest = builder.RemainingSpan;
- var w = 0;
- for (var i = 0; i < value.Length; i++)
- {
- var c = value[i];
- switch (c)
- {
- case '"': dest[w++] = '\\'; dest[w++] = '"'; break;
- case '\\': dest[w++] = '\\'; dest[w++] = '\\'; break;
- case '\b': dest[w++] = '\\'; dest[w++] = 'b'; break;
- case '\f': dest[w++] = '\\'; dest[w++] = 'f'; break;
- case '\n': dest[w++] = '\\'; dest[w++] = 'n'; break;
- case '\r': dest[w++] = '\\'; dest[w++] = 'r'; break;
- case '\t': dest[w++] = '\\'; dest[w++] = 't'; break;
- default:
- if (c < ' ')
- {
- dest[w++] = '\\';
- dest[w++] = 'u';
- dest[w++] = '0';
- dest[w++] = '0';
- WriteHexByte((byte)c, dest.Slice(w, 2));
- w += 2;
- }
- else
- {
- dest[w++] = c;
- }
- break;
- }
- }
- builder.Advance(required);
+ return false;
+ }
+
+ if (required == value.Length)
+ {
+ value.CopyTo(builder.RemainingSpan);
+ builder.Advance(value.Length);
return true;
}
- private static int GetJsonEscapedLength(ReadOnlySpan value)
+ var dest = builder.RemainingSpan;
+ var w = 0;
+ for (var i = 0; i < value.Length; i++)
{
- var extra = 0;
- for (var i = 0; i < value.Length; i++)
+ var c = value[i];
+ switch (c)
{
- var c = value[i];
- switch (c)
- {
- case '"':
- case '\\':
- case '\b':
- case '\f':
- case '\n':
- case '\r':
- case '\t':
- extra += 1; // becomes two chars instead of one
- break;
- default:
- if (c < ' ')
- {
- extra += 5; // \u00XX adds 5 extra over the original 1
- }
- break;
- }
+ case '"':
+ dest[w++] = '\\';
+ dest[w++] = '"';
+ break;
+ case '\\':
+ dest[w++] = '\\';
+ dest[w++] = '\\';
+ break;
+ case '\b':
+ dest[w++] = '\\';
+ dest[w++] = 'b';
+ break;
+ case '\f':
+ dest[w++] = '\\';
+ dest[w++] = 'f';
+ break;
+ case '\n':
+ dest[w++] = '\\';
+ dest[w++] = 'n';
+ break;
+ case '\r':
+ dest[w++] = '\\';
+ dest[w++] = 'r';
+ break;
+ case '\t':
+ dest[w++] = '\\';
+ dest[w++] = 't';
+ break;
+
+ default:
+ if (c < ' ')
+ {
+ dest[w++] = '\\';
+ dest[w++] = 'u';
+ dest[w++] = '0';
+ dest[w++] = '0';
+ WriteHexByte((byte)c, dest.Slice(w, 2));
+ w += 2;
+ }
+ else
+ {
+ dest[w++] = c;
+ }
+
+ break;
+ }
+ }
+
+ builder.Advance(required);
+ return true;
+ }
+
+ private static int GetJsonEscapedLength(ReadOnlySpan value)
+ {
+ var extra = 0;
+ foreach (var c in value)
+ {
+ switch (c)
+ {
+ case '"':
+ case '\\':
+ case '\b':
+ case '\f':
+ case '\n':
+ case '\r':
+ case '\t':
+ extra += 1; // becomes two chars instead of one
+ break;
+
+ default:
+ if (c < ' ')
+ {
+ extra += 5; // \u00XX adds 5 extra over the original 1
+ }
+
+ break;
}
- return value.Length + extra;
}
- private static void WriteHexByte(byte b, Span dest)
+ return value.Length + extra;
+ }
+
+ private static void WriteHexByte(byte b, Span dest)
+ {
+ const string hex = "0123456789ABCDEF";
+ dest[0] = hex[b >> 4 & 0xF];
+ dest[1] = hex[b & 0xF];
+ }
+
+ public static ref ZaSpanStringBuilder AppendHtmlEscaped(ref this ZaSpanStringBuilder builder, ReadOnlySpan value)
+ {
+ if (!TryAppendHtmlEscaped(ref builder, value))
{
- const string hex = "0123456789ABCDEF";
- dest[0] = hex[(b >> 4) & 0xF];
- dest[1] = hex[b & 0xF];
+ ThrowOutOfRangeException();
}
- public static ref ZaSpanStringBuilder AppendHtmlEscaped(ref this ZaSpanStringBuilder builder, ReadOnlySpan value)
+ return ref builder;
+ }
+
+ public static bool TryAppendHtmlEscaped(ref this ZaSpanStringBuilder builder, ReadOnlySpan value)
+ {
+ var required = GetHtmlEscapedLength(value);
+ if (required > builder.RemainingSpan.Length)
{
- if (!TryAppendHtmlEscaped(ref builder, value))
- {
- ThrowOutOfRangeException();
- }
- return ref builder;
+ return false;
}
- public static bool TryAppendHtmlEscaped(ref this ZaSpanStringBuilder builder, ReadOnlySpan value)
+ if (required == value.Length)
{
- var required = GetHtmlEscapedLength(value);
- if (required > builder.RemainingSpan.Length)
- {
- return false;
- }
- if (required == value.Length)
- {
- value.CopyTo(builder.RemainingSpan);
- builder.Advance(value.Length);
- return true;
- }
- var dest = builder.RemainingSpan;
- var w = 0;
- for (var i = 0; i < value.Length; i++)
- {
- switch (value[i])
- {
- case '&': dest[w++] = '&'; dest[w++] = 'a'; dest[w++] = 'm'; dest[w++] = 'p'; dest[w++] = ';'; break; // &
- case '<': dest[w++] = '&'; dest[w++] = 'l'; dest[w++] = 't'; dest[w++] = ';'; break; // <
- case '>': dest[w++] = '&'; dest[w++] = 'g'; dest[w++] = 't'; dest[w++] = ';'; break; // >
- case '"': dest[w++] = '&'; dest[w++] = 'q'; dest[w++] = 'u'; dest[w++] = 'o'; dest[w++] = 't'; dest[w++] = ';'; break; // "
- case '\'': dest[w++] = '&'; dest[w++] = '#'; dest[w++] = '3'; dest[w++] = '9'; dest[w++] = ';'; break; // '
- default: dest[w++] = value[i]; break;
- }
- }
- builder.Advance(required);
+ value.CopyTo(builder.RemainingSpan);
+ builder.Advance(value.Length);
return true;
}
- private static int GetHtmlEscapedLength(ReadOnlySpan value)
+ var dest = builder.RemainingSpan;
+ var w = 0;
+ foreach (var t in value)
+ {
+ switch (t)
+ {
+ case '&':
+ dest[w++] = '&';
+ dest[w++] = 'a';
+ dest[w++] = 'm';
+ dest[w++] = 'p';
+ dest[w++] = ';';
+ break; // &
+ case '<':
+ dest[w++] = '&';
+ dest[w++] = 'l';
+ dest[w++] = 't';
+ dest[w++] = ';';
+ break; // <
+ case '>':
+ dest[w++] = '&';
+ dest[w++] = 'g';
+ dest[w++] = 't';
+ dest[w++] = ';';
+ break; // >
+ case '"':
+ dest[w++] = '&';
+ dest[w++] = 'q';
+ dest[w++] = 'u';
+ dest[w++] = 'o';
+ dest[w++] = 't';
+ dest[w++] = ';';
+ break; // "
+ case '\'':
+ dest[w++] = '&';
+ dest[w++] = '#';
+ dest[w++] = '3';
+ dest[w++] = '9';
+ dest[w++] = ';';
+ break; // '
+ default: dest[w++] = t; break;
+ }
+ }
+
+ builder.Advance(required);
+ return true;
+ }
+
+ private static int GetHtmlEscapedLength(ReadOnlySpan value)
+ {
+ var extra = 0;
+ foreach (var t in value)
{
- var extra = 0;
- for (var i = 0; i < value.Length; i++)
+ switch (t)
{
- switch (value[i])
- {
- case '&': extra += 4; break; // & (5) - 1 original = +4
- case '<':
- case '>': extra += 3; break; // < or > (4) -1 = +3
- case '"': extra += 5; break; // " (6) -1 = +5
- case '\'': extra += 4; break; // ' (5) -1 = +4
- }
+ case '&': extra += 4; break; // & (5) - 1 original = +4
+ case '<':
+ case '>': extra += 3; break; // < or > (4) -1 = +3
+ case '"': extra += 5; break; // " (6) -1 = +5
+ case '\'': extra += 4; break; // ' (5) -1 = +4
}
- return value.Length + extra;
}
- public static ref ZaSpanStringBuilder AppendCsvEscaped(ref this ZaSpanStringBuilder builder, ReadOnlySpan value)
+ return value.Length + extra;
+ }
+
+ public static ref ZaSpanStringBuilder AppendCsvEscaped(ref this ZaSpanStringBuilder builder, ReadOnlySpan value)
+ {
+ if (!TryAppendCsvEscaped(ref builder, value))
{
- if (!TryAppendCsvEscaped(ref builder, value))
- {
- ThrowOutOfRangeException();
- }
- return ref builder;
+ ThrowOutOfRangeException();
}
- public static bool TryAppendCsvEscaped(ref this ZaSpanStringBuilder builder, ReadOnlySpan value)
+ return ref builder;
+ }
+
+ public static bool TryAppendCsvEscaped(ref this ZaSpanStringBuilder builder, ReadOnlySpan value)
+ {
+ var needsQuote = NeedsCsvQuoting(value);
+ if (!needsQuote)
{
- var needsQuote = NeedsCsvQuoting(value);
- if (!needsQuote)
- {
- return builder.TryAppend(value);
- }
- var quoteCount = 0;
- for (var i = 0; i < value.Length; i++) if (value[i] == '"') quoteCount++;
- var required = value.Length + quoteCount + 2;
- if (required > builder.RemainingSpan.Length)
- {
- return false;
- }
- var dest = builder.RemainingSpan;
- var w = 0;
- dest[w++] = '"';
- for (var i = 0; i < value.Length; i++)
- {
- var c = value[i];
- dest[w++] = c;
- if (c == '"')
- {
- dest[w++] = '"';
- }
- }
- dest[w++] = '"';
- builder.Advance(required);
- return true;
+ return builder.TryAppend(value);
+ }
+
+ var quoteCount = 0;
+ foreach (var t in value)
+ if (t == '"')
+ quoteCount++;
+
+ var required = value.Length + quoteCount + 2;
+ if (required > builder.RemainingSpan.Length)
+ {
+ return false;
}
- private static bool NeedsCsvQuoting(ReadOnlySpan value)
+ var dest = builder.RemainingSpan;
+ var w = 0;
+ dest[w++] = '"';
+ foreach (var c in value)
{
- if (value.Length == 0) return false;
- if (char.IsWhiteSpace(value[0]) || char.IsWhiteSpace(value[^1])) return true;
- for (var i = 0; i < value.Length; i++)
+ dest[w++] = c;
+ if (c == '"')
{
- var c = value[i];
- if (c == ',' || c == '"' || c == '\n' || c == '\r') return true;
+ dest[w++] = '"';
}
- return false;
}
- // URL encoding and composition helpers
+ dest[w] = '"';
+ builder.Advance(required);
+ return true;
+ }
- private static bool IsUnreservedAscii(char c)
+ private static bool NeedsCsvQuoting(ReadOnlySpan value)
+ {
+ if (value.Length == 0) return false;
+ if (char.IsWhiteSpace(value[0]) || char.IsWhiteSpace(value[^1])) return true;
+ foreach (var c in value)
{
- return (c >= 'A' && c <= 'Z') ||
- (c >= 'a' && c <= 'z') ||
- (c >= '0' && c <= '9') ||
- c is '-' or '_' or '.' or '~';
+ if (c is ',' or '"' or '\n' or '\r') return true;
}
- public static ref ZaSpanStringBuilder AppendUrlEncoded(ref this ZaSpanStringBuilder builder, ReadOnlySpan value)
+ return false;
+ }
+
+ // URL encoding and composition helpers
+
+ private static bool IsUnreservedAscii(char c)
+ {
+ return c is >= 'A' and <= 'Z' or >= 'a' and <= 'z' or < '0' or > '9' or '-' or '_' or '.' or '~';
+ }
+
+ public static ref ZaSpanStringBuilder AppendUrlEncoded(ref this ZaSpanStringBuilder builder, ReadOnlySpan value)
+ {
+ if (!TryAppendUrlEncoded(ref builder, value))
{
- if (!TryAppendUrlEncoded(ref builder, value))
- {
- ThrowOutOfRangeException();
- }
- return ref builder;
+ ThrowOutOfRangeException();
}
- public static bool TryAppendUrlEncoded(ref this ZaSpanStringBuilder builder, ReadOnlySpan value)
+ return ref builder;
+ }
+
+ public static bool TryAppendUrlEncoded(ref this ZaSpanStringBuilder builder, ReadOnlySpan value)
+ {
+ var required = GetUrlEncodedLength(value);
+ if (required > builder.RemainingSpan.Length)
{
- var required = GetUrlEncodedLength(value);
- if (required > builder.RemainingSpan.Length)
- {
- return false;
- }
+ return false;
+ }
- var dest = builder.RemainingSpan;
- var w = 0;
- for (int i = 0; i < value.Length; i++)
+ var dest = builder.RemainingSpan;
+ var w = 0;
+ for (var i = 0; i < value.Length; i++)
+ {
+ var c = value[i];
+ if (c <= 0x7F)
{
- var c = value[i];
- if (c <= 0x7F)
+ if (IsUnreservedAscii(c))
{
- if (IsUnreservedAscii(c))
- {
- dest[w++] = c;
- }
- else
- {
- dest[w++] = '%';
- WriteHexByte((byte)c, dest.Slice(w, 2));
- w += 2;
- }
- }
- else if (char.IsHighSurrogate(c) && i + 1 < value.Length && char.IsLowSurrogate(value[i + 1]))
- {
- var high = c;
- var low = value[++i];
- var codePoint = 0x10000 + (((high - 0xD800) << 10) | (low - 0xDC00));
- w += PercentEncodeUtf8FromCodePoint(codePoint, dest.Slice(w));
+ dest[w++] = c;
}
else
{
- var codePoint = (int)c;
- w += PercentEncodeUtf8FromCodePoint(codePoint, dest.Slice(w));
+ dest[w++] = '%';
+ WriteHexByte((byte)c, dest.Slice(w, 2));
+ w += 2;
}
}
-
- builder.Advance(required);
- return true;
+ else if (char.IsHighSurrogate(c) && i + 1 < value.Length && char.IsLowSurrogate(value[i + 1]))
+ {
+ var high = c;
+ var low = value[++i];
+ var codePoint = 0x10000 + (high - 0xD800 << 10 | low - 0xDC00);
+ w += PercentEncodeUtf8FromCodePoint(codePoint, dest.Slice(w));
+ }
+ else
+ {
+ var codePoint = (int)c;
+ w += PercentEncodeUtf8FromCodePoint(codePoint, dest.Slice(w));
+ }
}
- private static int GetUrlEncodedLength(ReadOnlySpan value)
+ builder.Advance(required);
+ return true;
+ }
+
+ private static int GetUrlEncodedLength(ReadOnlySpan value)
+ {
+ var length = 0;
+ for (var i = 0; i < value.Length; i++)
{
- var length = 0;
- for (int i = 0; i < value.Length; i++)
+ var c = value[i];
+ if (c <= 0x7F)
{
- var c = value[i];
- if (c <= 0x7F)
- {
- length += IsUnreservedAscii(c) ? 1 : 3;
- }
- else if (char.IsHighSurrogate(c) && i + 1 < value.Length && char.IsLowSurrogate(value[i + 1]))
- {
- length += 4 * 3; // 4 UTF-8 bytes -> %HH %HH %HH %HH
- i++; // consume low surrogate
- }
- else
- {
- // Non-surrogate BMP char: 0x80..0x7FF => 2 bytes; 0x800..0xFFFF => 3 bytes
- length += (c <= 0x7FF) ? 2 * 3 : 3 * 3;
- }
+ length += IsUnreservedAscii(c) ? 1 : 3;
+ }
+ else if (char.IsHighSurrogate(c) && i + 1 < value.Length && char.IsLowSurrogate(value[i + 1]))
+ {
+ length += 4 * 3; // 4 UTF-8 bytes -> %HH %HH %HH %HH
+ i++; // consume low surrogate
+ }
+ else
+ {
+ // Non-surrogate BMP char: 0x80..0x7FF => 2 bytes; 0x800..0xFFFF => 3 bytes
+ length += c <= 0x7FF ? 2 * 3 : 3 * 3;
}
- return length;
}
- private static int PercentEncodeUtf8FromCodePoint(int codePoint, Span dest)
+ return length;
+ }
+
+ private static int PercentEncodeUtf8FromCodePoint(int codePoint, Span dest)
+ {
+ switch (codePoint)
{
// Returns number of chars written to dest (multiple of 3)
- if (codePoint <= 0x7F)
- {
+ case <= 0x7F:
dest[0] = '%';
WriteHexByte((byte)codePoint, dest.Slice(1, 2));
return 3;
- }
- if (codePoint <= 0x7FF)
+ case <= 0x7FF:
{
- var b1 = (byte)(0b1100_0000 | (codePoint >> 6));
- var b2 = (byte)(0b1000_0000 | (codePoint & 0b0011_1111));
- dest[0] = '%'; WriteHexByte(b1, dest.Slice(1, 2));
- dest[3] = '%'; WriteHexByte(b2, dest.Slice(4, 2));
+ var b1 = (byte)(0b1100_0000 | codePoint >> 6);
+ var b2 = (byte)(0b1000_0000 | codePoint & 0b0011_1111);
+ dest[0] = '%';
+ WriteHexByte(b1, dest.Slice(1, 2));
+ dest[3] = '%';
+ WriteHexByte(b2, dest.Slice(4, 2));
return 6;
}
- if (codePoint <= 0xFFFF)
+
+ case <= 0xFFFF:
{
- var b1 = (byte)(0b1110_0000 | (codePoint >> 12));
- var b2 = (byte)(0b1000_0000 | ((codePoint >> 6) & 0b0011_1111));
- var b3 = (byte)(0b1000_0000 | (codePoint & 0b0011_1111));
- dest[0] = '%'; WriteHexByte(b1, dest.Slice(1, 2));
- dest[3] = '%'; WriteHexByte(b2, dest.Slice(4, 2));
- dest[6] = '%'; WriteHexByte(b3, dest.Slice(7, 2));
+ var b1 = (byte)(0b1110_0000 | codePoint >> 12);
+ var b2 = (byte)(0b1000_0000 | codePoint >> 6 & 0b0011_1111);
+ var b3 = (byte)(0b1000_0000 | codePoint & 0b0011_1111);
+ dest[0] = '%';
+ WriteHexByte(b1, dest.Slice(1, 2));
+ dest[3] = '%';
+ WriteHexByte(b2, dest.Slice(4, 2));
+ dest[6] = '%';
+ WriteHexByte(b3, dest.Slice(7, 2));
return 9;
}
- else
+
+ default:
{
- var b1 = (byte)(0b1111_0000 | (codePoint >> 18));
- var b2 = (byte)(0b1000_0000 | ((codePoint >> 12) & 0b0011_1111));
- var b3 = (byte)(0b1000_0000 | ((codePoint >> 6) & 0b0011_1111));
- var b4 = (byte)(0b1000_0000 | (codePoint & 0b0011_1111));
- dest[0] = '%'; WriteHexByte(b1, dest.Slice(1, 2));
- dest[3] = '%'; WriteHexByte(b2, dest.Slice(4, 2));
- dest[6] = '%'; WriteHexByte(b3, dest.Slice(7, 2));
- dest[9] = '%'; WriteHexByte(b4, dest.Slice(10, 2));
+ var b1 = (byte)(0b1111_0000 | codePoint >> 18);
+ var b2 = (byte)(0b1000_0000 | codePoint >> 12 & 0b0011_1111);
+ var b3 = (byte)(0b1000_0000 | codePoint >> 6 & 0b0011_1111);
+ var b4 = (byte)(0b1000_0000 | codePoint & 0b0011_1111);
+ dest[0] = '%';
+ WriteHexByte(b1, dest.Slice(1, 2));
+ dest[3] = '%';
+ WriteHexByte(b2, dest.Slice(4, 2));
+ dest[6] = '%';
+ WriteHexByte(b3, dest.Slice(7, 2));
+ dest[9] = '%';
+ WriteHexByte(b4, dest.Slice(10, 2));
return 12;
}
}
+ }
- public static ref ZaSpanStringBuilder AppendPathSegment(ref this ZaSpanStringBuilder builder, ReadOnlySpan segment, char separator = '/')
+ public static ref ZaSpanStringBuilder AppendPathSegment(ref this ZaSpanStringBuilder builder, ReadOnlySpan segment, char separator = '/')
+ {
+ if (builder.Length > 0 && builder[^1] != separator)
{
- if (builder.Length > 0 && builder[builder.Length - 1] != separator)
- {
- builder.Append(separator);
- }
-
- // Trim leading separators in segment
- int start = 0;
- while (start < segment.Length && segment[start] == separator) start++;
- if (start < segment.Length)
- {
- builder.Append(segment[start..]);
- }
- return ref builder;
+ builder.Append(separator);
}
- public static ref ZaSpanStringBuilder AppendQueryParam(ref this ZaSpanStringBuilder builder, ReadOnlySpan key, ReadOnlySpan value, bool urlEncode = true, bool isFirst = false)
+ // Trim leading separators in segment
+ var start = 0;
+ while (start < segment.Length && segment[start] == separator) start++;
+ if (start < segment.Length)
{
- builder.Append(isFirst ? '?' : '&');
- if (urlEncode)
- {
- builder.AppendUrlEncoded(key);
- builder.Append('=');
- builder.AppendUrlEncoded(value);
- }
- else
- {
- builder.Append(key);
- builder.Append('=');
- builder.Append(value);
- }
+ builder.Append(segment[start..]);
+ }
- return ref builder;
+ return ref builder;
+ }
+
+ public static ref ZaSpanStringBuilder AppendQueryParam(ref this ZaSpanStringBuilder builder, ReadOnlySpan key, ReadOnlySpan value, bool urlEncode = true, bool isFirst = false)
+ {
+ builder.Append(isFirst ? '?' : '&');
+ if (urlEncode)
+ {
+ builder.AppendUrlEncoded(key);
+ builder.Append('=');
+ builder.AppendUrlEncoded(value);
}
+ else
+ {
+ builder.Append(key);
+ builder.Append('=');
+ builder.Append(value);
+ }
+
+ return ref builder;
+ }
public static ref ZaSpanStringBuilder AppendFormat(ref this ZaSpanStringBuilder builder, string format, params object?[] args)
{
diff --git a/src/ZaString/Extensions/ZaUtf8SpanWriterExtensions.cs b/src/ZaString/Extensions/ZaUtf8SpanWriterExtensions.cs
index b0c344e..a8ad280 100644
--- a/src/ZaString/Extensions/ZaUtf8SpanWriterExtensions.cs
+++ b/src/ZaString/Extensions/ZaUtf8SpanWriterExtensions.cs
@@ -1,13 +1,12 @@
using System.Buffers;
-using System.Globalization;
-using System.Text;
using System.Buffers.Text;
+using System.Text;
using ZaString.Core;
namespace ZaString.Extensions;
///
-/// Provides extension methods for the .
+/// Provides extension methods for the .
///
public static class ZaUtf8SpanWriterExtensions
{
@@ -42,13 +41,19 @@ public static ref ZaUtf8SpanWriter Append(ref this ZaUtf8SpanWriter writer, stri
public static ref ZaUtf8SpanWriter Append(ref this ZaUtf8SpanWriter writer, char value)
{
- var bytes = Encoding.UTF8.GetByteCount(stackalloc char[1] { value });
+ var bytes = Encoding.UTF8.GetByteCount(stackalloc char[1]
+ {
+ value
+ });
if (bytes > writer.RemainingSpan.Length)
{
ThrowOutOfRangeException();
}
- var written = Encoding.UTF8.GetBytes(stackalloc char[1] { value }, writer.RemainingSpan);
+ var written = Encoding.UTF8.GetBytes(stackalloc char[1]
+ {
+ value
+ }, writer.RemainingSpan);
writer.Advance(written);
return ref writer;
}
@@ -112,6 +117,7 @@ public static ref ZaUtf8SpanWriter AppendLine(ref this ZaUtf8SpanWriter writer,
{
writer.Append(value);
}
+
return ref writer.AppendLine();
}
@@ -130,7 +136,7 @@ public static ref ZaUtf8SpanWriter AppendHex(ref this ZaUtf8SpanWriter writer, R
for (var i = 0; i < data.Length; i++)
{
var b = data[i];
- dest[w++] = (byte)hex[(b >> 4) & 0xF];
+ dest[w++] = (byte)hex[b >> 4 & 0xF];
dest[w++] = (byte)hex[b & 0xF];
}
@@ -160,4 +166,4 @@ private static void ThrowOutOfRangeException()
{
throw new ArgumentOutOfRangeException("value", "The destination buffer is too small.");
}
-}
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/tests/ZaString.Tests/ZaPooledStringBuilderTests.cs b/tests/ZaString.Tests/ZaPooledStringBuilderTests.cs
index 1bfbf8c..7e7d81e 100644
--- a/tests/ZaString.Tests/ZaPooledStringBuilderTests.cs
+++ b/tests/ZaString.Tests/ZaPooledStringBuilderTests.cs
@@ -7,7 +7,7 @@ public class ZaPooledStringBuilderTests
[Fact]
public void Append_GrowsAndBuilds()
{
- using var b = ZaPooledStringBuilder.Rent(initialCapacity: 4);
+ using var b = ZaPooledStringBuilder.Rent(4);
b.Append("Hello").Append(", ").Append("World!");
Assert.Equal("Hello, World!", b.AsSpan());
Assert.Equal("Hello, World!", b.ToString());
@@ -30,5 +30,4 @@ public void Clear_ResetsLength()
Assert.Equal(0, b.Length);
Assert.Equal("", b.AsSpan());
}
-}
-
+}
\ No newline at end of file
diff --git a/tests/ZaString.Tests/ZaSpanStringBuilderAppendHelpersTests.cs b/tests/ZaString.Tests/ZaSpanStringBuilderAppendHelpersTests.cs
index b2fc737..01a3cbf 100644
--- a/tests/ZaString.Tests/ZaSpanStringBuilderAppendHelpersTests.cs
+++ b/tests/ZaString.Tests/ZaSpanStringBuilderAppendHelpersTests.cs
@@ -73,10 +73,13 @@ public void AppendJoin_ISpanFormattable_Works_WithProvider()
Span buffer = stackalloc char[20];
var builder = ZaSpanStringBuilder.Create(buffer);
- var values = new[] { 1.5, 2.5 };
+ var values = new[]
+ {
+ 1.5,
+ 2.5
+ };
builder.AppendJoin("; ".AsSpan(), values, provider: fr);
Assert.Equal("1,5; 2,5", builder.AsSpan());
}
-}
-
+}
\ No newline at end of file
diff --git a/tests/ZaString.Tests/ZaSpanStringBuilderBasicTests.cs b/tests/ZaString.Tests/ZaSpanStringBuilderBasicTests.cs
index ced6d6e..25bd8ed 100644
--- a/tests/ZaString.Tests/ZaSpanStringBuilderBasicTests.cs
+++ b/tests/ZaString.Tests/ZaSpanStringBuilderBasicTests.cs
@@ -607,7 +607,7 @@ public void Clear_PreservesCapacity()
builder.Append("Test");
var originalCapacity = builder.Capacity;
-
+
builder.Clear();
Assert.Equal(originalCapacity, builder.Capacity);
diff --git a/tests/ZaString.Tests/ZaSpanStringBuilderEdgeCasesTests.cs b/tests/ZaString.Tests/ZaSpanStringBuilderEdgeCasesTests.cs
index a8db740..6168323 100644
--- a/tests/ZaString.Tests/ZaSpanStringBuilderEdgeCasesTests.cs
+++ b/tests/ZaString.Tests/ZaSpanStringBuilderEdgeCasesTests.cs
@@ -233,9 +233,9 @@ public void Clear_AfterMultipleOperations_ResetsCorrectly()
builder.Append("Hello").Append(' ').Append(42).Append(true);
var lengthBeforeClear = builder.Length;
-
+
builder.Clear();
-
+
Assert.True(lengthBeforeClear > 0);
Assert.Equal(0, builder.Length);
Assert.Equal("", builder.AsSpan());
diff --git a/tests/ZaString.Tests/ZaSpanStringBuilderEscapingTests.cs b/tests/ZaString.Tests/ZaSpanStringBuilderEscapingTests.cs
index 42c4aa0..7843240 100644
--- a/tests/ZaString.Tests/ZaSpanStringBuilderEscapingTests.cs
+++ b/tests/ZaString.Tests/ZaSpanStringBuilderEscapingTests.cs
@@ -48,5 +48,4 @@ public void TryAppendCsvEscaped_Quotes_Commas_Newlines()
Assert.True(ok);
Assert.Equal("\" a,\"\"b\"\"\n\"", builder.AsSpan());
}
-}
-
+}
\ No newline at end of file
diff --git a/tests/ZaString.Tests/ZaSpanStringBuilderFormatTests.cs b/tests/ZaString.Tests/ZaSpanStringBuilderFormatTests.cs
index a2dfa2b..480a100 100644
--- a/tests/ZaString.Tests/ZaSpanStringBuilderFormatTests.cs
+++ b/tests/ZaString.Tests/ZaSpanStringBuilderFormatTests.cs
@@ -54,4 +54,4 @@ public void AppendFormat_WithISpanFormattable_UsesSpanFormattable()
var expected = string.Format("Int: {0:N0}, Double: {1:F2}", 123, 456.789);
Assert.Equal(expected, builder.AsSpan().ToString());
}
-}
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/tests/ZaString.Tests/ZaSpanStringBuilderInterpolationTests.cs b/tests/ZaString.Tests/ZaSpanStringBuilderInterpolationTests.cs
index 3cd9339..ce52fe5 100644
--- a/tests/ZaString.Tests/ZaSpanStringBuilderInterpolationTests.cs
+++ b/tests/ZaString.Tests/ZaSpanStringBuilderInterpolationTests.cs
@@ -43,5 +43,4 @@ public void AppendLine_InterpolatedString_AppendsNewline()
Assert.Equal($"Line: 42{Environment.NewLine}", builder.AsSpan());
}
-}
-
+}
\ No newline at end of file
diff --git a/tests/ZaString.Tests/ZaSpanStringBuilderMutationHelpersTests.cs b/tests/ZaString.Tests/ZaSpanStringBuilderMutationHelpersTests.cs
index 51a5b0d..2cdd879 100644
--- a/tests/ZaString.Tests/ZaSpanStringBuilderMutationHelpersTests.cs
+++ b/tests/ZaString.Tests/ZaSpanStringBuilderMutationHelpersTests.cs
@@ -55,5 +55,4 @@ public void EnsureEndsWith_NoOpWhenAlreadyEnding()
Assert.Equal("abcx", builder.AsSpan());
}
-}
-
+}
\ No newline at end of file
diff --git a/tests/ZaString.Tests/ZaSpanStringBuilderTryAppendTests.cs b/tests/ZaString.Tests/ZaSpanStringBuilderTryAppendTests.cs
index abaaab0..26421ad 100644
--- a/tests/ZaString.Tests/ZaSpanStringBuilderTryAppendTests.cs
+++ b/tests/ZaString.Tests/ZaSpanStringBuilderTryAppendTests.cs
@@ -38,7 +38,7 @@ public void TryAppend_String_Null_ReturnsTrue_NoChange()
Span buffer = stackalloc char[3];
var builder = ZaSpanStringBuilder.Create(buffer);
- var ok = builder.TryAppend((string?)null);
+ var ok = builder.TryAppend(null);
Assert.True(ok);
Assert.Equal(0, builder.Length);
@@ -137,7 +137,7 @@ public void TryAppendLine_OnlyNewline_InsufficientCapacity_ReturnsFalse_NoChange
{
var newlineLen = Environment.NewLine.Length;
var capacity = Math.Max(0, newlineLen - 1);
- Span buffer = capacity == 0 ? Span.Empty : stackalloc char[capacity];
+ var buffer = capacity == 0 ? Span.Empty : stackalloc char[capacity];
var builder = ZaSpanStringBuilder.Create(buffer);
var ok = builder.TryAppendLine();
@@ -181,5 +181,4 @@ public void TryAppendLine_String_Succeeds_WhenCapacitySufficient()
Assert.Equal(value + Environment.NewLine, builder.AsSpan());
Assert.Equal(required, builder.Length);
}
-}
-
+}
\ No newline at end of file
diff --git a/tests/ZaString.Tests/ZaSpanStringBuilderUrlHelpersTests.cs b/tests/ZaString.Tests/ZaSpanStringBuilderUrlHelpersTests.cs
index 8eb9e03..e636cb0 100644
--- a/tests/ZaString.Tests/ZaSpanStringBuilderUrlHelpersTests.cs
+++ b/tests/ZaString.Tests/ZaSpanStringBuilderUrlHelpersTests.cs
@@ -46,10 +46,9 @@ public void AppendQueryParam_Encodes_And_Uses_Correct_Delimiters()
var builder = ZaSpanStringBuilder.Create(buffer);
builder.Append("/search")
- .AppendQueryParam("q", "a b", urlEncode: true, isFirst: true)
- .AppendQueryParam("page", "1", urlEncode: false);
+ .AppendQueryParam("q", "a b", true, true)
+ .AppendQueryParam("page", "1", false);
Assert.Equal("/search?q=a%20b&page=1", builder.AsSpan());
}
-}
-
+}
\ No newline at end of file
diff --git a/tests/ZaString.Tests/ZaUtf8SpanWriterTests.cs b/tests/ZaString.Tests/ZaUtf8SpanWriterTests.cs
index 93d889d..528e90b 100644
--- a/tests/ZaString.Tests/ZaUtf8SpanWriterTests.cs
+++ b/tests/ZaString.Tests/ZaUtf8SpanWriterTests.cs
@@ -60,8 +60,13 @@ public void AppendHex_FormatsBytesAsHex()
Span buffer = stackalloc byte[16];
var writer = ZaUtf8SpanWriter.Create(buffer);
- var data = new byte[] { 0xAB, 0xCD, 0xEF };
- writer.AppendHex(data, uppercase: true);
+ var data = new byte[]
+ {
+ 0xAB,
+ 0xCD,
+ 0xEF
+ };
+ writer.AppendHex(data, true);
var expected = Encoding.UTF8.GetBytes("ABCDEF");
Assert.True(writer.AsSpan().SequenceEqual(expected));
@@ -73,11 +78,16 @@ public void AppendBase64_EncodesBytes()
Span buffer = stackalloc byte[32];
var writer = ZaUtf8SpanWriter.Create(buffer);
- var data = new byte[] { 0x01, 0x02, 0x03 };
+ var data = new byte[]
+ {
+ 0x01,
+ 0x02,
+ 0x03
+ };
writer.AppendBase64(data);
var expected = Convert.ToBase64String(data);
var expectedBytes = Encoding.UTF8.GetBytes(expected);
Assert.True(writer.AsSpan().SequenceEqual(expectedBytes));
}
-}
\ No newline at end of file
+}
\ No newline at end of file
From 418e47ab449d8bf61fb22202e0a954d61e5983a5 Mon Sep 17 00:00:00 2001
From: Corentin Giaufer Saubert <43623834+CorentinGS@users.noreply.github.com>
Date: Thu, 7 Aug 2025 23:28:42 +0200
Subject: [PATCH 12/13] fix: Correct logical error in IsUnreservedAscii method
to properly include numeric characters in ZaSpanStringBuilderExtensions
---
src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs b/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs
index d967a72..c5b155a 100644
--- a/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs
+++ b/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs
@@ -815,7 +815,7 @@ private static bool NeedsCsvQuoting(ReadOnlySpan value)
private static bool IsUnreservedAscii(char c)
{
- return c is >= 'A' and <= 'Z' or >= 'a' and <= 'z' or < '0' or > '9' or '-' or '_' or '.' or '~';
+ return c is >= 'A' and <= 'Z' or >= 'a' and <= 'z' or >= '0' and <= '9' or '-' or '_' or '.' or '~';
}
public static ref ZaSpanStringBuilder AppendUrlEncoded(ref this ZaSpanStringBuilder builder, ReadOnlySpan value)
From 694774febba63a6a30e0f77e7bcd4640aa7992e8 Mon Sep 17 00:00:00 2001
From: Corentin Giaufer Saubert <43623834+CorentinGS@users.noreply.github.com>
Date: Thu, 7 Aug 2025 23:29:53 +0200
Subject: [PATCH 13/13] fix: Simplify code point calculation in
ZaSpanStringBuilderExtensions to improve clarity and maintainability
---
src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs b/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs
index c5b155a..4c1a724 100644
--- a/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs
+++ b/src/ZaString/Extensions/ZaSpanStringBuilderExtensions.cs
@@ -856,15 +856,14 @@ public static bool TryAppendUrlEncoded(ref this ZaSpanStringBuilder builder, Rea
}
else if (char.IsHighSurrogate(c) && i + 1 < value.Length && char.IsLowSurrogate(value[i + 1]))
{
- var high = c;
var low = value[++i];
- var codePoint = 0x10000 + (high - 0xD800 << 10 | low - 0xDC00);
- w += PercentEncodeUtf8FromCodePoint(codePoint, dest.Slice(w));
+ var codePoint = 0x10000 + (c - 0xD800 << 10 | low - 0xDC00);
+ w += PercentEncodeUtf8FromCodePoint(codePoint, dest[w..]);
}
else
{
var codePoint = (int)c;
- w += PercentEncodeUtf8FromCodePoint(codePoint, dest.Slice(w));
+ w += PercentEncodeUtf8FromCodePoint(codePoint, dest[w..]);
}
}