Skip to content

Commit ebc1973

Browse files
committed
feat: add AppendInterpolatedStringHandler
1 parent 4831a1e commit ebc1973

File tree

3 files changed

+212
-0
lines changed

3 files changed

+212
-0
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ All notable changes to **ValueStringBuilder** will be documented in this file. T
66

77
## [Unreleased]
88

9+
### Added
10+
11+
- Added `AppendInterpolatedStringHandler` to support zero-allocation string interpolation in `Append` and `AppendLine` methods.
12+
913
## [3.2.0] - 2025-10-31
1014

1115
### Changed
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
using System.Runtime.CompilerServices;
2+
3+
namespace LinkDotNet.StringBuilder;
4+
5+
public ref partial struct ValueStringBuilder
6+
{
7+
/// <summary>
8+
/// Appends an interpolated string to the builder.
9+
/// </summary>
10+
/// <param name="handler">The interpolated string handler.</param>
11+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
12+
public void Append([InterpolatedStringHandlerArgument("")] ref AppendInterpolatedStringHandler handler)
13+
{
14+
this = handler.Builder;
15+
}
16+
17+
/// <summary>
18+
/// Appends an interpolated string followed by a new line to the builder.
19+
/// </summary>
20+
/// <param name="handler">The interpolated string handler.</param>
21+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
22+
public void AppendLine([InterpolatedStringHandlerArgument("")] ref AppendInterpolatedStringHandler handler)
23+
{
24+
this = handler.Builder;
25+
Append(Environment.NewLine);
26+
}
27+
28+
/// <summary>
29+
/// Nested struct which handles interpolated strings for <see cref="ValueStringBuilder"/>.
30+
/// </summary>
31+
[InterpolatedStringHandler]
32+
public ref struct AppendInterpolatedStringHandler
33+
{
34+
internal ValueStringBuilder Builder;
35+
36+
/// <summary>
37+
/// Initializes a new instance of the <see cref="AppendInterpolatedStringHandler"/> struct.
38+
/// </summary>
39+
/// <param name="literalLength">The length of the literal part of the interpolated string.</param>
40+
/// <param name="formattedCount">The number of formatted segments in the interpolated string.</param>
41+
/// <param name="builder">The builder to append to.</param>
42+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
43+
public AppendInterpolatedStringHandler(int literalLength, int formattedCount, ValueStringBuilder builder)
44+
{
45+
this.Builder = builder;
46+
47+
// A conservative guess for the capacity.
48+
this.Builder.EnsureCapacity(this.Builder.Length + literalLength + (formattedCount * 11));
49+
}
50+
51+
/// <summary>
52+
/// Appends a literal string to the handler.
53+
/// </summary>
54+
/// <param name="value">The literal string.</param>
55+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
56+
public void AppendLiteral(string value) => Builder.Append(value);
57+
58+
/// <summary>
59+
/// Appends a formatted value to the handler.
60+
/// </summary>
61+
/// <param name="value">The value to format.</param>
62+
/// <typeparam name="T">The type of the value.</typeparam>
63+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
64+
public void AppendFormatted<T>(T value)
65+
{
66+
if (value is ISpanFormattable formattable)
67+
{
68+
Builder.Append(formattable);
69+
}
70+
else
71+
{
72+
Builder.Append(value?.ToString());
73+
}
74+
}
75+
76+
/// <summary>
77+
/// Appends a formatted value with a given format.
78+
/// </summary>
79+
/// <param name="value">The value to format.</param>
80+
/// <param name="format">The format string.</param>
81+
/// <typeparam name="T">The type of the value.</typeparam>
82+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
83+
public void AppendFormatted<T>(T value, string? format)
84+
{
85+
if (value is ISpanFormattable formattable)
86+
{
87+
Builder.Append(formattable, format);
88+
}
89+
else
90+
{
91+
Builder.Append(value?.ToString());
92+
}
93+
}
94+
95+
/// <summary>
96+
/// Appends a character span to the handler.
97+
/// </summary>
98+
/// <param name="value">The character span.</param>
99+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
100+
public void AppendFormatted(ReadOnlySpan<char> value) => Builder.Append(value);
101+
102+
/// <summary>
103+
/// Appends a string to the handler.
104+
/// </summary>
105+
/// <param name="value">The string value.</param>
106+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
107+
public void AppendFormatted(string? value) => Builder.Append(value);
108+
}
109+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
namespace LinkDotNet.StringBuilder.UnitTests;
2+
3+
public class ValueStringBuilderInterpolatedStringTests
4+
{
5+
[Fact]
6+
public void ShouldAppendInterpolatedString()
7+
{
8+
using var builder = new ValueStringBuilder();
9+
var name = "World";
10+
var version = 1.0;
11+
12+
builder.Append($"Hello {name}, version {version}");
13+
14+
builder.ToString().ShouldBe("Hello World, version 1");
15+
}
16+
17+
[Fact]
18+
public void ShouldAppendInterpolatedStringWithFormat()
19+
{
20+
using var builder = new ValueStringBuilder();
21+
var price = 1.2345;
22+
23+
builder.Append($"Price: {price:N2}");
24+
25+
builder.ToString().ShouldBe("Price: 1.23");
26+
}
27+
28+
[Fact]
29+
public void ShouldAppendLineInterpolatedString()
30+
{
31+
using var builder = new ValueStringBuilder();
32+
var name = "World";
33+
34+
builder.AppendLine($"Hello {name}");
35+
36+
builder.ToString().ShouldBe($"Hello World{Environment.NewLine}");
37+
}
38+
39+
[Fact]
40+
public void ShouldHandleSpanInInterpolatedString()
41+
{
42+
using var builder = new ValueStringBuilder();
43+
ReadOnlySpan<char> span = "from span";
44+
45+
builder.Append($"Value {span}");
46+
47+
builder.ToString().ShouldBe("Value from span");
48+
}
49+
50+
[Fact]
51+
public void ShouldHandleCustomType()
52+
{
53+
using var builder = new ValueStringBuilder();
54+
var custom = new CustomType { Value = "Test" };
55+
56+
builder.Append($"Custom: {custom}");
57+
58+
builder.ToString().ShouldBe("Custom: Test");
59+
}
60+
61+
[Fact]
62+
public void ShouldAppendToExistingContent()
63+
{
64+
using var builder = new ValueStringBuilder("Initial ");
65+
66+
builder.Append($"Appended {123}");
67+
68+
builder.ToString().ShouldBe("Initial Appended 123");
69+
}
70+
71+
[Fact]
72+
public void ShouldAppendMultipleInterpolatedStrings()
73+
{
74+
using var builder = new ValueStringBuilder();
75+
76+
builder.Append($"First {1} ");
77+
builder.Append($"Second {2}");
78+
79+
builder.ToString().ShouldBe("First 1 Second 2");
80+
}
81+
82+
[Fact]
83+
public void ShouldClearAndThenAppendInterpolatedString()
84+
{
85+
using var builder = new ValueStringBuilder("Initial");
86+
builder.Clear();
87+
88+
builder.Append($"New {1}");
89+
90+
builder.ToString().ShouldBe("New 1");
91+
}
92+
93+
private class CustomType
94+
{
95+
public string Value { get; set; } = string.Empty;
96+
97+
public override string ToString() => Value;
98+
}
99+
}

0 commit comments

Comments
 (0)