From 2c8cb129ed084e374f06eaa29f21e9ab0b9e8066 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 6 Apr 2023 07:12:46 -0400 Subject: [PATCH] Add Utf8.TryWrite (#83852) * Add Utf8.TryWrite Initial implementation of Utf8.TryWriteUtf8. The performance of this won't be great at present, but will improve as our cores types add implementation of IUtf8SpanFormattable. For tests, I copy/pasted the tests we had for MemoryExtensions.TryWrite and searched/replaced to make them work for Utf8.TryWrite. * Address PR feedback --- .../System.Private.CoreLib.Shared.projitems | 1 + .../src/System/IUtf8SpanFormattable.cs | 21 + .../src/System/Text/Unicode/Utf8.cs | 418 +++++++++ .../System.Runtime/ref/System.Runtime.cs | 27 + .../tests/System.Runtime.Tests.csproj | 1 + .../System/Text/Unicode/Utf8Tests.TryWrite.cs | 817 ++++++++++++++++++ .../tests/System/Text/Unicode/Utf8Tests.cs | 2 +- 7 files changed, 1286 insertions(+), 1 deletion(-) create mode 100644 src/libraries/System.Private.CoreLib/src/System/IUtf8SpanFormattable.cs create mode 100644 src/libraries/System.Runtime/tests/System/Text/Unicode/Utf8Tests.TryWrite.cs diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index d03e7042d0d8e..e526cb9ac33c1 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -517,6 +517,7 @@ + diff --git a/src/libraries/System.Private.CoreLib/src/System/IUtf8SpanFormattable.cs b/src/libraries/System.Private.CoreLib/src/System/IUtf8SpanFormattable.cs new file mode 100644 index 0000000000000..6815b84f0aaea --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IUtf8SpanFormattable.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System +{ + /// Provides functionality to format the string representation of an object into a span as UTF8. + public interface IUtf8SpanFormattable + { + /// Tries to format the value of the current instance as UTF8 into the provided span of bytes. + /// When this method returns, this instance's value formatted as a span of bytes. + /// When this method returns, the number of bytes that were written in . + /// A span containing the characters that represent a standard or custom format string that defines the acceptable format for . + /// An optional object that supplies culture-specific formatting information for . + /// if the formatting was successful; otherwise, . + /// + /// An implementation of this interface should produce the same string of characters as an implementation of or + /// on the same type. TryFormat should return false only if there is not enough space in the destination buffer; any other failures should throw an exception. + /// + bool TryFormat(Span destination, out int bytesWritten, ReadOnlySpan format, IFormatProvider? provider); + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/Text/Unicode/Utf8.cs b/src/libraries/System.Private.CoreLib/src/System/Text/Unicode/Utf8.cs index 94c1d57a4f164..a642035c8d7ba 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Text/Unicode/Utf8.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Text/Unicode/Utf8.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; +using System.ComponentModel; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -221,5 +222,422 @@ public static unsafe OperationStatus ToUtf16(ReadOnlySpan source, SpanWrites the specified interpolated string to the UTF8 byte span. + /// The span to which the interpolated string should be formatted. + /// The interpolated string. + /// The number of characters written to the span. + /// true if the entire interpolated string could be formatted successfully; otherwise, false. + public static bool TryWrite(Span destination, [InterpolatedStringHandlerArgument(nameof(destination))] ref TryWriteInterpolatedStringHandler handler, out int bytesWritten) + { + // The span argument isn't used directly in the method; rather, it'll be used by the compiler to create the handler. + // We could validate here that span == handler._destination, but that doesn't seem necessary. + if (handler._success) + { + bytesWritten = handler._pos; + return true; + } + + bytesWritten = 0; + return false; + } + + /// Writes the specified interpolated string to the UTF8 byte span. + /// The span to which the interpolated string should be formatted. + /// An object that supplies culture-specific formatting information. + /// The interpolated string. + /// The number of characters written to the span. + /// true if the entire interpolated string could be formatted successfully; otherwise, false. + public static bool TryWrite(Span destination, IFormatProvider? provider, [InterpolatedStringHandlerArgument(nameof(destination), nameof(provider))] ref TryWriteInterpolatedStringHandler handler, out int bytesWritten) => + // The provider is passed to the handler by the compiler, so the actual implementation of the method + // is the same as the non-provider overload. + TryWrite(destination, ref handler, out bytesWritten); + + /// Provides a handler used by the language compiler to format interpolated strings into UTF8 byte spans. + [EditorBrowsable(EditorBrowsableState.Never)] + [InterpolatedStringHandler] + public ref struct TryWriteInterpolatedStringHandler + { + /// The destination UTF8 buffer. + private readonly Span _destination; + /// Optional provider to pass to IFormattable.ToString, ISpanFormattable.TryFormat, and IUtf8SpanFormattable.TryFormat calls. + private readonly IFormatProvider? _provider; + /// The number of bytes written to . + internal int _pos; + /// true if all formatting operations have succeeded; otherwise, false. + internal bool _success; + /// Whether provides an ICustomFormatter. + private readonly bool _hasCustomFormatter; + + /// Creates a handler used to write an interpolated string into a UTF8 . + /// The number of constant characters outside of interpolation expressions in the interpolated string. + /// The number of interpolation expressions in the interpolated string. + /// The destination buffer. + /// Upon return, true if the destination may be long enough to support the formatting, or false if it won't be. + /// 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. + public TryWriteInterpolatedStringHandler(int literalLength, int formattedCount, Span destination, out bool shouldAppend) + { + _destination = destination; + _provider = null; + _pos = 0; + _success = shouldAppend = destination.Length >= literalLength; // UTF8 encoding never produces fewer bytes than input characters + _hasCustomFormatter = false; + } + + /// Creates a handler used to write an interpolated string into a UTF8 . + /// The number of constant characters outside of interpolation expressions in the interpolated string. + /// The number of interpolation expressions in the interpolated string. + /// The destination buffer. + /// An object that supplies culture-specific formatting information. + /// Upon return, true if the destination may be long enough to support the formatting, or false if it won't be. + /// 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. + public TryWriteInterpolatedStringHandler(int literalLength, int formattedCount, Span destination, IFormatProvider? provider, out bool shouldAppend) + { + _destination = destination; + _provider = provider; + _pos = 0; + _success = shouldAppend = destination.Length >= literalLength; // UTF8 encoding never produces fewer bytes than input characters + _hasCustomFormatter = provider is not null && DefaultInterpolatedStringHandler.HasCustomFormatter(provider); + } + + /// Writes the specified string to the handler. + /// The string to write. + /// true if the value could be formatted to the span; otherwise, false. + public bool AppendLiteral(string value) => AppendFormatted(value.AsSpan()); + + // TODO https://github.com/dotnet/csharplang/issues/7072: + // Add this if/when C# supports u8 literals with string interpolation. + // If that happens prior to this type being released, the above AppendLiteral(string) + // should also be removed. If that doesn't happen, we should look into ways to optimize + // the above AppendLiteral, such as by making the underlying encoding operation a JIT + // intrinsic that can emit substitute a "abc"u8 equivalent for an "abc" string literal. + //public bool AppendLiteral(scoped ReadOnlySpan value) => AppendFormatted(value); + + /// Writes the specified value to the handler. + /// The value to write. + /// The type of the value to write. + public bool AppendFormatted(T value) + { + // This method could delegate to AppendFormatted with a null format, but explicitly passing + // default as the format to TryFormat helps to improve code quality in some cases when TryFormat is inlined, + // e.g. for Int32 it enables the JIT to eliminate code in the inlined method based on a length check on the format. + + // If there's a custom formatter, always use it. + if (_hasCustomFormatter) + { + return AppendCustomFormatter(value, format: null); + } + + // If the value can format itself directly into our buffer, do so. + if (value is IUtf8SpanFormattable) + { + if (((IUtf8SpanFormattable)value).TryFormat(_destination.Slice(_pos), out int bytesWritten, format: default, _provider)) + { + _pos += bytesWritten; + return true; + } + + return Fail(); + } + + string? s; + if (value is IFormattable) + { + // If the value can format itself directly into a UTF16 buffer, do so, then transcode. + if (value is ISpanFormattable) + { + return AppendSpanFormattable(value, format: null); + } + + // If the value can ToString with the format / provider, get the resulting string, then append that. + s = ((IFormattable)value).ToString(null, _provider); + } + else + { + // Fall back to a normal ToString and append that. + s = value?.ToString(); + } + + return AppendFormatted(s.AsSpan()); + } + + /// Writes the specified value to the handler. + /// The value to write. + /// The format string. + /// The type of the value to write. + public bool AppendFormatted(T value, string? format) + { + // If there's a custom formatter, always use it. + if (_hasCustomFormatter) + { + return AppendCustomFormatter(value, format); + } + + // If the value can format itself directly into our buffer, do so. + if (value is IUtf8SpanFormattable) + { + if (((IUtf8SpanFormattable)value).TryFormat(_destination.Slice(_pos), out int bytesWritten, format, _provider)) + { + _pos += bytesWritten; + return true; + } + + return Fail(); + } + + string? s; + if (value is IFormattable) + { + // If the value can format itself directly into a UTF16 buffer, do so, then transcode. + if (value is ISpanFormattable) + { + return AppendSpanFormattable(value, format); + } + + // If the value can ToString with the format / provider, get the resulting string, then append that. + s = ((IFormattable)value).ToString(format, _provider); + } + else + { + // Fall back to a normal ToString and append that. + s = value?.ToString(); + } + + return AppendFormatted(s.AsSpan()); + } + + /// Writes the specified value to the handler. + /// The value to write. + /// 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. + /// The type of the value to write. + public bool AppendFormatted(T value, int alignment) + { + int startingPos = _pos; + if (AppendFormatted(value)) + { + return alignment == 0 || TryAppendOrInsertAlignmentIfNeeded(startingPos, alignment); + } + + return Fail(); + } + + /// Writes the specified value to the handler. + /// The value to write. + /// The format string. + /// 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. + /// The type of the value to write. + public bool AppendFormatted(T value, int alignment, string? format) + { + int startingPos = _pos; + if (AppendFormatted(value, format)) + { + return alignment == 0 || TryAppendOrInsertAlignmentIfNeeded(startingPos, alignment); + } + + return Fail(); + } + + /// Writes the specified character span to the handler. + /// The span to write. + public bool AppendFormatted(scoped ReadOnlySpan value) + { + if (FromUtf16(value, _destination.Slice(_pos), out _, out int bytesWritten) == OperationStatus.Done) + { + _pos += bytesWritten; + return true; + } + + return Fail(); + } + + /// Writes the specified string of chars to the handler. + /// The span to write. + /// 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. + /// The format string. + public bool AppendFormatted(scoped ReadOnlySpan value, int alignment = 0, string? format = null) + { + int startingPos = _pos; + if (AppendFormatted(value)) + { + return alignment == 0 || TryAppendOrInsertAlignmentIfNeeded(startingPos, alignment); + } + + return Fail(); + } + + /// Writes the specified span of UTF8 bytes to the handler. + /// The span to write. + public bool AppendFormatted(scoped ReadOnlySpan utf8Value) + { + if (utf8Value.TryCopyTo(_destination.Slice(_pos))) + { + _pos += utf8Value.Length; + return true; + } + + return Fail(); + } + + /// Writes the specified span of UTF8 bytes to the handler. + /// The span to write. + /// 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. + /// The format string. + public bool AppendFormatted(scoped ReadOnlySpan utf8Value, int alignment = 0, string? format = null) + { + int startingPos = _pos; + if (AppendFormatted(utf8Value)) + { + return alignment == 0 || TryAppendOrInsertAlignmentIfNeeded(startingPos, alignment); + } + + return Fail(); + } + + /// Writes the specified value to the handler. + /// The value to write. + public bool AppendFormatted(string? value) => + _hasCustomFormatter ? AppendCustomFormatter(value, format: null) : + AppendFormatted(value.AsSpan()); + + /// Writes the specified value to the handler. + /// The value to write. + /// 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. + /// The format string. + public bool 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 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(value, alignment, format); + + /// Writes the specified value to the handler. + /// The value to write. + /// 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. + /// The format string. + public bool 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(value, alignment, format); + + /// Formats the value using the custom formatter from the provider. + /// The value to write. + /// The format string. + /// The type of the value to write. + [MethodImpl(MethodImplOptions.NoInlining)] + private bool AppendCustomFormatter(T value, string? format) + { + // This case is very rare, but we need to handle it prior to the other checks in case + // a provider was used that supplied an ICustomFormatter which wanted to intercept the particular value. + // We do the cast here rather than in the ctor, even though this could be executed multiple times per + // formatting, to make the cast pay for play. + Debug.Assert(_hasCustomFormatter); + Debug.Assert(_provider is not null); + + ICustomFormatter? formatter = (ICustomFormatter?)_provider.GetFormat(typeof(ICustomFormatter)); + Debug.Assert(formatter is not null, "An incorrectly written provider said it implemented ICustomFormatter, and then didn't"); + + if (formatter is not null && + formatter.Format(format, value, _provider) is string customFormatted) + { + return AppendFormatted(customFormatted.AsSpan()); + } + + return true; + } + + /// Writes the specified ISpanFormattable to the handler. + /// The value to write. It must be an ISpanFormattable but isn't constrained because the caller doesn't have a cosntraint. + /// The format string. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool AppendSpanFormattable(T value, string? format) + { + Debug.Assert(value is ISpanFormattable); + + Span utf16 = stackalloc char[256]; + return ((ISpanFormattable)value).TryFormat(utf16, out int charsWritten, format, _provider) ? + AppendFormatted(utf16.Slice(0, charsWritten)) : + GrowAndAppendFormatted(ref this, value, utf16.Length, out charsWritten, format); + + [MethodImpl(MethodImplOptions.NoInlining)] + static bool GrowAndAppendFormatted(scoped ref TryWriteInterpolatedStringHandler thisRef, T value, int length, out int charsWritten, string? format) + { + Debug.Assert(value is ISpanFormattable); + + while (true) + { + int newLength = length * 2; + if ((uint)newLength > Array.MaxLength) + { + newLength = length == Array.MaxLength ? + Array.MaxLength + 1 : // force OOM + Array.MaxLength; + } + length = newLength; + + char[] array = ArrayPool.Shared.Rent(length); + try + { + if (((ISpanFormattable)value).TryFormat(array, out charsWritten, format, thisRef._provider)) + { + return thisRef.AppendFormatted(array.AsSpan(0, charsWritten)); + } + } + finally + { + ArrayPool.Shared.Return(array); + } + } + } + } + + /// Handles adding any padding required for aligning a formatted value in an interpolation expression. + /// The position at which the written value started. + /// Non-zero 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. + private bool TryAppendOrInsertAlignmentIfNeeded(int startingPos, int alignment) + { + Debug.Assert(startingPos >= 0 && startingPos <= _pos); + Debug.Assert(alignment != 0); + + int bytesWritten = _pos - startingPos; + + bool leftAlign = false; + if (alignment < 0) + { + leftAlign = true; + alignment = -alignment; + } + + int paddingNeeded = alignment - bytesWritten; + if (paddingNeeded <= 0) + { + return true; + } + + if (paddingNeeded <= _destination.Length - _pos) + { + if (leftAlign) + { + _destination.Slice(_pos, paddingNeeded).Fill((byte)' '); + } + else + { + _destination.Slice(startingPos, bytesWritten).CopyTo(_destination.Slice(startingPos + paddingNeeded)); + _destination.Slice(startingPos, paddingNeeded).Fill((byte)' '); + } + + _pos += paddingNeeded; + return true; + } + + return Fail(); + } + + /// Marks formatting as having failed and returns false. + private bool Fail() + { + _success = false; + return false; + } + } } } diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 1de1056254206..86305a208d9be 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -3865,6 +3865,10 @@ public partial interface ISpanParsable : System.IParsable where TS static abstract TSelf Parse(System.ReadOnlySpan s, System.IFormatProvider? provider); static abstract bool TryParse(System.ReadOnlySpan s, System.IFormatProvider? provider, [System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out TSelf result); } + public partial interface IUtf8SpanFormattable + { + bool TryFormat(System.Span destination, out int bytesWritten, System.ReadOnlySpan format, System.IFormatProvider? provider); + } public partial class Lazy<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]T> { public Lazy() { } @@ -14437,6 +14441,29 @@ public static partial class Utf8 { public static System.Buffers.OperationStatus FromUtf16(System.ReadOnlySpan source, System.Span destination, out int charsRead, out int bytesWritten, bool replaceInvalidSequences = true, bool isFinalBlock = true) { throw null; } public static System.Buffers.OperationStatus ToUtf16(System.ReadOnlySpan source, System.Span destination, out int bytesRead, out int charsWritten, bool replaceInvalidSequences = true, bool isFinalBlock = true) { throw null; } + public static bool TryWrite(System.Span destination, [System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute("destination")] ref System.Text.Unicode.Utf8.TryWriteInterpolatedStringHandler handler, out int bytesWritten) { throw null; } + public static bool TryWrite(System.Span destination, IFormatProvider? provider, [System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute("destination", "provider")] ref System.Text.Unicode.Utf8.TryWriteInterpolatedStringHandler handler, out int bytesWritten) { throw null; } + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] + [System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute] + public ref struct TryWriteInterpolatedStringHandler + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public TryWriteInterpolatedStringHandler(int literalLength, int formattedCount, System.Span destination, out bool shouldAppend) { throw null; } + public TryWriteInterpolatedStringHandler(int literalLength, int formattedCount, System.Span destination, IFormatProvider? provider, out bool shouldAppend) { throw null; } + public bool AppendLiteral(string value) { throw null; } + public bool AppendFormatted(scoped System.ReadOnlySpan value) { throw null; } + public bool AppendFormatted(scoped System.ReadOnlySpan value, int alignment = 0, string? format = null) { throw null; } + public bool AppendFormatted(scoped System.ReadOnlySpan utf8Value) { throw null; } + public bool AppendFormatted(scoped System.ReadOnlySpan utf8Value, int alignment = 0, string? format = null) { throw null; } + public bool AppendFormatted(T value) { throw null; } + public bool AppendFormatted(T value, string? format) { throw null; } + public bool AppendFormatted(T value, int alignment) { throw null; } + public bool AppendFormatted(T value, int alignment, string? format) { throw null; } + public bool AppendFormatted(object? value, int alignment = 0, string? format = null) { throw null; } + public bool AppendFormatted(string? value) { throw null; } + public bool AppendFormatted(string? value, int alignment = 0, string? format = null) { throw null; } + } } } namespace System.Threading diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj b/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj index 266ac502fa531..78956c7862aa5 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj @@ -317,6 +317,7 @@ + diff --git a/src/libraries/System.Runtime/tests/System/Text/Unicode/Utf8Tests.TryWrite.cs b/src/libraries/System.Runtime/tests/System/Text/Unicode/Utf8Tests.TryWrite.cs new file mode 100644 index 0000000000000..c9cdaf3d0ef87 --- /dev/null +++ b/src/libraries/System.Runtime/tests/System/Text/Unicode/Utf8Tests.TryWrite.cs @@ -0,0 +1,817 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers.Text; +using System.Globalization; +using System.Text; +using System.Text.Unicode; +using Xunit; + +namespace System.Text.Unicode.Tests +{ + public partial class Utf8Tests + { + private readonly byte[] _buffer = new byte[4096]; + + [Theory] + [InlineData(0, 0)] + [InlineData(1, 1)] + [InlineData(42, 84)] + [InlineData(-1, 0)] + [InlineData(-1, -1)] + [InlineData(-16, 1)] + public void LengthAndHoleArguments_Valid(int literalLength, int formattedCount) + { + bool shouldAppend; + + new Utf8.TryWriteInterpolatedStringHandler(literalLength, formattedCount, new byte[Math.Max(0, literalLength)], out shouldAppend); + Assert.True(shouldAppend); + + new Utf8.TryWriteInterpolatedStringHandler(literalLength, formattedCount, new byte[1 + Math.Max(0, literalLength)], out shouldAppend); + Assert.True(shouldAppend); + + if (literalLength > 0) + { + new Utf8.TryWriteInterpolatedStringHandler(literalLength, formattedCount, new byte[literalLength - 1], out shouldAppend); + Assert.False(shouldAppend); + } + + foreach (IFormatProvider provider in new IFormatProvider[] { null, new ConcatFormatter(), CultureInfo.InvariantCulture, CultureInfo.CurrentCulture, new CultureInfo("en-US"), new CultureInfo("fr-FR") }) + { + new Utf8.TryWriteInterpolatedStringHandler(literalLength, formattedCount, new byte[Math.Max(0, literalLength)], out shouldAppend); + Assert.True(shouldAppend); + + new Utf8.TryWriteInterpolatedStringHandler(literalLength, formattedCount, new byte[1 + Math.Max(0, literalLength)], out shouldAppend); + Assert.True(shouldAppend); + + if (literalLength > 0) + { + new Utf8.TryWriteInterpolatedStringHandler(literalLength, formattedCount, new byte[literalLength - 1], out shouldAppend); + Assert.False(shouldAppend); + } + } + } + + [Fact] + public void AppendLiteral() + { + var expected = new StringBuilder(); + var actual = new Utf8.TryWriteInterpolatedStringHandler(0, 0, _buffer, out _); + + foreach (string s in new[] { "", "a", "bc", "def", "this is a long string", "!" }) + { + expected.Append(s); + actual.AppendLiteral(s); + } + + Assert.True(Utf8.TryWrite(_buffer, ref actual, out int bytesWritten)); + Assert.Equal(expected.ToString(), Encoding.UTF8.GetString(_buffer.AsSpan(0, bytesWritten))); + } + + [Fact] + public void AppendFormatted_ReadOnlySpanChar() + { + var expected = new StringBuilder(); + var actual = new Utf8.TryWriteInterpolatedStringHandler(0, 0, _buffer, out _); + + foreach (string s in new[] { "", "a", "bc", "def", "this is a longer string", "!" }) + { + // span + expected.Append(s); + actual.AppendFormatted((ReadOnlySpan)s); + + // span, format + expected.AppendFormat("{0:X2}", s); + actual.AppendFormatted((ReadOnlySpan)s, format: "X2"); + + foreach (int alignment in new[] { 0, 3, -3 }) + { + // span, alignment + expected.AppendFormat("{0," + alignment.ToString(CultureInfo.InvariantCulture) + "}", s); + actual.AppendFormatted((ReadOnlySpan)s, alignment); + + // span, alignment, format + expected.AppendFormat("{0," + alignment.ToString(CultureInfo.InvariantCulture) + ":X2}", s); + actual.AppendFormatted((ReadOnlySpan)s, alignment, "X2"); + } + } + + Assert.True(Utf8.TryWrite(_buffer, ref actual, out int bytesWritten)); + Assert.Equal(expected.ToString(), Encoding.UTF8.GetString(_buffer.AsSpan(0, bytesWritten))); + } + + [Fact] + public void AppendFormatted_ReadOnlySpanByte() + { + var expected = new StringBuilder(); + var actual = new Utf8.TryWriteInterpolatedStringHandler(0, 0, _buffer, out _); + + foreach (string s in new[] { "", "a", "bc", "def", "this is a longer string", "!" }) + { + ReadOnlySpan utf8 = Encoding.UTF8.GetBytes(s); + + // span + expected.Append(s); + actual.AppendFormatted(utf8); + + // span, format + expected.AppendFormat("{0:X2}", s); + actual.AppendFormatted(utf8, format: "X2"); + + foreach (int alignment in new[] { 0, 3, -3 }) + { + // span, alignment + expected.AppendFormat("{0," + alignment.ToString(CultureInfo.InvariantCulture) + "}", s); + actual.AppendFormatted(utf8, alignment); + + // span, alignment, format + expected.AppendFormat("{0," + alignment.ToString(CultureInfo.InvariantCulture) + ":X2}", s); + actual.AppendFormatted(utf8, alignment, "X2"); + } + } + + Assert.True(Utf8.TryWrite(_buffer, ref actual, out int bytesWritten)); + Assert.Equal(expected.ToString(), Encoding.UTF8.GetString(_buffer.AsSpan(0, bytesWritten))); + } + + [Fact] + public void AppendFormatted_String() + { + var expected = new StringBuilder(); + var actual = new Utf8.TryWriteInterpolatedStringHandler(0, 0, _buffer, out _); + + foreach (string s in new[] { null, "", "a", "bc", "def", "this is a longer string", "!" }) + { + // string + expected.AppendFormat("{0}", s); + actual.AppendFormatted(s); + + // string, format + expected.AppendFormat("{0:X2}", s); + actual.AppendFormatted(s, "X2"); + + foreach (int alignment in new[] { 0, 3, -3 }) + { + // string, alignment + expected.AppendFormat("{0," + alignment.ToString(CultureInfo.InvariantCulture) + "}", s); + actual.AppendFormatted(s, alignment); + + // string, alignment, format + expected.AppendFormat("{0," + alignment.ToString(CultureInfo.InvariantCulture) + ":X2}", s); + actual.AppendFormatted(s, alignment, "X2"); + } + } + + Assert.True(Utf8.TryWrite(_buffer, ref actual, out int bytesWritten)); + Assert.Equal(expected.ToString(), Encoding.UTF8.GetString(_buffer.AsSpan(0, bytesWritten))); + } + + [Fact] + public void AppendFormatted_String_ICustomFormatter() + { + var provider = new ConcatFormatter(); + + var expected = new StringBuilder(); + var actual = new Utf8.TryWriteInterpolatedStringHandler(0, 0, _buffer, provider, out _); + + foreach (string s in new[] { null, "", "a" }) + { + // string + expected.AppendFormat(provider, "{0}", s); + actual.AppendFormatted(s); + + // string, format + expected.AppendFormat(provider, "{0:X2}", s); + actual.AppendFormatted(s, "X2"); + + // string, alignment + expected.AppendFormat(provider, "{0,3}", s); + actual.AppendFormatted(s, 3); + + // string, alignment, format + expected.AppendFormat(provider, "{0,-3:X2}", s); + actual.AppendFormatted(s, -3, "X2"); + } + + Assert.True(Utf8.TryWrite(_buffer, ref actual, out int bytesWritten)); + Assert.Equal(expected.ToString(), Encoding.UTF8.GetString(_buffer.AsSpan(0, bytesWritten))); + } + + [Fact] + public void AppendFormatted_ReferenceTypes() + { + var expected = new StringBuilder(); + var actual = new Utf8.TryWriteInterpolatedStringHandler(0, 0, _buffer, out _); + + foreach (string rawInput in new[] { null, "", "a", "bc", "def", "this is a longer string", "!" }) + { + foreach (object o in new object[] + { + rawInput, // raw string directly; ToString will return itself + new StringWrapper(rawInput), // wrapper object that returns string from ToString + new FormattableStringWrapper(rawInput), // IFormattable wrapper around string + new SpanFormattableStringWrapper(rawInput) // ISpanFormattable wrapper around string + }) + { + // object + expected.AppendFormat("{0}", o); + actual.AppendFormatted(o); + if (o is IHasToStringState tss1) + { + Assert.True(string.IsNullOrEmpty(tss1.ToStringState.LastFormat)); + AssertModeMatchesType(tss1); + } + + // object, format + expected.AppendFormat("{0:X2}", o); + actual.AppendFormatted(o, "X2"); + if (o is IHasToStringState tss2) + { + Assert.Equal("X2", tss2.ToStringState.LastFormat); + AssertModeMatchesType(tss2); + } + + foreach (int alignment in new[] { 0, 3, -3 }) + { + // object, alignment + expected.AppendFormat("{0," + alignment.ToString(CultureInfo.InvariantCulture) + "}", o); + actual.AppendFormatted(o, alignment); + if (o is IHasToStringState tss3) + { + Assert.True(string.IsNullOrEmpty(tss3.ToStringState.LastFormat)); + AssertModeMatchesType(tss3); + } + + // object, alignment, format + expected.AppendFormat("{0," + alignment.ToString(CultureInfo.InvariantCulture) + ":X2}", o); + actual.AppendFormatted(o, alignment, "X2"); + if (o is IHasToStringState tss4) + { + Assert.Equal("X2", tss4.ToStringState.LastFormat); + AssertModeMatchesType(tss4); + } + } + } + } + + Assert.True(Utf8.TryWrite(_buffer, ref actual, out int bytesWritten)); + Assert.Equal(expected.ToString(), Encoding.UTF8.GetString(_buffer.AsSpan(0, bytesWritten))); + } + + [Fact] + public void AppendFormatted_ReferenceTypes_CreateProviderFlowed() + { + var provider = new CultureInfo("en-US"); + Utf8.TryWriteInterpolatedStringHandler handler = new Utf8.TryWriteInterpolatedStringHandler(1, 2, _buffer, provider, out _); + + foreach (IHasToStringState tss in new IHasToStringState[] { new FormattableStringWrapper("hello"), new SpanFormattableStringWrapper("hello") }) + { + handler.AppendFormatted(tss); + Assert.Same(provider, tss.ToStringState.LastProvider); + + handler.AppendFormatted(tss, 1); + Assert.Same(provider, tss.ToStringState.LastProvider); + + handler.AppendFormatted(tss, "X2"); + Assert.Same(provider, tss.ToStringState.LastProvider); + + handler.AppendFormatted(tss, 1, "X2"); + Assert.Same(provider, tss.ToStringState.LastProvider); + } + } + + [Fact] + public void AppendFormatted_ReferenceTypes_ICustomFormatter() + { + var provider = new ConcatFormatter(); + + var expected = new StringBuilder(); + var actual = new Utf8.TryWriteInterpolatedStringHandler(0, 0, _buffer, provider, out _); + + foreach (string s in new[] { null, "", "a" }) + { + foreach (IHasToStringState tss in new IHasToStringState[] { new FormattableStringWrapper(s), new SpanFormattableStringWrapper(s) }) + { + void AssertTss(IHasToStringState tss, string format) + { + Assert.Equal(format, tss.ToStringState.LastFormat); + Assert.Same(provider, tss.ToStringState.LastProvider); + Assert.Equal(ToStringMode.ICustomFormatterFormat, tss.ToStringState.ToStringMode); + } + + // object + expected.AppendFormat(provider, "{0}", tss); + actual.AppendFormatted(tss); + AssertTss(tss, null); + + // object, format + expected.AppendFormat(provider, "{0:X2}", tss); + actual.AppendFormatted(tss, "X2"); + AssertTss(tss, "X2"); + + // object, alignment + expected.AppendFormat(provider, "{0,3}", tss); + actual.AppendFormatted(tss, 3); + AssertTss(tss, null); + + // object, alignment, format + expected.AppendFormat(provider, "{0,-3:X2}", tss); + actual.AppendFormatted(tss, -3, "X2"); + AssertTss(tss, "X2"); + } + } + + Assert.True(Utf8.TryWrite(_buffer, ref actual, out int bytesWritten)); + Assert.Equal(expected.ToString(), Encoding.UTF8.GetString(_buffer.AsSpan(0, bytesWritten))); + } + + [Fact] + public void AppendFormatted_ValueTypes() + { + void Test(T t) + { + var expected = new StringBuilder(); + var actual = new Utf8.TryWriteInterpolatedStringHandler(0, 0, _buffer, out _); + + // struct + expected.AppendFormat("{0}", t); + actual.AppendFormatted(t); + Assert.True(string.IsNullOrEmpty(((IHasToStringState)t).ToStringState.LastFormat)); + AssertModeMatchesType(((IHasToStringState)t)); + + // struct, format + expected.AppendFormat("{0:X2}", t); + actual.AppendFormatted(t, "X2"); + Assert.Equal("X2", ((IHasToStringState)t).ToStringState.LastFormat); + AssertModeMatchesType(((IHasToStringState)t)); + + foreach (int alignment in new[] { 0, 3, -3 }) + { + // struct, alignment + expected.AppendFormat("{0," + alignment.ToString(CultureInfo.InvariantCulture) + "}", t); + actual.AppendFormatted(t, alignment); + Assert.True(string.IsNullOrEmpty(((IHasToStringState)t).ToStringState.LastFormat)); + AssertModeMatchesType(((IHasToStringState)t)); + + // struct, alignment, format + expected.AppendFormat("{0," + alignment.ToString(CultureInfo.InvariantCulture) + ":X2}", t); + actual.AppendFormatted(t, alignment, "X2"); + Assert.Equal("X2", ((IHasToStringState)t).ToStringState.LastFormat); + AssertModeMatchesType(((IHasToStringState)t)); + } + + Assert.True(Utf8.TryWrite(_buffer, ref actual, out int bytesWritten)); + Assert.Equal(expected.ToString(), Encoding.UTF8.GetString(_buffer.AsSpan(0, bytesWritten))); + } + + Test(new FormattableInt32Wrapper(42)); + Test(new SpanFormattableInt32Wrapper(84)); + Test((FormattableInt32Wrapper?)new FormattableInt32Wrapper(42)); + Test((SpanFormattableInt32Wrapper?)new SpanFormattableInt32Wrapper(84)); + } + + [Fact] + public void AppendFormatted_ValueTypes_CreateProviderFlowed() + { + void Test(T t) + { + var provider = new CultureInfo("en-US"); + Utf8.TryWriteInterpolatedStringHandler handler = new Utf8.TryWriteInterpolatedStringHandler(1, 2, _buffer, provider, out _); + + handler.AppendFormatted(t); + Assert.Same(provider, ((IHasToStringState)t).ToStringState.LastProvider); + + handler.AppendFormatted(t, 1); + Assert.Same(provider, ((IHasToStringState)t).ToStringState.LastProvider); + + handler.AppendFormatted(t, "X2"); + Assert.Same(provider, ((IHasToStringState)t).ToStringState.LastProvider); + + handler.AppendFormatted(t, 1, "X2"); + Assert.Same(provider, ((IHasToStringState)t).ToStringState.LastProvider); + } + + Test(new FormattableInt32Wrapper(42)); + Test(new SpanFormattableInt32Wrapper(84)); + Test((FormattableInt32Wrapper?)new FormattableInt32Wrapper(42)); + Test((SpanFormattableInt32Wrapper?)new SpanFormattableInt32Wrapper(84)); + } + + [Fact] + public void AppendFormatted_ValueTypes_ICustomFormatter() + { + var provider = new ConcatFormatter(); + + void Test(T t) + { + void AssertTss(T tss, string format) + { + Assert.Equal(format, ((IHasToStringState)tss).ToStringState.LastFormat); + Assert.Same(provider, ((IHasToStringState)tss).ToStringState.LastProvider); + Assert.Equal(ToStringMode.ICustomFormatterFormat, ((IHasToStringState)tss).ToStringState.ToStringMode); + } + + var expected = new StringBuilder(); + var actual = new Utf8.TryWriteInterpolatedStringHandler(0, 0, _buffer, provider, out _); + + // struct + expected.AppendFormat(provider, "{0}", t); + actual.AppendFormatted(t); + AssertTss(t, null); + + // struct, format + expected.AppendFormat(provider, "{0:X2}", t); + actual.AppendFormatted(t, "X2"); + AssertTss(t, "X2"); + + // struct, alignment + expected.AppendFormat(provider, "{0,3}", t); + actual.AppendFormatted(t, 3); + AssertTss(t, null); + + // struct, alignment, format + expected.AppendFormat(provider, "{0,-3:X2}", t); + actual.AppendFormatted(t, -3, "X2"); + AssertTss(t, "X2"); + + Assert.True(Utf8.TryWrite(_buffer, ref actual, out int bytesWritten)); + Assert.Equal(expected.ToString(), Encoding.UTF8.GetString(_buffer.AsSpan(0, bytesWritten))); + } + + Test(new FormattableInt32Wrapper(42)); + Test(new SpanFormattableInt32Wrapper(84)); + Test((FormattableInt32Wrapper?)new FormattableInt32Wrapper(42)); + Test((SpanFormattableInt32Wrapper?)new SpanFormattableInt32Wrapper(84)); + } + + [Fact] + public void AppendFormatted_EmptyBuffer_ZeroLengthWritesSuccessful() + { + Utf8.TryWriteInterpolatedStringHandler b = new Utf8.TryWriteInterpolatedStringHandler(0, 0, _buffer.AsSpan(0, 0), out bool shouldAppend); + Assert.True(shouldAppend); + + Assert.True(b.AppendLiteral("")); + Assert.True(b.AppendFormatted((object)"", alignment: 0, format: "X2")); + Assert.True(b.AppendFormatted((string?)null)); + Assert.True(b.AppendFormatted("")); + Assert.True(b.AppendFormatted("", alignment: 0, format: "X2")); + Assert.True(b.AppendFormatted("")); + Assert.True(b.AppendFormatted("", alignment: 0)); + Assert.True(b.AppendFormatted("", format: "X2")); + Assert.True(b.AppendFormatted("", alignment: 0, format: "X2")); + Assert.True(b.AppendFormatted("".AsSpan())); + Assert.True(b.AppendFormatted("".AsSpan(), alignment: 0, format: "X2")); + + Assert.True(Utf8.TryWrite(_buffer.AsSpan(0, 0), ref b, out int bytesWritten)); + Assert.Equal(0, bytesWritten); + } + + [Theory] + [InlineData(0)] + [InlineData(100)] + public void AppendFormatted_BufferTooSmall(int bufferLength) + { + Span buffer = _buffer.AsSpan(0, bufferLength); + + for (int i = 0; i <= 31; i++) + { + Utf8.TryWriteInterpolatedStringHandler b = new Utf8.TryWriteInterpolatedStringHandler(0, 0, buffer, out bool shouldAppend); + Assert.True(shouldAppend); + + Assert.True(b.AppendLiteral(new string('s', bufferLength))); + + bool result = i switch + { + 0 => b.AppendLiteral(" "), + 1 => b.AppendFormatted((object)" ", alignment: 0, format: "X2"), + 2 => b.AppendFormatted(" "), + 3 => b.AppendFormatted(" ", alignment: 0, format: "X2"), + 4 => b.AppendFormatted(" "), + 5 => b.AppendFormatted(" ", alignment: 0), + 6 => b.AppendFormatted(" ", format: "X2"), + 7 => b.AppendFormatted(" ", alignment: 0, format: "X2"), + 8 => b.AppendFormatted(" ".AsSpan()), + 9 => b.AppendFormatted(" ".AsSpan(), alignment: 0, format: "X2"), + 10 => b.AppendFormatted(new FormattableStringWrapper(" ")), + 11 => b.AppendFormatted(new FormattableStringWrapper(" "), alignment: 0), + 12 => b.AppendFormatted(new FormattableStringWrapper(" "), format: "X2"), + 13 => b.AppendFormatted(new FormattableStringWrapper(" "), alignment: 0, format: "X2"), + 14 => b.AppendFormatted(new SpanFormattableStringWrapper(" ")), + 15 => b.AppendFormatted(new SpanFormattableStringWrapper(" "), alignment: 0), + 16 => b.AppendFormatted(new SpanFormattableStringWrapper(" "), format: "X2"), + 17 => b.AppendFormatted(new SpanFormattableStringWrapper(" "), alignment: 0, format: "X2"), + 18 => b.AppendFormatted(new FormattableInt32Wrapper(1)), + 19 => b.AppendFormatted(new FormattableInt32Wrapper(1), alignment: 0), + 20 => b.AppendFormatted(new FormattableInt32Wrapper(1), format: "X2"), + 21 => b.AppendFormatted(new FormattableInt32Wrapper(1), alignment: 0, format: "X2"), + 22 => b.AppendFormatted(new SpanFormattableInt32Wrapper(1)), + 23 => b.AppendFormatted(new SpanFormattableInt32Wrapper(1), alignment: 0), + 24 => b.AppendFormatted(new SpanFormattableInt32Wrapper(1), format: "X2"), + 25 => b.AppendFormatted(new SpanFormattableInt32Wrapper(1), alignment: 0, format: "X2"), + 26 => b.AppendFormatted("", alignment: 1), + 27 => b.AppendFormatted("", alignment: -1), + 28 => b.AppendFormatted(" ", alignment: 1, format: "X2"), + 29 => b.AppendFormatted(" ", alignment: -1, format: "X2"), + 30 => b.AppendFormatted(" "u8), + 31 => b.AppendFormatted(" "u8, alignment: 0, format: "X2"), + _ => throw new Exception(), + }; + Assert.False(result); + + Assert.False(Utf8.TryWrite(buffer, ref b, out int bytesWritten)); + Assert.Equal(0, bytesWritten); + } + } + + [Fact] + public void AppendFormatted_BufferTooSmall_CustomFormatter() + { + var provider = new ConstFormatter(" "); + + Utf8.TryWriteInterpolatedStringHandler b = new Utf8.TryWriteInterpolatedStringHandler(0, 0, _buffer.AsSpan(0, 0), provider, out bool shouldAppend); + Assert.True(shouldAppend); + + // don't use custom formatter + Assert.True(b.AppendLiteral("")); + Assert.True(b.AppendFormatted("".AsSpan())); + Assert.True(b.AppendFormatted("".AsSpan(), alignment: 0, format: "X2")); + Assert.True(b.AppendFormatted(""u8)); + Assert.True(b.AppendFormatted(""u8, alignment: 0, format: "X2")); + + // do use custom formatter + Assert.False(b.AppendFormatted((object)"", alignment: 0, format: "X2")); + Assert.False(b.AppendFormatted((string?)null)); + Assert.False(b.AppendFormatted("")); + Assert.False(b.AppendFormatted("", alignment: 0, format: "X2")); + Assert.False(b.AppendFormatted("")); + Assert.False(b.AppendFormatted("", alignment: 0)); + Assert.False(b.AppendFormatted("", format: "X2")); + Assert.False(b.AppendFormatted("", alignment: 0, format: "X2")); + + Assert.False(Utf8.TryWrite(_buffer.AsSpan(0, 0), ref b, out int bytesWritten)); + Assert.Equal(0, bytesWritten); + } + + [Fact] + public void UseInterpolationSyntax_ResultMatchesExpected() + { + int int32Value = 1024; + bool boolValue = true; + Version versionValue = Version.Parse("1.2.3.4"); + + bool formatted = Utf8.TryWrite(_buffer, $"Hello {int32Value,8:X4} {boolValue} {versionValue}", out int bytesWritten); + Assert.True(formatted); + Assert.Equal("Hello 0400 True 1.2.3.4", Encoding.UTF8.GetString(_buffer.AsSpan(0, bytesWritten))); + } + + private static void AssertModeMatchesType(T tss) where T : IHasToStringState + { + ToStringMode expected = + tss is ISpanFormattable ? ToStringMode.ISpanFormattableTryFormat : + tss is IFormattable ? ToStringMode.IFormattableToString : + ToStringMode.ObjectToString; + Assert.Equal(expected, tss.ToStringState.ToStringMode); + } + + private struct Utf8SpanFormattableInt32Wrapper : IUtf8SpanFormattable, IHasToStringState + { + private readonly int _value; + public ToStringState ToStringState { get; } + + public Utf8SpanFormattableInt32Wrapper(int value) + { + ToStringState = new ToStringState(); + _value = value; + } + + public bool TryFormat(Span destination, out int bytesWritten, ReadOnlySpan format, IFormatProvider provider) + { + ToStringState.LastFormat = format.ToString(); + ToStringState.LastProvider = provider; + ToStringState.ToStringMode = ToStringMode.ISpanFormattableTryFormat; + + ReadOnlySpan src = Encoding.UTF8.GetBytes(_value.ToString(format.ToString(), provider)).AsSpan(); + if (src.TryCopyTo(destination)) + { + bytesWritten = src.Length; + return true; + } + + bytesWritten = 0; + return false; + } + + public string ToString(string format, IFormatProvider formatProvider) + { + ToStringState.LastFormat = format; + ToStringState.LastProvider = formatProvider; + ToStringState.ToStringMode = ToStringMode.IFormattableToString; + return _value.ToString(format, formatProvider); + } + + public override string ToString() + { + ToStringState.LastFormat = null; + ToStringState.LastProvider = null; + ToStringState.ToStringMode = ToStringMode.ObjectToString; + return _value.ToString(); + } + } + + private sealed class SpanFormattableStringWrapper : IFormattable, ISpanFormattable, IHasToStringState + { + private readonly string _value; + public ToStringState ToStringState { get; } = new ToStringState(); + + public SpanFormattableStringWrapper(string value) => _value = value; + + public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider provider) + { + ToStringState.LastFormat = format.ToString(); + ToStringState.LastProvider = provider; + ToStringState.ToStringMode = ToStringMode.ISpanFormattableTryFormat; + + if (_value is null) + { + charsWritten = 0; + return true; + } + + if (_value.Length > destination.Length) + { + charsWritten = 0; + return false; + } + + charsWritten = _value.Length; + _value.AsSpan().CopyTo(destination); + return true; + } + + public string ToString(string format, IFormatProvider formatProvider) + { + ToStringState.LastFormat = format; + ToStringState.LastProvider = formatProvider; + ToStringState.ToStringMode = ToStringMode.IFormattableToString; + return _value; + } + + public override string ToString() + { + ToStringState.LastFormat = null; + ToStringState.LastProvider = null; + ToStringState.ToStringMode = ToStringMode.ObjectToString; + return _value; + } + } + + private struct SpanFormattableInt32Wrapper : IFormattable, ISpanFormattable, IHasToStringState + { + private readonly int _value; + public ToStringState ToStringState { get; } + + public SpanFormattableInt32Wrapper(int value) + { + ToStringState = new ToStringState(); + _value = value; + } + + public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider provider) + { + ToStringState.LastFormat = format.ToString(); + ToStringState.LastProvider = provider; + ToStringState.ToStringMode = ToStringMode.ISpanFormattableTryFormat; + + return _value.TryFormat(destination, out charsWritten, format, provider); + } + + public string ToString(string format, IFormatProvider formatProvider) + { + ToStringState.LastFormat = format; + ToStringState.LastProvider = formatProvider; + ToStringState.ToStringMode = ToStringMode.IFormattableToString; + return _value.ToString(format, formatProvider); + } + + public override string ToString() + { + ToStringState.LastFormat = null; + ToStringState.LastProvider = null; + ToStringState.ToStringMode = ToStringMode.ObjectToString; + return _value.ToString(); + } + } + + private sealed class FormattableStringWrapper : IFormattable, IHasToStringState + { + private readonly string _value; + public ToStringState ToStringState { get; } = new ToStringState(); + + public FormattableStringWrapper(string s) => _value = s; + + public string ToString(string format, IFormatProvider formatProvider) + { + ToStringState.LastFormat = format; + ToStringState.LastProvider = formatProvider; + ToStringState.ToStringMode = ToStringMode.IFormattableToString; + return _value; + } + + public override string ToString() + { + ToStringState.LastFormat = null; + ToStringState.LastProvider = null; + ToStringState.ToStringMode = ToStringMode.ObjectToString; + return _value; + } + } + + private struct FormattableInt32Wrapper : IFormattable, IHasToStringState + { + private readonly int _value; + public ToStringState ToStringState { get; } + + public FormattableInt32Wrapper(int i) + { + ToStringState = new ToStringState(); + _value = i; + } + + public string ToString(string format, IFormatProvider formatProvider) + { + ToStringState.LastFormat = format; + ToStringState.LastProvider = formatProvider; + ToStringState.ToStringMode = ToStringMode.IFormattableToString; + return _value.ToString(format, formatProvider); + } + + public override string ToString() + { + ToStringState.LastFormat = null; + ToStringState.LastProvider = null; + ToStringState.ToStringMode = ToStringMode.ObjectToString; + return _value.ToString(); + } + } + + private sealed class ToStringState + { + public string LastFormat { get; set; } + public IFormatProvider LastProvider { get; set; } + public ToStringMode ToStringMode { get; set; } + } + + private interface IHasToStringState + { + ToStringState ToStringState { get; } + } + + private enum ToStringMode + { + ObjectToString, + IFormattableToString, + ISpanFormattableTryFormat, + ICustomFormatterFormat, + } + + private sealed class StringWrapper + { + private readonly string _value; + + public StringWrapper(string s) => _value = s; + + public override string ToString() => _value; + } + + private sealed class ConcatFormatter : IFormatProvider, ICustomFormatter + { + public object GetFormat(Type formatType) => formatType == typeof(ICustomFormatter) ? this : null; + + public string Format(string format, object arg, IFormatProvider formatProvider) + { + string s = format + " " + arg + formatProvider; + + if (arg is IHasToStringState tss) + { + // Set after using arg.ToString() in concat above + tss.ToStringState.LastFormat = format; + tss.ToStringState.LastProvider = formatProvider; + tss.ToStringState.ToStringMode = ToStringMode.ICustomFormatterFormat; + } + + return s; + } + } + + private sealed class ConstFormatter : IFormatProvider, ICustomFormatter + { + private readonly string _value; + + public ConstFormatter(string value) => _value = value; + + public object GetFormat(Type formatType) => formatType == typeof(ICustomFormatter) ? this : null; + + public string Format(string format, object arg, IFormatProvider formatProvider) => _value; + } + } +} diff --git a/src/libraries/System.Runtime/tests/System/Text/Unicode/Utf8Tests.cs b/src/libraries/System.Runtime/tests/System/Text/Unicode/Utf8Tests.cs index 6db4ba15cba37..a24438dcbb59f 100644 --- a/src/libraries/System.Runtime/tests/System/Text/Unicode/Utf8Tests.cs +++ b/src/libraries/System.Runtime/tests/System/Text/Unicode/Utf8Tests.cs @@ -10,7 +10,7 @@ namespace System.Text.Unicode.Tests { - public class Utf8Tests + public partial class Utf8Tests { private const string X_UTF8 = "58"; // U+0058 LATIN CAPITAL LETTER X, 1 byte private const string X_UTF16 = "X";