feat: Adds cliloc support and fixes valuestringbuilder ctor (#1013)
- [X] Adds cliloc support using the following API:
```cs
public static class Localization
{
string GetText(int number);
string GetText(int number, string lang);
string Format(int number, params object[] args);
string Format(int number, string lang, params object[] args);
string Format(int number, $"{arg1}{arg2}{arg3}");
string Format(int number, lang, $"{arg1}{arg2}{arg3}");
bool TryGetLocalization(int number, out LocalizationEntry entry);
bool TryGetLocalization(int number, string lang, out LocalizationEntry entry);
}
public class LocalizationEntry
{
string Language { get; }
string Number { get; }
string Text { get; }
string?[] TextSlices { get; } // Used for string building
string StringFormatter { get; }
string Format(params object[] args);
string Format($"{arg1}{arg2}{arg3}");
}
```
- [X] Fixes a bug with ValueStringBuilder and default initialization size
- [X] Optimizes ValueStringBuilder to use `ISpanFormattable`
- [X] Adds `Append<T>(T value);` support ValueStringBuilder
- [X] Adds `Append($"");` string interpolation support to ValueStringBuider
This commit is contained in:
parent
790cb5d733
commit
be7da3a3dc
8 changed files with 1046 additions and 156 deletions
|
|
@ -1,8 +1,8 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
|
|
@ -14,6 +14,10 @@ public ref struct ValueStringBuilder
|
|||
private Span<char> _chars;
|
||||
private int _length;
|
||||
|
||||
public ValueStringBuilder() : this(64)
|
||||
{
|
||||
}
|
||||
|
||||
// If this ctor is used, you cannot pass in stackalloc ROS for append/replace.
|
||||
public ValueStringBuilder(ReadOnlySpan<char> initialString) : this(initialString.Length)
|
||||
{
|
||||
|
|
@ -140,7 +144,7 @@ public ref struct ValueStringBuilder
|
|||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Insert(int index, string s)
|
||||
public void Insert(int index, string? s)
|
||||
{
|
||||
if (s == null)
|
||||
{
|
||||
|
|
@ -160,67 +164,39 @@ public ref struct ValueStringBuilder
|
|||
_length += count;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Append(char c)
|
||||
public void Append<T>(T value, string? format = null)
|
||||
{
|
||||
int pos = _length;
|
||||
if ((uint)pos < (uint)_chars.Length)
|
||||
if (value is IFormattable)
|
||||
{
|
||||
_chars[pos] = c;
|
||||
_length = pos + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
GrowAndAppend(c);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Append(int value, NumberFormatInfo info = null)
|
||||
{
|
||||
if (value >= 0)
|
||||
{
|
||||
Append((uint)value);
|
||||
return;
|
||||
}
|
||||
|
||||
Append((info ?? NumberFormatInfo.CurrentInfo).NegativeSign);
|
||||
Append((uint)-value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public unsafe void Append(uint value)
|
||||
{
|
||||
int bufferLength = value.CountDigits();
|
||||
|
||||
int pos = _length;
|
||||
if ((uint)pos + (uint)bufferLength >= _chars.Length)
|
||||
{
|
||||
Grow(bufferLength);
|
||||
}
|
||||
|
||||
if (bufferLength == 1)
|
||||
{
|
||||
_chars[pos] = (char)(value + '0');
|
||||
_length = pos + 1;
|
||||
return;
|
||||
}
|
||||
|
||||
fixed (char* buffer = _chars[pos..])
|
||||
{
|
||||
char* p = buffer + bufferLength;
|
||||
do
|
||||
if (value is ISpanFormattable)
|
||||
{
|
||||
value = Utility.DivRem(value, 10, out uint remainder);
|
||||
*--p = (char)(remainder + '0');
|
||||
} while (value != 0);
|
||||
}
|
||||
Span<char> destination = _chars[_length..];
|
||||
int charsWritten;
|
||||
while (!((ISpanFormattable)value).TryFormat(destination, out charsWritten, format, default))
|
||||
{
|
||||
Grow(1);
|
||||
}
|
||||
|
||||
_length = pos + bufferLength;
|
||||
if ((uint)charsWritten > (uint)destination.Length)
|
||||
{
|
||||
throw new FormatException("Invalid string");
|
||||
}
|
||||
|
||||
_length += charsWritten;
|
||||
}
|
||||
else
|
||||
{
|
||||
Append(((IFormattable)value).ToString(format, default)); // constrained call avoiding boxing for value types
|
||||
}
|
||||
}
|
||||
else if (value is not null)
|
||||
{
|
||||
Append(value.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Append(string s)
|
||||
public void Append(string? s)
|
||||
{
|
||||
if (s == null)
|
||||
{
|
||||
|
|
@ -240,7 +216,7 @@ public ref struct ValueStringBuilder
|
|||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void AppendLine(string s)
|
||||
public void AppendLine(string? s)
|
||||
{
|
||||
if (s == null)
|
||||
{
|
||||
|
|
@ -261,7 +237,7 @@ public ref struct ValueStringBuilder
|
|||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void AppendSlow(string s)
|
||||
private void AppendSlow(string? s)
|
||||
{
|
||||
int pos = _length;
|
||||
if (pos > _chars.Length - s.Length)
|
||||
|
|
@ -332,14 +308,6 @@ public ref struct ValueStringBuilder
|
|||
return _chars.Slice(origPos, length);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void GrowAndAppend(char c)
|
||||
{
|
||||
Grow(1);
|
||||
Append(c);
|
||||
}
|
||||
|
||||
#nullable enable
|
||||
/// <summary>
|
||||
/// Resize the internal buffer either by doubling current buffer size or
|
||||
/// by adding <paramref name="additionalCapacityBeyondPos"/> to
|
||||
|
|
@ -469,4 +437,165 @@ public ref struct ValueStringBuilder
|
|||
|
||||
_length -= length;
|
||||
}
|
||||
|
||||
/// <summary>Provides a handler used by the language compiler to append interpolated strings into <see cref="ValueStringBuilder"/> instances.</summary>
|
||||
[InterpolatedStringHandler]
|
||||
public ref struct AppendInterpolatedStringHandler
|
||||
{
|
||||
// Implementation note:
|
||||
// As this type is only intended to be targeted by the compiler, public APIs eschew argument validation logic
|
||||
// in a variety of places, e.g. allowing a null input when one isn't expected to produce a NullReferenceException rather
|
||||
// than an ArgumentNullException.
|
||||
|
||||
/// <summary>The associated StringBuilder to which to append.</summary>
|
||||
internal ValueStringBuilder _stringBuilder;
|
||||
|
||||
/// <summary>Creates a handler used to append an interpolated string into a <see cref="ValueStringBuilder"/>.</summary>
|
||||
/// <param name="literalLength">The number of constant characters outside of interpolation expressions in the interpolated string.</param>
|
||||
/// <param name="formattedCount">The number of interpolation expressions in the interpolated string.</param>
|
||||
/// <param name="stringBuilder">The associated StringBuilder to which to append.</param>
|
||||
/// <remarks>This is intended to be called only by compiler-generated code. Arguments are not validated as they'd otherwise be for members intended to be used directly.</remarks>
|
||||
public AppendInterpolatedStringHandler(int literalLength, int formattedCount, ValueStringBuilder stringBuilder)
|
||||
{
|
||||
_stringBuilder = stringBuilder;
|
||||
}
|
||||
|
||||
/// <summary>Writes the specified string to the handler.</summary>
|
||||
/// <param name="value">The string to write.</param>
|
||||
public void AppendLiteral(string value) => _stringBuilder.Append(value);
|
||||
|
||||
// Design note:
|
||||
// This provides the same set of overloads and semantics as DefaultInterpolatedStringHandler.
|
||||
|
||||
/// <summary>Writes the specified value to the handler.</summary>
|
||||
/// <param name="value">The value to write.</param>
|
||||
public void AppendFormatted<T>(T value) => _stringBuilder.Append(value);
|
||||
|
||||
/// <summary>Writes the specified value to the handler.</summary>
|
||||
/// <param name="value">The value to write.</param>
|
||||
/// <param name="format">The format string.</param>
|
||||
public void AppendFormatted<T>(T value, string? format) => _stringBuilder.Append(value, format);
|
||||
|
||||
/// <summary>Writes the specified value to the handler.</summary>
|
||||
/// <param name="value">The value to write.</param>
|
||||
/// <param name="alignment">Minimum number of characters that should be written for this value. If the value is negative, it indicates left-aligned and the required minimum is the absolute value.</param>
|
||||
public void AppendFormatted<T>(T value, int alignment) =>
|
||||
AppendFormatted(value, alignment, format: null);
|
||||
|
||||
/// <summary>Writes the specified value to the handler.</summary>
|
||||
/// <param name="value">The value to write.</param>
|
||||
/// <param name="format">The format string.</param>
|
||||
/// <param name="alignment">Minimum number of characters that should be written for this value. If the value is negative, it indicates left-aligned and the required minimum is the absolute value.</param>
|
||||
public void AppendFormatted<T>(T value, int alignment, string? format)
|
||||
{
|
||||
if (alignment == 0)
|
||||
{
|
||||
// This overload is used as a fallback from several disambiguation overloads, so special-case 0.
|
||||
AppendFormatted(value, format);
|
||||
}
|
||||
else if (alignment < 0)
|
||||
{
|
||||
// Left aligned: format into the handler, then append any additional padding required.
|
||||
int start = _stringBuilder.Length;
|
||||
AppendFormatted(value, format);
|
||||
int paddingRequired = -alignment - (_stringBuilder.Length - start);
|
||||
if (paddingRequired > 0)
|
||||
{
|
||||
_stringBuilder.Append(' ', paddingRequired);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var startingPos = _stringBuilder._length;
|
||||
AppendFormatted(value, format);
|
||||
|
||||
InsertAlignment(startingPos, alignment);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Writes the specified character span to the handler.</summary>
|
||||
/// <param name="value">The span to write.</param>
|
||||
public void AppendFormatted(ReadOnlySpan<char> value) => _stringBuilder.Append(value);
|
||||
|
||||
/// <summary>Writes the specified string of chars to the handler.</summary>
|
||||
/// <param name="value">The span to write.</param>
|
||||
/// <param name="alignment">Minimum number of characters that should be written for this value. If the value is negative, it indicates left-aligned and the required minimum is the absolute value.</param>
|
||||
/// <param name="format">The format string.</param>
|
||||
public void AppendFormatted(ReadOnlySpan<char> value, int alignment = 0, string? format = null)
|
||||
{
|
||||
if (alignment == 0)
|
||||
{
|
||||
_stringBuilder.Append(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
bool leftAlign = false;
|
||||
if (alignment < 0)
|
||||
{
|
||||
leftAlign = true;
|
||||
alignment = -alignment;
|
||||
}
|
||||
|
||||
int paddingRequired = alignment - value.Length;
|
||||
if (paddingRequired <= 0)
|
||||
{
|
||||
_stringBuilder.Append(value);
|
||||
}
|
||||
else if (leftAlign)
|
||||
{
|
||||
_stringBuilder.Append(value);
|
||||
_stringBuilder.Append(' ', paddingRequired);
|
||||
}
|
||||
else
|
||||
{
|
||||
_stringBuilder.Append(' ', paddingRequired);
|
||||
_stringBuilder.Append(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Writes the specified value to the handler.</summary>
|
||||
/// <param name="value">The value to write.</param>
|
||||
public void AppendFormatted(string? value) => _stringBuilder.Append(value);
|
||||
|
||||
/// <summary>Writes the specified value to the handler.</summary>
|
||||
/// <param name="value">The value to write.</param>
|
||||
/// <param name="alignment">Minimum number of characters that should be written for this value. If the value is negative, it indicates left-aligned and the required minimum is the absolute value.</param>
|
||||
/// <param name="format">The format string.</param>
|
||||
public void AppendFormatted(string? value, int alignment = 0, string? format = null) =>
|
||||
// Format is meaningless for strings and doesn't make sense for someone to specify. We have the overload
|
||||
// simply to disambiguate between ROS<char> and object, just in case someone does specify a format, as
|
||||
// string is implicitly convertible to both. Just delegate to the T-based implementation.
|
||||
AppendFormatted<string?>(value, alignment, format);
|
||||
|
||||
/// <summary>Writes the specified value to the handler.</summary>
|
||||
/// <param name="value">The value to write.</param>
|
||||
/// <param name="alignment">Minimum number of characters that should be written for this value. If the value is negative, it indicates left-aligned and the required minimum is the absolute value.</param>
|
||||
/// <param name="format">The format string.</param>
|
||||
public void AppendFormatted(object? value, int alignment = 0, string? format = null) =>
|
||||
// This overload is expected to be used rarely, only if either a) something strongly typed as object is
|
||||
// formatted with both an alignment and a format, or b) the compiler is unable to target type to T. It
|
||||
// exists purely to help make cases from (b) compile. Just delegate to the T-based implementation.
|
||||
AppendFormatted<object?>(value, alignment, format);
|
||||
|
||||
private void InsertAlignment(int startingPos, int alignment)
|
||||
{
|
||||
var charsWritten = _stringBuilder._length - startingPos;
|
||||
|
||||
var paddingNeeded = alignment - charsWritten;
|
||||
if (paddingNeeded > 0)
|
||||
{
|
||||
var chars = _stringBuilder._chars;
|
||||
if (chars.Length - _stringBuilder._length < paddingNeeded)
|
||||
{
|
||||
_stringBuilder.Grow(paddingNeeded);
|
||||
}
|
||||
|
||||
chars.Slice(startingPos, charsWritten).CopyTo(chars[(startingPos + paddingNeeded)..]);
|
||||
chars.Slice(startingPos, paddingNeeded).Fill(' ');
|
||||
|
||||
_stringBuilder._length += paddingNeeded;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue