Skip to content

Commit 7090275

Browse files
committed
feat: Added AppendFormat
1 parent 74e3cd3 commit 7090275

File tree

4 files changed

+305
-0
lines changed

4 files changed

+305
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#if NET6_0
2+
namespace System.Diagnostics.CodeAnalysis;
3+
4+
/// <summary>Fake version of the StringSyntaxAttribute, which was introduced in .NET 7</summary>
5+
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)]
6+
[SuppressMessage("Design", "CA1019:Define accessors for attribute arguments", Justification = "The sole purpose is to have the same public surface as the class in .NET7 and above.")]
7+
public sealed class StringSyntaxAttribute : Attribute
8+
{
9+
/// <summary>The syntax identifier for strings containing composite formats.</summary>
10+
public const string CompositeFormat = nameof(CompositeFormat);
11+
12+
/// <summary>
13+
/// Initializes a new instance of the <see cref="StringSyntaxAttribute"/> class.
14+
/// </summary>
15+
public StringSyntaxAttribute(string syntax)
16+
{
17+
}
18+
19+
/// <summary>
20+
/// Initializes a new instance of the <see cref="StringSyntaxAttribute"/> class.
21+
/// </summary>
22+
public StringSyntaxAttribute(string syntax, params object?[] arguments)
23+
{
24+
}
25+
}
26+
#endif
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
3+
namespace LinkDotNet.StringBuilder;
4+
5+
public ref partial struct ValueStringBuilder
6+
{
7+
/// <summary>
8+
/// Appends the format string to the given <see cref="ValueStringBuilder"/> instance.
9+
/// </summary>
10+
/// <param name="format">Format string.</param>
11+
/// <param name="arg">Argument for <c>{0}</c>.</param>
12+
/// <typeparam name="T">Any type.</typeparam>
13+
/// <remarks>
14+
/// The current version does not allow for a custom format.
15+
/// So: <code>AppendFormat("{0:00}")</code> is not allowed and will result in an exception.
16+
/// </remarks>
17+
public void AppendFormat<T>(
18+
[StringSyntax(StringSyntaxAttribute.CompositeFormat)] ReadOnlySpan<char> format,
19+
T arg)
20+
{
21+
var formatIndex = 0;
22+
while (formatIndex < format.Length)
23+
{
24+
var c = format[formatIndex];
25+
if (c == '{')
26+
{
27+
var endIndex = format[(formatIndex + 1)..].IndexOf('}');
28+
if (endIndex == -1)
29+
{
30+
Append(format);
31+
return;
32+
}
33+
34+
var placeholder = format.Slice(formatIndex, endIndex + 2);
35+
36+
GetValidArgumentIndex(placeholder, 0);
37+
38+
AppendInternal(arg);
39+
formatIndex += endIndex + 2;
40+
}
41+
else
42+
{
43+
Append(c);
44+
formatIndex++;
45+
}
46+
}
47+
}
48+
49+
/// <summary>
50+
/// Appends the format string to the given <see cref="ValueStringBuilder"/> instance.
51+
/// </summary>
52+
/// <param name="format">Format string.</param>
53+
/// <param name="arg1">Argument for <c>{0}</c>.</param>
54+
/// <param name="arg2">Argument for <c>{1}</c>.</param>
55+
/// <typeparam name="T1">Any type for <param name="arg1"></param>.</typeparam>
56+
/// <typeparam name="T2">Any type for <param name="arg2"></param>.</typeparam>
57+
/// <remarks>
58+
/// The current version does not allow for a custom format.
59+
/// So: <code>AppendFormat("{0:00}")</code> is not allowed and will result in an exception.
60+
/// </remarks>
61+
public void AppendFormat<T1, T2>(
62+
[StringSyntax(StringSyntaxAttribute.CompositeFormat)] ReadOnlySpan<char> format,
63+
T1 arg1,
64+
T2 arg2)
65+
{
66+
var formatIndex = 0;
67+
while (formatIndex < format.Length)
68+
{
69+
var c = format[formatIndex];
70+
if (c == '{')
71+
{
72+
var endIndex = format[(formatIndex + 1)..].IndexOf('}');
73+
if (endIndex == -1)
74+
{
75+
Append(format);
76+
return;
77+
}
78+
79+
var placeholder = format.Slice(formatIndex, endIndex + 2);
80+
81+
var index = GetValidArgumentIndex(placeholder, 1);
82+
83+
switch (index)
84+
{
85+
case 0:
86+
AppendInternal(arg1);
87+
break;
88+
case 1:
89+
AppendInternal(arg2);
90+
break;
91+
}
92+
93+
formatIndex += endIndex + 2;
94+
}
95+
else
96+
{
97+
Append(c);
98+
formatIndex++;
99+
}
100+
}
101+
}
102+
103+
/// <summary>
104+
/// Appends the format string to the given <see cref="ValueStringBuilder"/> instance.
105+
/// </summary>
106+
/// <param name="format">Format string.</param>
107+
/// <param name="arg1">Argument for <c>{0}</c>.</param>
108+
/// <param name="arg2">Argument for <c>{1}</c>.</param>
109+
/// <param name="arg3">Argument for <c>{2}</c>.</param>
110+
/// <typeparam name="T1">Any type for <param name="arg1"></param>.</typeparam>
111+
/// <typeparam name="T2">Any type for <param name="arg2"></param>.</typeparam>
112+
/// <typeparam name="T3">Any type for <param name="arg3"></param>.</typeparam>
113+
/// <remarks>
114+
/// The current version does not allow for a custom format.
115+
/// So: <code>AppendFormat("{0:00}")</code> is not allowed and will result in an exception.
116+
/// </remarks>
117+
public void AppendFormat<T1, T2, T3>(
118+
[StringSyntax(StringSyntaxAttribute.CompositeFormat)] ReadOnlySpan<char> format,
119+
T1 arg1,
120+
T2 arg2,
121+
T3 arg3)
122+
{
123+
var formatIndex = 0;
124+
while (formatIndex < format.Length)
125+
{
126+
var c = format[formatIndex];
127+
if (c == '{')
128+
{
129+
var endIndex = format[(formatIndex + 1)..].IndexOf('}');
130+
if (endIndex == -1)
131+
{
132+
Append(format);
133+
return;
134+
}
135+
136+
var placeholder = format.Slice(formatIndex, endIndex + 2);
137+
138+
var index = GetValidArgumentIndex(placeholder, 2);
139+
140+
switch (index)
141+
{
142+
case 0:
143+
AppendInternal(arg1);
144+
break;
145+
case 1:
146+
AppendInternal(arg2);
147+
break;
148+
case 2:
149+
AppendInternal(arg3);
150+
break;
151+
}
152+
153+
formatIndex += endIndex + 2;
154+
}
155+
else
156+
{
157+
Append(c);
158+
formatIndex++;
159+
}
160+
}
161+
}
162+
163+
private static int GetValidArgumentIndex(ReadOnlySpan<char> placeholder, int allowedRange)
164+
{
165+
#pragma warning disable MA0011
166+
if (!int.TryParse(placeholder[1..^1], out var argIndex))
167+
#pragma warning restore MA0011
168+
{
169+
throw new FormatException("Invalid argument index in format string: " + placeholder.ToString());
170+
}
171+
172+
if (argIndex < 0 || argIndex > allowedRange)
173+
{
174+
throw new FormatException("Invalid argument index in format string: " + placeholder.ToString());
175+
}
176+
177+
return argIndex;
178+
}
179+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using BenchmarkDotNet.Attributes;
2+
3+
namespace LinkDotNet.StringBuilder.Benchmarks;
4+
5+
[MemoryDiagnoser]
6+
public class AppendFormatBenchmark
7+
{
8+
[Benchmark(Baseline = true)]
9+
public string DotNetStringBuilderAppendFormat()
10+
{
11+
var builder = new System.Text.StringBuilder();
12+
for (var i = 0; i < 10; i++)
13+
{
14+
builder.AppendFormat("Hello {0} dear {1}. {2}", 2, "world", 30);
15+
}
16+
17+
return builder.ToString();
18+
}
19+
20+
[Benchmark]
21+
public string ValueStringBuilderAppendFormat()
22+
{
23+
using var builder = new ValueStringBuilder();
24+
for (var i = 0; i < 10; i++)
25+
{
26+
builder.AppendFormat("Hello {0} dear {1}. {2}", 2, "world", 30);
27+
}
28+
29+
return builder.ToString();
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
using System;
2+
3+
namespace LinkDotNet.StringBuilder.UnitTests;
4+
5+
public class ValueStringBuilderAppendFormatTests
6+
{
7+
[Theory]
8+
[InlineData("Hello {0}", 2, "Hello 2")]
9+
[InlineData("{0}{0}", 2, "22")]
10+
[InlineData("{0} World", "Hello", "Hello World")]
11+
[InlineData("Hello World", "2", "Hello World")]
12+
public void ShouldAppendFormatWithOneArgument(string format, object arg, string expected)
13+
{
14+
using var builder = new ValueStringBuilder();
15+
16+
builder.AppendFormat(format, arg);
17+
18+
builder.ToString().Should().Be(expected);
19+
}
20+
21+
[Theory]
22+
[InlineData("{0:00}")]
23+
[InlineData("{1000}")]
24+
[InlineData("{Text}")]
25+
public void ShouldThrowWhenFormatWrongOneArgument(string format)
26+
{
27+
using var builder = new ValueStringBuilder();
28+
29+
try
30+
{
31+
builder.AppendFormat(format, 1);
32+
}
33+
catch (FormatException)
34+
{
35+
Assert.True(true);
36+
return;
37+
}
38+
39+
Assert.False(true);
40+
}
41+
42+
[Theory]
43+
[InlineData("Hello {0} {1}", 2, 3, "Hello 2 3")]
44+
[InlineData("{0}{0}{1}", 2, 3, "223")]
45+
[InlineData("{0} World", "Hello", "", "Hello World")]
46+
[InlineData("Hello World", "2", "", "Hello World")]
47+
public void ShouldAppendFormatWithTwoArguments(string format, object arg1, object arg2, string expected)
48+
{
49+
using var builder = new ValueStringBuilder();
50+
51+
builder.AppendFormat(format, arg1, arg2);
52+
53+
builder.ToString().Should().Be(expected);
54+
}
55+
56+
[Theory]
57+
[InlineData("Hello {0} {1} {2}", 2, 3, 4, "Hello 2 3 4")]
58+
[InlineData("{0}{0}{1}{2}", 2, 3, 3, "2233")]
59+
[InlineData("{0} World", "Hello", "", "", "Hello World")]
60+
[InlineData("Hello World", "2", "", "", "Hello World")]
61+
public void ShouldAppendFormatWithThreeArguments(string format, object arg1, object arg2, object arg3, string expected)
62+
{
63+
using var builder = new ValueStringBuilder();
64+
65+
builder.AppendFormat(format, arg1, arg2, arg3);
66+
67+
builder.ToString().Should().Be(expected);
68+
}
69+
}

0 commit comments

Comments
 (0)