Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 224 additions & 0 deletions samples/ZaString.Demo/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics;
using System.Globalization;
using System.Text;
using ZaString.Core;
using ZaString.Extensions;
Expand Down Expand Up @@ -37,9 +38,167 @@ public static void Main()
CharacterModificationDemo();
Console.WriteLine();

TryAppendDemo();
Console.WriteLine();

AppendHelpersDemo();
Console.WriteLine();

InterpolationDemo();
Console.WriteLine();

JsonEscapingDemo();
Console.WriteLine();

UrlHelpersDemo();
Console.WriteLine();

PooledBuilderDemo();
Console.WriteLine();

Utf8WriterDemo();
Console.WriteLine();

FormatDemo();
Console.WriteLine();

Console.WriteLine("Demo complete!");
}

private static void TryAppendDemo()
{
Console.WriteLine("--- TryAppend ---");

// Small buffer: demonstrate non-throwing false return
Span<char> 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<char> 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<T>
Span<char> 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<char> 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[]
{
1.5,
2.5,
3.5
};
builder.AppendJoin<double>("; ".AsSpan(), values, provider: new CultureInfo("fr-FR"));

Console.WriteLine(builder.AsSpan().ToString());
}

private static void InterpolationDemo()
{
Console.WriteLine("--- Interpolated String Handler ---");

Span<char> 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 JsonEscapingDemo()
{
Console.WriteLine("--- JSON Escaping ---");

// Build a small JSON object with escaped string values
Span<char> 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<char> 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<char> 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 UrlHelpersDemo()
{
Console.WriteLine("--- URL Helpers ---");

Span<char> 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 PooledBuilderDemo()
{
Console.WriteLine("--- Pooled Builder ---");

using var b = ZaPooledStringBuilder.Rent(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 ---");
Expand Down Expand Up @@ -296,4 +455,69 @@ 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<byte> 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, true);
writer.Append(", Base64: ").AppendBase64(data);
Console.WriteLine(Encoding.UTF8.GetString(writer.AsSpan()));
}

private static void FormatDemo()
{
Console.WriteLine("--- Composite Formatting ---");

Span<char> 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());
}
}
138 changes: 138 additions & 0 deletions src/ZaString/Core/ZaPooledStringBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
using System.Buffers;
using System.Globalization;

namespace ZaString.Core;

/// <summary>
/// A growable, pooled string builder that minimizes allocations by renting buffers from ArrayPool.
/// </summary>
public sealed class ZaPooledStringBuilder : IDisposable
{
private readonly ArrayPool<char> _pool;
private char[] _buffer;
private bool _disposed;

private ZaPooledStringBuilder(ArrayPool<char> pool, int initialCapacity)
{
_pool = pool;
_buffer = pool.Rent(Math.Max(1, initialCapacity));
Length = 0;
}

public int Length { get; private set; }

public int Capacity
{
get => _buffer.Length;
}

public void Dispose()
{
if (_disposed) return;
_disposed = true;
var buf = _buffer;
_buffer = [];
_pool.Return(buf);
}

public static ZaPooledStringBuilder Rent(int initialCapacity = 256, ArrayPool<char>? pool = null)
{
return new ZaPooledStringBuilder(pool ?? ArrayPool<char>.Shared, initialCapacity);
}

public ReadOnlySpan<char> 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)
{
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 *= 2;
}

var newBuffer = _pool.Rent(newCapacity);
_buffer.AsSpan(0, Length).CopyTo(newBuffer);
_pool.Return(_buffer);
_buffer = newBuffer;
}

public ZaPooledStringBuilder Append(ReadOnlySpan<char> 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>(T value, ReadOnlySpan<char> 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();
}
}
Loading