From ad01821ad8d0bbd305c3e72985f57e7e152a29a1 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 13 Apr 2023 10:22:35 -0400 Subject: [PATCH] Implement IUtf8SpanFormattable on Complex --- .../ref/System.Runtime.Numerics.cs | 5 +- .../src/System/Numerics/Complex.cs | 90 +++++++++++-------- .../tests/ComplexTests.cs | 83 ++++++++++++++--- 3 files changed, 125 insertions(+), 53 deletions(-) diff --git a/src/libraries/System.Runtime.Numerics/ref/System.Runtime.Numerics.cs b/src/libraries/System.Runtime.Numerics/ref/System.Runtime.Numerics.cs index 0d371c4d7886c..eb870ed17e3d5 100644 --- a/src/libraries/System.Runtime.Numerics/ref/System.Runtime.Numerics.cs +++ b/src/libraries/System.Runtime.Numerics/ref/System.Runtime.Numerics.cs @@ -240,7 +240,7 @@ namespace System.Numerics public static bool TryParse([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? value, out System.Numerics.BigInteger result) { throw null; } public bool TryWriteBytes(System.Span destination, out int bytesWritten, bool isUnsigned = false, bool isBigEndian = false) { throw null; } } - public readonly partial struct Complex : System.IEquatable, System.IFormattable, System.IParsable, System.ISpanFormattable, System.ISpanParsable, System.Numerics.IAdditionOperators, System.Numerics.IAdditiveIdentity, System.Numerics.IDecrementOperators, System.Numerics.IDivisionOperators, System.Numerics.IEqualityOperators, System.Numerics.IIncrementOperators, System.Numerics.IMultiplicativeIdentity, System.Numerics.IMultiplyOperators, System.Numerics.INumberBase, System.Numerics.ISignedNumber, System.Numerics.ISubtractionOperators, System.Numerics.IUnaryNegationOperators, System.Numerics.IUnaryPlusOperators + public readonly partial struct Complex : System.IEquatable, System.IFormattable, System.IParsable, System.ISpanFormattable, System.ISpanParsable, System.Numerics.IAdditionOperators, System.Numerics.IAdditiveIdentity, System.Numerics.IDecrementOperators, System.Numerics.IDivisionOperators, System.Numerics.IEqualityOperators, System.Numerics.IIncrementOperators, System.Numerics.IMultiplicativeIdentity, System.Numerics.IMultiplyOperators, System.Numerics.INumberBase, System.Numerics.ISignedNumber, System.Numerics.ISubtractionOperators, System.Numerics.IUnaryNegationOperators, System.Numerics.IUnaryPlusOperators, System.IUtf8SpanFormattable { private readonly int _dummyPrimitive; public static readonly System.Numerics.Complex ImaginaryOne; @@ -376,7 +376,8 @@ namespace System.Numerics public string ToString(System.IFormatProvider? provider) { throw null; } public string ToString([System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("NumericFormat")] string? format) { throw null; } public string ToString([System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("NumericFormat")] string? format, System.IFormatProvider? provider) { throw null; } - public bool TryFormat(System.Span destination, out int charsWritten, System.ReadOnlySpan format, System.IFormatProvider? provider) { throw null; } + public bool TryFormat(System.Span destination, out int charsWritten, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("NumericFormat")] System.ReadOnlySpan format = default, System.IFormatProvider? provider = null) { throw null; } + public bool TryFormat(System.Span utf8Destination, out int bytesWritten, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("NumericFormat")] System.ReadOnlySpan format = default, System.IFormatProvider? provider = null) { throw null; } public static bool TryParse(System.ReadOnlySpan s, System.Globalization.NumberStyles style, System.IFormatProvider? provider, out System.Numerics.Complex result) { throw null; } public static bool TryParse(System.ReadOnlySpan s, System.IFormatProvider? provider, out System.Numerics.Complex result) { throw null; } public static bool TryParse([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? s, System.Globalization.NumberStyles style, System.IFormatProvider? provider, out System.Numerics.Complex result) { throw null; } diff --git a/src/libraries/System.Runtime.Numerics/src/System/Numerics/Complex.cs b/src/libraries/System.Runtime.Numerics/src/System/Numerics/Complex.cs index 0e73eb3b94960..88f8ef25fe205 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Numerics/Complex.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Numerics/Complex.cs @@ -19,7 +19,8 @@ public readonly struct Complex : IEquatable, IFormattable, INumberBase, - ISignedNumber + ISignedNumber, + IUtf8SpanFormattable { private const NumberStyles DefaultNumberStyle = NumberStyles.Float | NumberStyles.AllowThousands; @@ -393,14 +394,23 @@ public bool Equals(Complex value) public override int GetHashCode() => HashCode.Combine(m_real, m_imaginary); - public override string ToString() => $"<{m_real}; {m_imaginary}>"; + public override string ToString() => ToString(null, null); public string ToString([StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format) => ToString(format, null); public string ToString(IFormatProvider? provider) => ToString(null, provider); - public string ToString([StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format, IFormatProvider? provider) => - $"<{m_real.ToString(format, provider)}; {m_imaginary.ToString(format, provider)}>"; + public string ToString([StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format, IFormatProvider? provider) + { + // $"<{m_real.ToString(format, provider)}; {m_imaginary.ToString(format, provider)}>"; + var handler = new DefaultInterpolatedStringHandler(4, 2, provider, stackalloc char[512]); + handler.AppendLiteral("<"); + handler.AppendFormatted(m_real, format); + handler.AppendLiteral("; "); + handler.AppendFormatted(m_imaginary, format); + handler.AppendLiteral(">"); + return handler.ToStringAndClear(); + } public static Complex Sin(Complex value) { @@ -2199,46 +2209,52 @@ public static bool TryParse([NotNullWhen(true)] string? s, NumberStyles style, I // /// - public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) - { - int charsWrittenSoFar = 0; - - // We have at least 6 more characters for: <0; 0> - if (destination.Length < 6) - { - charsWritten = charsWrittenSoFar; - return false; - } + public bool TryFormat(Span destination, out int charsWritten, [StringSyntax(StringSyntaxAttribute.NumericFormat)] ReadOnlySpan format = default, IFormatProvider? provider = null) => + TryFormatCore(destination, out charsWritten, format, provider); - destination[charsWrittenSoFar++] = '<'; + public bool TryFormat(Span utf8Destination, out int bytesWritten, [StringSyntax(StringSyntaxAttribute.NumericFormat)] ReadOnlySpan format = default, IFormatProvider? provider = null) => + TryFormatCore(utf8Destination, out bytesWritten, format, provider); - bool tryFormatSucceeded = m_real.TryFormat(destination.Slice(charsWrittenSoFar), out int tryFormatCharsWritten, format, provider); - charsWrittenSoFar += tryFormatCharsWritten; + private bool TryFormatCore(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) where TChar : unmanaged, IBinaryInteger + { + Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); - // We have at least 4 more characters for: ; 0> - if (!tryFormatSucceeded || (destination.Length < (charsWrittenSoFar + 4))) + // We have at least 6 more characters for: <0; 0> + if (destination.Length >= 6) { - charsWritten = charsWrittenSoFar; - return false; - } - - destination[charsWrittenSoFar++] = ';'; - destination[charsWrittenSoFar++] = ' '; - - tryFormatSucceeded = m_imaginary.TryFormat(destination.Slice(charsWrittenSoFar), out tryFormatCharsWritten, format, provider); - charsWrittenSoFar += tryFormatCharsWritten; + int realChars; + if (typeof(TChar) == typeof(char) ? + m_real.TryFormat(MemoryMarshal.Cast(destination.Slice(1)), out realChars, format, provider) : + m_real.TryFormat(MemoryMarshal.Cast(destination.Slice(1)), out realChars, format, provider)) + { + destination[0] = TChar.CreateTruncating('<'); + destination = destination.Slice(1 + realChars); // + 1 for < - // We have at least 1 more character for: > - if (!tryFormatSucceeded || (destination.Length < (charsWrittenSoFar + 1))) - { - charsWritten = charsWrittenSoFar; - return false; + // We have at least 4 more characters for: ; 0> + if (destination.Length >= 4) + { + int imaginaryChars; + if (typeof(TChar) == typeof(char) ? + m_imaginary.TryFormat(MemoryMarshal.Cast(destination.Slice(2)), out imaginaryChars, format, provider) : + m_imaginary.TryFormat(MemoryMarshal.Cast(destination.Slice(2)), out imaginaryChars, format, provider)) + { + // We have 1 more character for: > + if ((uint)(2 + imaginaryChars) < (uint)destination.Length) + { + destination[0] = TChar.CreateTruncating(';'); + destination[1] = TChar.CreateTruncating(' '); + destination[2 + imaginaryChars] = TChar.CreateTruncating('>'); + + charsWritten = realChars + imaginaryChars + 4; + return true; + } + } + } + } } - destination[charsWrittenSoFar++] = '>'; - - charsWritten = charsWrittenSoFar; - return true; + charsWritten = 0; + return false; } // diff --git a/src/libraries/System.Runtime.Numerics/tests/ComplexTests.cs b/src/libraries/System.Runtime.Numerics/tests/ComplexTests.cs index c5f0f4fefaa81..ea37fb5dc4614 100644 --- a/src/libraries/System.Runtime.Numerics/tests/ComplexTests.cs +++ b/src/libraries/System.Runtime.Numerics/tests/ComplexTests.cs @@ -3,8 +3,9 @@ using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Runtime.CompilerServices; - +using System.Text; using Xunit; namespace System.Numerics.Tests @@ -1730,25 +1731,79 @@ public static void Tanh_Advanced(double real, double imaginary, double expectedR public static void ToStringTest(double real, double imaginary) { var complex = new Complex(real, imaginary); + NumberFormatInfo numberFormatInfo = CultureInfo.CurrentCulture.NumberFormat; - string expected = "<" + real.ToString() + "; " + imaginary.ToString() + ">"; - string actual = complex.ToString(); - Assert.Equal(expected, actual); + Assert.Equal($"<{real}; {imaginary}>", complex.ToString()); - NumberFormatInfo numberFormatInfo = CultureInfo.CurrentCulture.NumberFormat; - expected = "<" + real.ToString(numberFormatInfo) + "; " + imaginary.ToString(numberFormatInfo) + ">"; - actual = complex.ToString(numberFormatInfo); - Assert.Equal(expected, complex.ToString(numberFormatInfo)); + Assert.Equal($"<{real.ToString(numberFormatInfo)}; {imaginary.ToString(numberFormatInfo)}>", complex.ToString(numberFormatInfo)); + Assert.Equal($"<{real.ToString((string)null)}; {imaginary.ToString((string)null)}>", complex.ToString((string)null)); + Assert.Equal($"<{real.ToString((string)null, numberFormatInfo)}; {imaginary.ToString((string)null, numberFormatInfo)}>", complex.ToString((string)null, numberFormatInfo)); foreach (string format in s_supportedStandardNumericFormats) { - expected = "<" + real.ToString(format) + "; " + imaginary.ToString(format) + ">"; - actual = complex.ToString(format); - Assert.Equal(expected, actual); + Assert.Equal($"<{real.ToString(format)}; {imaginary.ToString(format)}>", complex.ToString(format)); + Assert.Equal($"<{real.ToString(format, numberFormatInfo)}; {imaginary.ToString(format, numberFormatInfo)}>", complex.ToString(format, numberFormatInfo)); + } + } + + [Theory] + [MemberData(nameof(Boundaries_2_TestData))] + [MemberData(nameof(Primitives_2_TestData))] + [MemberData(nameof(Random_2_TestData))] + [MemberData(nameof(SmallRandom_2_TestData))] + [MemberData(nameof(Invalid_2_TestData))] + public static void TryFormatTest(double real, double imaginary) + { + var complex = new Complex(real, imaginary); + + // UTF16 + { + foreach (NumberFormatInfo numberFormatInfo in new[] { CultureInfo.CurrentCulture.NumberFormat, null }) + { + foreach (string format in s_supportedStandardNumericFormats.Append(null)) + { + string expected = $"<{real.ToString(format, numberFormatInfo)}; {imaginary.ToString(format, numberFormatInfo)}>"; + int charsWritten; + + // Just right or larger than required storage + for (int additional = 0; additional < 2; additional++) + { + char[] chars = new char[expected.Length + additional]; + Assert.True(complex.TryFormat(chars, out charsWritten, format, numberFormatInfo)); + Assert.Equal(expected.Length, charsWritten); + Assert.Equal(expected, new string(expected.AsSpan(0, expected.Length))); + } + + // Too small storage + Assert.False(complex.TryFormat(new char[expected.Length - 1], out charsWritten, format, numberFormatInfo)); + Assert.Equal(0, charsWritten); + } + } + } - expected = "<" + real.ToString(format, numberFormatInfo) + "; " + imaginary.ToString(format, numberFormatInfo) + ">"; - actual = complex.ToString(format, numberFormatInfo); - Assert.Equal(expected, actual); + // UTF8 + { + foreach (NumberFormatInfo numberFormatInfo in new[] { CultureInfo.CurrentCulture.NumberFormat, null }) + { + foreach (string format in s_supportedStandardNumericFormats.Append(null)) + { + byte[] expected = Encoding.UTF8.GetBytes($"<{real.ToString(format, numberFormatInfo)}; {imaginary.ToString(format, numberFormatInfo)}>"); + int bytesWritten; + + // Just right or larger than required storage + for (int additional = 0; additional < 2; additional++) + { + byte[] bytes = new byte[expected.Length + additional]; + Assert.True(complex.TryFormat(bytes, out bytesWritten, format, numberFormatInfo)); + Assert.Equal(expected.Length, bytesWritten); + Assert.Equal(expected, bytes.AsSpan(0, expected.Length).ToArray()); + } + + // Too small storage + Assert.False(complex.TryFormat(new byte[expected.Length - 1], out bytesWritten, format, numberFormatInfo)); + Assert.Equal(0, bytesWritten); + } + } } }