Skip to content

Commit

Permalink
Tweak ValueStringBuilder to support interpolated strings (#12876)
Browse files Browse the repository at this point in the history
Tweak ValueStringBuilder to allow it to be used as an interpolated string handler

One small usage in TypeExtensions. I'm going to be doing a larger change based on this as a follow-up.
  • Loading branch information
JeremyKuhne authored Feb 5, 2025
1 parent ec6a9f8 commit e0da5a8
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 28 deletions.
22 changes: 11 additions & 11 deletions src/System.Drawing.Common/src/System/Drawing/FontConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,50 +33,50 @@ public override bool CanConvertTo(ITypeDescriptorContext? context, [NotNullWhen(
culture ??= CultureInfo.CurrentCulture;

ValueStringBuilder sb = default;
sb.Append(font.Name);
sb.AppendLiteral(font.Name);
sb.Append(culture.TextInfo.ListSeparator[0]);
sb.Append(' ');
sb.Append(font.Size.ToString(culture.NumberFormat));
sb.AppendLiteral(font.Size.ToString(culture.NumberFormat));

switch (font.Unit)
{
// MS throws ArgumentException, if unit is set
// to GraphicsUnit.Display
// Don't know what to append for GraphicsUnit.Display
case GraphicsUnit.Display:
sb.Append("display");
sb.AppendLiteral("display");
break;

case GraphicsUnit.Document:
sb.Append("doc");
sb.AppendLiteral("doc");
break;

case GraphicsUnit.Point:
sb.Append("pt");
sb.AppendLiteral("pt");
break;

case GraphicsUnit.Inch:
sb.Append("in");
sb.AppendLiteral("in");
break;

case GraphicsUnit.Millimeter:
sb.Append("mm");
sb.AppendLiteral("mm");
break;

case GraphicsUnit.Pixel:
sb.Append("px");
sb.AppendLiteral("px");
break;

case GraphicsUnit.World:
sb.Append("world");
sb.AppendLiteral("world");
break;
}

if (font.Style != FontStyle.Regular)
{
sb.Append(culture.TextInfo.ListSeparator[0]);
sb.Append(" style=");
sb.Append(font.Style.ToString());
sb.AppendLiteral(" style=");
sb.AppendLiteral(font.Style.ToString());
}

return sb.ToString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,25 @@ namespace System.Text;
/// <summary>
/// String builder struct that allows using stack space for small strings.
/// </summary>
[InterpolatedStringHandler]
internal ref partial struct ValueStringBuilder
{
private const int GuessedLengthPerHole = 11;
private const int MinimumArrayPoolLength = 256;

private char[]? _arrayToReturnToPool;

private Span<char> _chars;
private int _pos;

public ValueStringBuilder(int literalLength, int formattedCount)
{
_arrayToReturnToPool = null;
_chars = ArrayPool<char>.Shared.Rent(
Math.Min(MinimumArrayPoolLength, literalLength + (GuessedLengthPerHole * formattedCount)));
_pos = 0;
}

public ValueStringBuilder(Span<char> initialBuffer)
{
_arrayToReturnToPool = null;
Expand Down Expand Up @@ -90,9 +103,11 @@ public ref char this[int index]
}
}

public override string ToString()
public override readonly string ToString() => _chars[.._pos].ToString();

public string ToStringAndClear()
{
string s = _chars[.._pos].ToString();
string s = ToString();
Dispose();
return s;
}
Expand Down Expand Up @@ -185,8 +200,14 @@ public void Append(char c)
}
}

/// <summary>
/// Append a string to the builder. If the string is <see langword="null"/>, this method does nothing.
/// </summary>
/// <devdoc>
/// Name must be AppendLiteral to work with interpolated strings.
/// </devdoc>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Append(string? s)
public void AppendLiteral(string? s)
{
if (s is null)
{
Expand Down Expand Up @@ -218,6 +239,24 @@ private void AppendSlow(string s)
_pos += s.Length;
}

public void AppendFormatted<TFormattable>(TFormattable value) where TFormattable : ISpanFormattable
{
int charsWritten;

// This must be cast inline to avoid boxing.
while (!((ISpanFormattable)value).TryFormat(_chars[_pos..], out charsWritten, format: default, provider: default))
{
Grow(1);
}

_pos += charsWritten;
return;
}

public void AppendFormatted(string? value) => Append(value.AsSpan());

public void AppendFormatted(object? value) => AppendLiteral(value?.ToString());

public void Append(char c, int count)
{
if (_pos > _chars.Length - count)
Expand Down Expand Up @@ -263,19 +302,6 @@ public void Append(ReadOnlySpan<char> value)
_pos += value.Length;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Span<char> AppendSpan(int length)
{
int origPos = _pos;
if (origPos > _chars.Length - length)
{
Grow(length);
}

_pos = origPos + length;
return _chars.Slice(origPos, length);
}

[MethodImpl(MethodImplOptions.NoInlining)]
private void GrowAndAppend(char c)
{
Expand Down
11 changes: 10 additions & 1 deletion src/System.Private.Windows.Core/src/System/TypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Collections.Immutable;
using System.Reflection.Metadata;
using System.Runtime.CompilerServices;
using System.Text;

namespace System;

Expand Down Expand Up @@ -250,7 +251,7 @@ public static TypeName ToTypeName(this Type type)
assemblyName = type.Assembly.FullName;
}

return TypeName.Parse($"{GetTypeFullName(type)}, {assemblyName}");
return ToTypeName($"{GetTypeFullName(type)}, {assemblyName}");

static string GetTypeFullName(Type type)
{
Expand Down Expand Up @@ -285,4 +286,12 @@ public static Type UnwrapIfNullable(this Type type) =>
type.IsGenericType && !type.IsGenericTypeDefinition && type.GetGenericTypeDefinition() == typeof(Nullable<>)
? type.GetGenericArguments()[0]
: type;

public static TypeName ToTypeName(ref ValueStringBuilder builder)
{
using (builder)
{
return TypeName.Parse(builder.AsSpan());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace System.Text;

public class ValueStringBuilderTests
{
[Fact]
public void Append_ShouldAppendSingleString()
{
using ValueStringBuilder builder = new(10);
builder.Append("Hello");
builder.ToString().Should().Be("Hello");
}

[Fact]
public void Append_ShouldAppendMultipleStrings()
{
using ValueStringBuilder builder = new(10);
builder.Append("Hello");
builder.Append(", ");
builder.Append("world!");
builder.ToString().Should().Be("Hello, world!");
}

[Fact]
public void Append_Char_ShouldAppendCharacters()
{
using ValueStringBuilder builder = new(10);
builder.Append('A');
builder.Append('B');
builder.Append('C');
builder.ToString().Should().Be("ABC");
}

[Fact]
public void Insert_ShouldInsertStringAtIndex()
{
using ValueStringBuilder builder = new(10);
builder.Append("Hello world!");
builder.Insert(6, "beautiful ");
builder.ToString().Should().Be("Hello beautiful world!");
}

[Fact]
public void Length_ShouldReturnCorrectValue()
{
using ValueStringBuilder builder = new(10);
builder.Append("12345");
builder.Length.Should().Be(5);
}

[Fact]
public void Capacity_ShouldIncreaseWhenExceeded()
{
using ValueStringBuilder builder = new(10);
builder.Append("This is a long string that exceeds the initial capacity.");
builder.Capacity.Should().BeGreaterThan(10);
builder.ToString().Should().Be("This is a long string that exceeds the initial capacity.");
}

[Theory]
[InlineData(10)]
[InlineData(100)]
public void AsHandler_Int(int value)
{
string result = TestFormat($"Hello, {value}!");
result.Should().Be($"Hello, {value}!");
}

[Theory]
[InlineData(DayOfWeek.Monday)]
[InlineData(DayOfWeek.Friday)]
public void AsHandler_Enum(DayOfWeek value)
{
string result = TestFormat($"Hello, it's {value}!");
result.Should().Be($"Hello, it's {value}!");
}

private static string TestFormat(ref ValueStringBuilder builder) => builder.ToStringAndClear();
}

0 comments on commit e0da5a8

Please sign in to comment.