Implement IUtf8SpanFormattable on Complex (#84779)
authorStephen Toub <stoub@microsoft.com>
Mon, 17 Apr 2023 15:21:04 +0000 (11:21 -0400)
committerGitHub <noreply@github.com>
Mon, 17 Apr 2023 15:21:04 +0000 (11:21 -0400)
src/libraries/System.Runtime.Numerics/ref/System.Runtime.Numerics.cs
src/libraries/System.Runtime.Numerics/src/System/Numerics/Complex.cs
src/libraries/System.Runtime.Numerics/tests/ComplexTests.cs

index 0d371c4..eb870ed 100644 (file)
@@ -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<byte> destination, out int bytesWritten, bool isUnsigned = false, bool isBigEndian = false) { throw null; }
     }
-    public readonly partial struct Complex : System.IEquatable<System.Numerics.Complex>, System.IFormattable, System.IParsable<System.Numerics.Complex>, System.ISpanFormattable, System.ISpanParsable<System.Numerics.Complex>, System.Numerics.IAdditionOperators<System.Numerics.Complex, System.Numerics.Complex, System.Numerics.Complex>, System.Numerics.IAdditiveIdentity<System.Numerics.Complex, System.Numerics.Complex>, System.Numerics.IDecrementOperators<System.Numerics.Complex>, System.Numerics.IDivisionOperators<System.Numerics.Complex, System.Numerics.Complex, System.Numerics.Complex>, System.Numerics.IEqualityOperators<System.Numerics.Complex, System.Numerics.Complex, bool>, System.Numerics.IIncrementOperators<System.Numerics.Complex>, System.Numerics.IMultiplicativeIdentity<System.Numerics.Complex, System.Numerics.Complex>, System.Numerics.IMultiplyOperators<System.Numerics.Complex, System.Numerics.Complex, System.Numerics.Complex>, System.Numerics.INumberBase<System.Numerics.Complex>, System.Numerics.ISignedNumber<System.Numerics.Complex>, System.Numerics.ISubtractionOperators<System.Numerics.Complex, System.Numerics.Complex, System.Numerics.Complex>, System.Numerics.IUnaryNegationOperators<System.Numerics.Complex, System.Numerics.Complex>, System.Numerics.IUnaryPlusOperators<System.Numerics.Complex, System.Numerics.Complex>
+    public readonly partial struct Complex : System.IEquatable<System.Numerics.Complex>, System.IFormattable, System.IParsable<System.Numerics.Complex>, System.ISpanFormattable, System.ISpanParsable<System.Numerics.Complex>, System.Numerics.IAdditionOperators<System.Numerics.Complex, System.Numerics.Complex, System.Numerics.Complex>, System.Numerics.IAdditiveIdentity<System.Numerics.Complex, System.Numerics.Complex>, System.Numerics.IDecrementOperators<System.Numerics.Complex>, System.Numerics.IDivisionOperators<System.Numerics.Complex, System.Numerics.Complex, System.Numerics.Complex>, System.Numerics.IEqualityOperators<System.Numerics.Complex, System.Numerics.Complex, bool>, System.Numerics.IIncrementOperators<System.Numerics.Complex>, System.Numerics.IMultiplicativeIdentity<System.Numerics.Complex, System.Numerics.Complex>, System.Numerics.IMultiplyOperators<System.Numerics.Complex, System.Numerics.Complex, System.Numerics.Complex>, System.Numerics.INumberBase<System.Numerics.Complex>, System.Numerics.ISignedNumber<System.Numerics.Complex>, System.Numerics.ISubtractionOperators<System.Numerics.Complex, System.Numerics.Complex, System.Numerics.Complex>, System.Numerics.IUnaryNegationOperators<System.Numerics.Complex, System.Numerics.Complex>, System.Numerics.IUnaryPlusOperators<System.Numerics.Complex, System.Numerics.Complex>, 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<char> destination, out int charsWritten, System.ReadOnlySpan<char> format, System.IFormatProvider? provider) { throw null; }
+        public bool TryFormat(System.Span<char> destination, out int charsWritten, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("NumericFormat")] System.ReadOnlySpan<char> format = default, System.IFormatProvider? provider = null) { throw null; }
+        public bool TryFormat(System.Span<byte> utf8Destination, out int bytesWritten, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("NumericFormat")] System.ReadOnlySpan<char> format = default, System.IFormatProvider? provider = null) { throw null; }
         public static bool TryParse(System.ReadOnlySpan<char> s, System.Globalization.NumberStyles style, System.IFormatProvider? provider, out System.Numerics.Complex result) { throw null; }
         public static bool TryParse(System.ReadOnlySpan<char> 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; }
index 0e73eb3..88f8ef2 100644 (file)
@@ -19,7 +19,8 @@ namespace System.Numerics
         : IEquatable<Complex>,
           IFormattable,
           INumberBase<Complex>,
-          ISignedNumber<Complex>
+          ISignedNumber<Complex>,
+          IUtf8SpanFormattable
     {
         private const NumberStyles DefaultNumberStyle = NumberStyles.Float | NumberStyles.AllowThousands;
 
@@ -393,14 +394,23 @@ namespace System.Numerics
 
         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 @@ namespace System.Numerics
         //
 
         /// <inheritdoc cref="ISpanFormattable.TryFormat(Span{char}, out int, ReadOnlySpan{char}, IFormatProvider?)" />
-        public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> 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<char> destination, out int charsWritten, [StringSyntax(StringSyntaxAttribute.NumericFormat)] ReadOnlySpan<char> format = default, IFormatProvider? provider = null) =>
+            TryFormatCore(destination, out charsWritten, format, provider);
 
-            destination[charsWrittenSoFar++] = '<';
+        public bool TryFormat(Span<byte> utf8Destination, out int bytesWritten, [StringSyntax(StringSyntaxAttribute.NumericFormat)] ReadOnlySpan<char> 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<TChar>(Span<TChar> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider) where TChar : unmanaged, IBinaryInteger<TChar>
+        {
+            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<TChar, char>(destination.Slice(1)), out realChars, format, provider) :
+                    m_real.TryFormat(MemoryMarshal.Cast<TChar, byte>(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<TChar, char>(destination.Slice(2)), out imaginaryChars, format, provider) :
+                            m_imaginary.TryFormat(MemoryMarshal.Cast<TChar, byte>(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;
         }
 
         //
index c5f0f4f..ea37fb5 100644 (file)
@@ -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 @@ namespace System.Numerics.Tests
         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);
+                    }
+                }
             }
         }