File: src\runtime\src\libraries\System.Private.CoreLib\src\System\Number.Formatting.cs
Web Access
Project: System.Private.CoreLib.csproj (System.Private.CoreLib)
// 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;
using System.Buffers.Binary;
using System.Buffers.Text;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;

namespace System
{
    // The Format methods provided by the numeric classes convert
    // the numeric value to a string using the format string given by the
    // format parameter. If the format parameter is null or
    // an empty string, the number is formatted as if the string "G" (general
    // format) was specified. The info parameter specifies the
    // NumberFormatInfo instance to use when formatting the number. If the
    // info parameter is null or omitted, the numeric formatting information
    // is obtained from the current culture. The NumberFormatInfo supplies
    // such information as the characters to use for decimal and thousand
    // separators, and the spelling and placement of currency symbols in monetary
    // values.
    //
    // Format strings fall into two categories: Standard format strings and
    // user-defined format strings. A format string consisting of a single
    // alphabetic character (A-Z or a-z), optionally followed by a sequence of
    // digits (0-9), is a standard format string. All other format strings are
    // used-defined format strings.
    //
    // A standard format string takes the form Axx, where A is an
    // alphabetic character called the format specifier and xx is a
    // sequence of digits called the precision specifier. The format
    // specifier controls the type of formatting applied to the number and the
    // precision specifier controls the number of significant digits or decimal
    // places of the formatting operation. The following table describes the
    // supported standard formats.
    //
    // C c - Currency format. The number is
    // converted to a string that represents a currency amount. The conversion is
    // controlled by the currency format information of the NumberFormatInfo
    // used to format the number. The precision specifier indicates the desired
    // number of decimal places. If the precision specifier is omitted, the default
    // currency precision given by the NumberFormatInfo is used.
    //
    // D d - Decimal format. This format is
    // supported for integral types only. The number is converted to a string of
    // decimal digits, prefixed by a minus sign if the number is negative. The
    // precision specifier indicates the minimum number of digits desired in the
    // resulting string. If required, the number will be left-padded with zeros to
    // produce the number of digits given by the precision specifier.
    //
    // E e Engineering (scientific) format.
    // The number is converted to a string of the form
    // "-d.ddd...E+ddd" or "-d.ddd...e+ddd", where each
    // 'd' indicates a digit (0-9). The string starts with a minus sign if the
    // number is negative, and one digit always precedes the decimal point. The
    // precision specifier indicates the desired number of digits after the decimal
    // point. If the precision specifier is omitted, a default of 6 digits after
    // the decimal point is used. The format specifier indicates whether to prefix
    // the exponent with an 'E' or an 'e'. The exponent is always consists of a
    // plus or minus sign and three digits.
    //
    // F f Fixed point format. The number is
    // converted to a string of the form "-ddd.ddd....", where each
    // 'd' indicates a digit (0-9). The string starts with a minus sign if the
    // number is negative. The precision specifier indicates the desired number of
    // decimal places. If the precision specifier is omitted, the default numeric
    // precision given by the NumberFormatInfo is used.
    //
    // G g - General format. The number is
    // converted to the shortest possible decimal representation using fixed point
    // or scientific format. The precision specifier determines the number of
    // significant digits in the resulting string. If the precision specifier is
    // omitted, the number of significant digits is determined by the type of the
    // number being converted (10 for int, 19 for long, 7 for
    // float, 15 for double, 19 for Currency, and 29 for
    // Decimal). Trailing zeros after the decimal point are removed, and the
    // resulting string contains a decimal point only if required. The resulting
    // string uses fixed point format if the exponent of the number is less than
    // the number of significant digits and greater than or equal to -4. Otherwise,
    // the resulting string uses scientific format, and the case of the format
    // specifier controls whether the exponent is prefixed with an 'E' or an 'e'.
    //
    // N n Number format. The number is
    // converted to a string of the form "-d,ddd,ddd.ddd....", where
    // each 'd' indicates a digit (0-9). The string starts with a minus sign if the
    // number is negative. Thousand separators are inserted between each group of
    // three digits to the left of the decimal point. The precision specifier
    // indicates the desired number of decimal places. If the precision specifier
    // is omitted, the default numeric precision given by the
    // NumberFormatInfo is used.
    //
    // X x - Hexadecimal format. This format is
    // supported for integral types only. The number is converted to a string of
    // hexadecimal digits. The format specifier indicates whether to use upper or
    // lower case characters for the hexadecimal digits above 9 ('X' for 'ABCDEF',
    // and 'x' for 'abcdef'). The precision specifier indicates the minimum number
    // of digits desired in the resulting string. If required, the number will be
    // left-padded with zeros to produce the number of digits given by the
    // precision specifier.
    //
    // B b - Binary format. This format is
    // supported for integral types only. The number is converted to a string of
    // binary digits, '0' or '1'. The precision specifier indicates the minimum number
    // of digits desired in the resulting string. If required, the number will be
    // left-padded with zeros to produce the number of digits given by the
    // precision specifier.
    //
    // Some examples of standard format strings and their results are shown in the
    // table below. (The examples all assume a default NumberFormatInfo.)
    //
    // Value        Format  Result
    // 12345.6789   C       $12,345.68
    // -12345.6789  C       ($12,345.68)
    // 12345        D       12345
    // 12345        D8      00012345
    // 12345.6789   E       1.234568E+004
    // 12345.6789   E10     1.2345678900E+004
    // 12345.6789   e4      1.2346e+004
    // 12345.6789   F       12345.68
    // 12345.6789   F0      12346
    // 12345.6789   F6      12345.678900
    // 12345.6789   G       12345.6789
    // 12345.6789   G7      12345.68
    // 123456789    G7      1.234568E8
    // 12345.6789   N       12,345.68
    // 123456789    N4      123,456,789.0000
    // 0x2c45e      x       2c45e
    // 0x2c45e      X       2C45E
    // 0x2c45e      X8      0002C45E
    //
    // Format strings that do not start with an alphabetic character, or that start
    // with an alphabetic character followed by a non-digit, are called
    // user-defined format strings. The following table describes the formatting
    // characters that are supported in user defined format strings.
    //
    //
    // 0 - Digit placeholder. If the value being
    // formatted has a digit in the position where the '0' appears in the format
    // string, then that digit is copied to the output string. Otherwise, a '0' is
    // stored in that position in the output string. The position of the leftmost
    // '0' before the decimal point and the rightmost '0' after the decimal point
    // determines the range of digits that are always present in the output
    // string.
    //
    // # - Digit placeholder. If the value being
    // formatted has a digit in the position where the '#' appears in the format
    // string, then that digit is copied to the output string. Otherwise, nothing
    // is stored in that position in the output string.
    //
    // . - Decimal point. The first '.' character
    // in the format string determines the location of the decimal separator in the
    // formatted value; any additional '.' characters are ignored. The actual
    // character used as a the decimal separator in the output string is given by
    // the NumberFormatInfo used to format the number.
    //
    // , - Thousand separator and number scaling.
    // The ',' character serves two purposes. First, if the format string contains
    // a ',' character between two digit placeholders (0 or #) and to the left of
    // the decimal point if one is present, then the output will have thousand
    // separators inserted between each group of three digits to the left of the
    // decimal separator. The actual character used as a the decimal separator in
    // the output string is given by the NumberFormatInfo used to format the
    // number. Second, if the format string contains one or more ',' characters
    // immediately to the left of the decimal point, or after the last digit
    // placeholder if there is no decimal point, then the number will be divided by
    // 1000 times the number of ',' characters before it is formatted. For example,
    // the format string '0,,' will represent 100 million as just 100. Use of the
    // ',' character to indicate scaling does not also cause the formatted number
    // to have thousand separators. Thus, to scale a number by 1 million and insert
    // thousand separators you would use the format string '#,##0,,'.
    //
    // % - Percentage placeholder. The presence of
    // a '%' character in the format string causes the number to be multiplied by
    // 100 before it is formatted. The '%' character itself is inserted in the
    // output string where it appears in the format string.
    //
    // E+ E- e+ e-   - Scientific notation.
    // If any of the strings 'E+', 'E-', 'e+', or 'e-' are present in the format
    // string and are immediately followed by at least one '0' character, then the
    // number is formatted using scientific notation with an 'E' or 'e' inserted
    // between the number and the exponent. The number of '0' characters following
    // the scientific notation indicator determines the minimum number of digits to
    // output for the exponent. The 'E+' and 'e+' formats indicate that a sign
    // character (plus or minus) should always precede the exponent. The 'E-' and
    // 'e-' formats indicate that a sign character should only precede negative
    // exponents.
    //
    // \ - Literal character. A backslash character
    // causes the next character in the format string to be copied to the output
    // string as-is. The backslash itself isn't copied, so to place a backslash
    // character in the output string, use two backslashes (\\) in the format
    // string.
    //
    // 'ABC' "ABC" - Literal string. Characters
    // enclosed in single or double quotation marks are copied to the output string
    // as-is and do not affect formatting.
    //
    // ; - Section separator. The ';' character is
    // used to separate sections for positive, negative, and zero numbers in the
    // format string.
    //
    // Other - All other characters are copied to
    // the output string in the position they appear.
    //
    // For fixed point formats (formats not containing an 'E+', 'E-', 'e+', or
    // 'e-'), the number is rounded to as many decimal places as there are digit
    // placeholders to the right of the decimal point. If the format string does
    // not contain a decimal point, the number is rounded to the nearest
    // integer. If the number has more digits than there are digit placeholders to
    // the left of the decimal point, the extra digits are copied to the output
    // string immediately before the first digit placeholder.
    //
    // For scientific formats, the number is rounded to as many significant digits
    // as there are digit placeholders in the format string.
    //
    // To allow for different formatting of positive, negative, and zero values, a
    // user-defined format string may contain up to three sections separated by
    // semicolons. The results of having one, two, or three sections in the format
    // string are described in the table below.
    //
    // Sections:
    //
    // One - The format string applies to all values.
    //
    // Two - The first section applies to positive values
    // and zeros, and the second section applies to negative values. If the number
    // to be formatted is negative, but becomes zero after rounding according to
    // the format in the second section, then the resulting zero is formatted
    // according to the first section.
    //
    // Three - The first section applies to positive
    // values, the second section applies to negative values, and the third section
    // applies to zeros. The second section may be left empty (by having no
    // characters between the semicolons), in which case the first section applies
    // to all non-zero values. If the number to be formatted is non-zero, but
    // becomes zero after rounding according to the format in the first or second
    // section, then the resulting zero is formatted according to the third
    // section.
    //
    // For both standard and user-defined formatting operations on values of type
    // float and double, if the value being formatted is a NaN (Not
    // a Number) or a positive or negative infinity, then regardless of the format
    // string, the resulting string is given by the NaNSymbol,
    // PositiveInfinitySymbol, or NegativeInfinitySymbol property of
    // the NumberFormatInfo used to format the number.

    internal static partial class Number
    {
        internal const int DecimalPrecision = 29; // Decimal.DecCalc also uses this value

        /// <summary>The non-inclusive upper bound of <see cref="SmallNumberCache.Value"/>.</summary>
        /// <remarks>
        /// This is a semi-arbitrary bound. For mono, which is often used for more size-constrained workloads,
        /// we keep the size really small, supporting only single digit values.  For coreclr, we use a larger
        /// value, still relatively small but large enough to accommodate common sources of numbers to strings, e.g. HTTP success status codes.
        /// By being >= 255, it also accommodates all byte.ToString()s.  If no small numbers are ever formatted, we incur
        /// the ~2400 bytes on 64-bit for the array itself.  If all small numbers are formatted, we incur ~11,500 bytes
        /// on 64-bit for the array and all the strings.
        /// </remarks>
        private const int SmallNumberCacheLength =
#if MONO
            10;
#else
            300;
#endif
        private static class SmallNumberCache
        {
            /// <summary>Lazily-populated cache of strings for uint values in the range [0, <see cref="SmallNumberCacheLength"/>).</summary>
            internal static readonly string?[] Value = new string[SmallNumberCacheLength];
        }

        // Keep the pair's alignment equal to char so every char span can be safely reinterpreted.
        [StructLayout(LayoutKind.Sequential, Pack = sizeof(char))]
        private readonly struct DigitPair
        {
            public readonly uint Value;

            public DigitPair(uint value) => Value = value;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static Span<char> GetFreshStringSpan(string result)
        {
            // The string has its definitive length and does not become observable until every character is initialized.
            return new Span<char>(ref result.GetRawStringData(), result.Length);
        }

        internal static string FormatDecimalIeee754<TDecimal, TValue>(TValue value, string? format, NumberFormatInfo info)
            where TDecimal : unmanaged, IDecimalIeee754ParseAndFormatInfo<TDecimal, TValue>
            where TValue : unmanaged, IBinaryInteger<TValue>
        {
            var vlb = new ValueListBuilder<char>(stackalloc char[CharStackBufferSize]);
            NumberBuffer number = new NumberBuffer(NumberBufferKind.DecimalIeee754, stackalloc byte[TDecimal.BufferLength]);
            string result = FormatDecimalIeee754<TDecimal, TValue, char>(ref vlb, ref number, value, format, info) ?? vlb.AsSpan().ToString();
            vlb.Dispose();
            return result;
        }

        // The number buffer is created by the caller so that it shares a scope with the value list builder;
        // otherwise passing it on to the formatting helpers is a ref-safety error now that Number is not unsafe.
        private static string? FormatDecimalIeee754<TDecimal, TValue, TChar>(ref ValueListBuilder<TChar> vlb, ref NumberBuffer number, TValue value, ReadOnlySpan<char> format, NumberFormatInfo info)
            where TDecimal : unmanaged, IDecimalIeee754ParseAndFormatInfo<TDecimal, TValue>
            where TValue : unmanaged, IBinaryInteger<TValue>
            where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));

            if (!TDecimal.IsFinite(value))
            {
                if (TDecimal.IsNaN(value))
                {
                    if (typeof(TChar) == typeof(char))
                    {
                        return info.NaNSymbol;
                    }
                    else
                    {
                        vlb.Append(info.NaNSymbolTChar<TChar>());
                        return null;
                    }
                }

                if (typeof(TChar) == typeof(char))
                {
                    return TDecimal.IsNegative(value) ? info.NegativeInfinitySymbol : info.PositiveInfinitySymbol;
                }
                else
                {
                    vlb.Append(TDecimal.IsNegative(value) ? info.NegativeInfinitySymbolTChar<TChar>() : info.PositiveInfinitySymbolTChar<TChar>());
                    return null;
                }
            }
            char fmt = ParseFormatSpecifier(format, out int digits);

            DecimalIeee754ToNumber<TDecimal, TValue>(value, ref number);

            if (fmt != 0)
            {
                if (fmt is 'G' or 'R' or 'g' or 'r')
                {
                    if (fmt is 'R' or 'r')
                    {
                        // The roundtrip specifier ignores any precision specifier and is otherwise identical to the general specifier
                        fmt = (char)(fmt - ('R' - 'G'));
                        digits = -1;
                    }

                    FormatGeneralAndRoundTripDecimalIeee754(ref vlb, ref number, (char)(fmt - ('G' - 'E')), digits, info);
                }
                else
                {
                    NumberToString(ref vlb, ref number, fmt, digits, info);
                }
            }
            else
            {
                NumberToStringFormat(ref vlb, ref number, format, info);
            }

            return null;
        }

        internal static bool TryFormatDecimalIeee754<TDecimal, TValue, TChar>(TValue value, ReadOnlySpan<char> format, NumberFormatInfo info, Span<TChar> destination, out int charsWritten)
            where TDecimal : unmanaged, IDecimalIeee754ParseAndFormatInfo<TDecimal, TValue>
            where TValue : unmanaged, IBinaryInteger<TValue>
            where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));

            var vlb = new ValueListBuilder<TChar>(stackalloc TChar[CharStackBufferSize]);
            NumberBuffer number = new NumberBuffer(NumberBufferKind.DecimalIeee754, stackalloc byte[TDecimal.BufferLength]);
            string? s = FormatDecimalIeee754<TDecimal, TValue, TChar>(ref vlb, ref number, value, format, info);

            Debug.Assert(s is null || typeof(TChar) == typeof(char));
            bool success = s != null ?
                TryCopyTo(s, destination, out charsWritten) :
                vlb.TryCopyTo(destination, out charsWritten);

            vlb.Dispose();
            return success;
        }

        /// <summary>
        /// Formats <paramref name="number"/> using the general format, preserving the quantum exponent so that
        /// reparsing the result recovers the same member of the cohort.
        /// </summary>
        /// <remarks>
        /// Fixed-point notation can only spell a quantum exponent that is at or below zero, since a positive
        /// quantum would require trailing zeros that reparse as a larger coefficient. Scientific notation is
        /// therefore required whenever the quantum exponent is positive, and is otherwise picked using the same
        /// compactness heuristic as the binary floating-point types.
        /// </remarks>
        private static void FormatGeneralAndRoundTripDecimalIeee754<TChar>(ref ValueListBuilder<TChar> vlb, ref NumberBuffer number, char expChar, int nMaxDigits, NumberFormatInfo info)
            where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(number.Kind == NumberBufferKind.DecimalIeee754);

            bool rounded = (nMaxDigits > 0) && (nMaxDigits < number.DigitsCount);

            if (rounded)
            {
                RoundNumber(ref number, nMaxDigits, isCorrectlyRounded: false);
            }

            if (number.IsNegative)
            {
                vlb.Append(info.NegativeSignTChar<TChar>());
            }

            int digitCount = number.DigitsCount;
            ReadOnlySpan<byte> dig = number.Digits.Slice(0, digitCount);

            // `Scale` is the coefficient digit count plus the quantum exponent, so `Scale` exceeding the number
            // of significant digits means the quantum exponent is positive. Rounding drops trailing coefficient
            // digits without touching `Scale`, so the requested precision is what remains significant in that
            // case; the dropped digits are recovered as trailing zeros below.
            int significantDigits = rounded ? nMaxDigits : digitCount;

            // A zero coefficient has no stored digits but still participates as the single digit `0` when
            // computing the adjusted exponent.
            int adjustedExponent = (digitCount != 0) ? (number.Scale - 1) : number.Scale;

            if ((number.Scale > significantDigits) || (adjustedExponent < -4))
            {
                vlb.Append(TChar.CastFrom((digitCount != 0) ? (char)dig[0] : '0'));

                if (digitCount > 1)
                {
                    vlb.Append(info.NumberDecimalSeparatorTChar<TChar>());

                    for (int i = 1; i < digitCount; i++)
                    {
                        vlb.Append(TChar.CastFrom((char)dig[i]));
                    }
                }

                FormatExponent(ref vlb, info, adjustedExponent, expChar, minDigits: 2, positiveSign: true);
                return;
            }

            int integerDigits = number.Scale;

            if (integerDigits > 0)
            {
                for (int i = 0; i < integerDigits; i++)
                {
                    // Rounding can leave fewer digits than the scale requires, in which case the remaining
                    // integer positions are trailing zeros of the rounded coefficient.
                    vlb.Append(TChar.CastFrom((i < digitCount) ? (char)dig[i] : '0'));
                }
            }
            else
            {
                vlb.Append(TChar.CastFrom('0'));
            }

            if (integerDigits < digitCount)
            {
                vlb.Append(info.NumberDecimalSeparatorTChar<TChar>());

                for (int i = integerDigits; i < 0; i++)
                {
                    vlb.Append(TChar.CastFrom('0'));
                }

                for (int i = Math.Max(integerDigits, 0); i < digitCount; i++)
                {
                    vlb.Append(TChar.CastFrom((char)dig[i]));
                }
            }
        }

        public static string FormatDecimal(decimal value, ReadOnlySpan<char> format, NumberFormatInfo info)
        {
            char fmt = ParseFormatSpecifier(format, out int digits);

            NumberBuffer number = new NumberBuffer(NumberBufferKind.Decimal, stackalloc byte[DecimalNumberBufferLength]);

            DecimalToNumber(ref value, ref number);

            var vlb = new ValueListBuilder<char>(stackalloc char[CharStackBufferSize]);

            if (fmt != 0)
            {
                NumberToString(ref vlb, ref number, fmt, digits, info);
            }
            else
            {
                NumberToStringFormat(ref vlb, ref number, format, info);
            }

            string result = vlb.AsSpan().ToString();
            vlb.Dispose();
            return result;
        }

        public static bool TryFormatDecimal<TChar>(decimal value, ReadOnlySpan<char> format, NumberFormatInfo info, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));

            char fmt = ParseFormatSpecifier(format, out int digits);

            NumberBuffer number = new NumberBuffer(NumberBufferKind.Decimal, stackalloc byte[DecimalNumberBufferLength]);

            DecimalToNumber(ref value, ref number);

            var vlb = new ValueListBuilder<TChar>(stackalloc TChar[CharStackBufferSize]);

            if (fmt != 0)
            {
                NumberToString(ref vlb, ref number, fmt, digits, info);
            }
            else
            {
                NumberToStringFormat(ref vlb, ref number, format, info);
            }

            bool success = vlb.TryCopyTo(destination, out charsWritten);
            vlb.Dispose();
            return success;
        }

        internal static void DecimalIeee754ToNumber<TDecimal, TValue>(TValue value, ref NumberBuffer number)
            where TDecimal : unmanaged, IDecimalIeee754ParseAndFormatInfo<TDecimal, TValue>
            where TValue : unmanaged, IBinaryInteger<TValue>
        {
            DecodedDecimalIeee754<TValue> unpackDecimal = Number.UnpackDecimalIeee754<TDecimal, TValue>(value);
            number.IsNegative = unpackDecimal.Signed;

            if (TValue.IsZero(unpackDecimal.Significand))
            {
                // A zero coefficient has no stored digits, so `Scale` carries the quantum exponent directly.
                // Every other format specifier calls `RoundNumber` (or resets `Scale` itself) before reading it.
                number.Scale = unpackDecimal.UnbiasedExponent;
                number.DigitsCount = 0;
                number.Digits[0] = (byte)'\0';
                number.CheckConsistency();
                return;
            }

            string significand = TDecimal.ToDecStr(unpackDecimal.Significand);

            Debug.Assert(significand.Length < TDecimal.BufferLength);

            for (int i = 0; i < significand.Length; i++)
            {
                number.Digits[i] = (byte)significand[i];
            }

            number.Scale = significand.Length + unpackDecimal.UnbiasedExponent;
            number.DigitsCount = significand.Length;
            number.Digits[significand.Length] = (byte)'\0';

            number.CheckConsistency();
        }

        internal static void DecimalToNumber(scoped ref decimal d, ref NumberBuffer number)
        {
            number.IsNegative = decimal.IsNegative(d);

            // Pre-compute the exact digit count from the 96-bit integer value so we can write
            // directly into digits[0..i) without a subsequent shift.
            UInt128 absValue = new UInt128((uint)d.High, ((ulong)(uint)d.Mid << 32) | (uint)d.Low);
            int i = absValue != UInt128.Zero ? FormattingHelpers.CountDigits(absValue) : 0;
            int scale = d.Scale; // capture before DecDivMod1E9 mutates d (it doesn't touch scale, but be explicit)

            number.DigitsCount = i;
            number.Scale = i - scale;

            Span<byte> digits = number.Digits;
            int index = i;
            while ((d.Mid | d.High) != 0)
            {
                index = UInt32ToDecChars(digits, index, decimal.DecDivMod1E9(ref d), 9);
            }
            UInt32ToDecChars(digits, index, d.Low, 0);

            digits[i] = (byte)'\0';
            number.CheckConsistency();
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static int GetFloatingPointMaxDigitsAndPrecision(char fmt, ref int precision, NumberFormatInfo info, out bool isSignificantDigits)
        {
            // We want to fast path the common case of no format and general format + precision.
            // These are commonly encountered and the full switch is otherwise large enough to show up in hot path profiles

            if (fmt == 0)
            {
                isSignificantDigits = true;
                return precision;
            }

            // Bitwise-or with space (' ') converts any uppercase character to
            // lowercase and keeps unsupported characters as something unsupported.
            fmt |= ' ';

            if (fmt == 'g')
            {
                // The general format uses the precision specifier to indicate the number of significant
                // digits to format. This defaults to the shortest roundtrippable string. Additionally,
                // given that we can't return zero significant digits, we treat 0 as returning the shortest
                // roundtrippable string as well.

                isSignificantDigits = true;

                if (precision == 0)
                {
                    precision = -1;
                    return 0;
                }
                return precision;
            }

            return Slow(fmt, ref precision, info, out isSignificantDigits);

            static int Slow(char fmt, ref int precision, NumberFormatInfo info, out bool isSignificantDigits)
            {
                int maxDigits = precision;

                switch (fmt)
                {
                    case 'c':
                    {
                        // The currency format uses the precision specifier to indicate the number of
                        // decimal digits to format. This defaults to NumberFormatInfo.CurrencyDecimalDigits.

                        if (precision == -1)
                        {
                            precision = info.CurrencyDecimalDigits;
                        }
                        isSignificantDigits = false;

                        break;
                    }

                    case 'e':
                    {
                        // The exponential format uses the precision specifier to indicate the number of
                        // decimal digits to format. This defaults to 6. However, the exponential format
                        // also always formats a single integral digit, so we need to increase the precision
                        // specifier and treat it as the number of significant digits to account for this.

                        if (precision == -1)
                        {
                            precision = DefaultPrecisionExponentialFormat;
                        }

                        precision++;
                        isSignificantDigits = true;

                        break;
                    }

                    case 'f':
                    case 'n':
                    {
                        // The fixed-point and number formats use the precision specifier to indicate the number
                        // of decimal digits to format. This defaults to NumberFormatInfo.NumberDecimalDigits.

                        if (precision == -1)
                        {
                            precision = info.NumberDecimalDigits;
                        }
                        isSignificantDigits = false;

                        break;
                    }

                    case 'p':
                    {
                        // The percent format uses the precision specifier to indicate the number of
                        // decimal digits to format. This defaults to NumberFormatInfo.PercentDecimalDigits.
                        // However, the percent format also always multiplies the number by 100, so we need
                        // to increase the precision specifier to ensure we get the appropriate number of digits.

                        if (precision == -1)
                        {
                            precision = info.PercentDecimalDigits;
                        }

                        precision += 2;
                        isSignificantDigits = false;

                        break;
                    }

                    case 'r':
                    {
                        // The roundtrip format ignores the precision specifier and always returns the shortest
                        // roundtrippable string.

                        precision = -1;
                        isSignificantDigits = true;

                        break;
                    }

                    default:
                    {
                        ThrowHelper.ThrowFormatException_BadFormatSpecifier();
                        goto case 'r'; // unreachable
                    }
                }

                return maxDigits;
            }
        }

        private static void FormatFloatingPointAsHex<TNumber, TChar>(ref ValueListBuilder<TChar> vlb, TNumber value, char fmt, int precision, NumberFormatInfo info)
            where TNumber : unmanaged, IBinaryFloatParseAndFormatInfo<TNumber>
            where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert((fmt | 0x20) == 'x');
            Debug.Assert(TNumber.IsFinite(value));

            bool isNegative = TNumber.IsNegative(value);

            if (isNegative)
            {
                vlb.Append(info.NegativeSignTChar<TChar>());
            }

            vlb.Append(TChar.CastFrom('0'));
            vlb.Append(TChar.CastFrom(fmt));

            ulong fraction = ExtractFractionAndBiasedExponent(value, out int exponent);

            if (fraction == 0)
            {
                // +/- 0
                vlb.Append(TChar.CastFrom('0'));

                if (precision > 0)
                {
                    vlb.Append(info.NumberDecimalSeparatorTChar<TChar>());
                    vlb.AppendSpan(precision).Fill(TChar.CastFrom('0'));
                }

                // Exponent sign is always emitted ('+' or '-'), consistent with the 'E' format.
                vlb.Append(TChar.CastFrom(fmt == 'X' ? 'P' : 'p'));
                vlb.Append(TChar.CastFrom('+'));
                vlb.Append(TChar.CastFrom('0'));

                return;
            }

            // ExtractFractionAndBiasedExponent returns (note: despite the name, the exponent is unbiased):
            //   For normal:   fraction = (1 << DenormalMantissaBits) | mantissa, exponent = biasedExp - ExponentBias - DenormalMantissaBits
            //   For denormal: fraction = mantissa, exponent = MinBinaryExponent - DenormalMantissaBits
            //
            // We want the form: 1.xxxxx * 2^e
            // So we need to normalize so that the leading 1 bit is at bit DenormalMantissaBits.
            // For normal numbers, this is already the case.
            // For denormal numbers, we need to shift left until the leading 1 is there.

            int mantissaBits = TNumber.DenormalMantissaBits;

            if (fraction < (1UL << mantissaBits))
            {
                // Denormal: shift the leading 1 up to the implicit bit position
                int lz = BitOperations.LeadingZeroCount(fraction) - (63 - mantissaBits);
                fraction <<= lz;
                exponent -= lz;
            }

            // Now fraction has the leading 1 at bit [mantissaBits], and the remaining bits below.
            // The unbiased exponent for the value is: exponent + mantissaBits (since fraction is
            // really fraction * 2^exponent, and we want 1.xxx * 2^actualExponent).
            int actualExponent = exponent + mantissaBits;

            // Strip the implicit leading 1 to get the fractional bits
            ulong significandBits = fraction & ((1UL << mantissaBits) - 1);

            // Leading digit is normally '1' for non-zero (the implicit bit)
            int leadingDigit = 1;

            // Determine how many hex digits to emit for the fractional part
            int defaultHexDigits = (mantissaBits + 3) / 4;

            if (precision == 0)
            {
                // Round significandBits into the leading digit
                ulong half = (mantissaBits > 0) ? (1UL << (mantissaBits - 1)) : 0;
                if (significandBits > half || (significandBits == half && (leadingDigit & 1) != 0))
                {
                    leadingDigit++;
                    // leadingDigit can't exceed 2 since it started at 1
                }

                significandBits = 0;
            }

            vlb.Append(TChar.CastFrom((char)('0' + leadingDigit)));

            if (precision > 0)
            {
                ulong shifted;

                if (precision < defaultHexDigits)
                {
                    // Need to round
                    int bitsToKeep = precision * 4;
                    int bitsToDiscard = mantissaBits - bitsToKeep;

                    // bitsToDiscard is always in (0, mantissaBits) here because precision >= 1
                    // (we're in the precision > 0 branch) and precision < defaultHexDigits
                    // (checked above), so bitsToKeep < mantissaBits and bitsToDiscard > 0.
                    // For all IEEE types mantissaBits <= 52, so bitsToDiscard < 64.
                    Debug.Assert(bitsToDiscard > 0 && bitsToDiscard < 64);
                    if (bitsToDiscard > 0 && bitsToDiscard < 64)
                    {
                        ulong roundBit = 1UL << (bitsToDiscard - 1);
                        ulong discardedBits = significandBits & ((1UL << bitsToDiscard) - 1);
                        bool roundUp = discardedBits > roundBit || (discardedBits == roundBit && ((significandBits >> bitsToDiscard) & 1) != 0);

                        if (roundUp)
                        {
                            significandBits = (significandBits >> bitsToDiscard) + 1;

                            // Check if rounding overflowed into leading digit
                            if (significandBits >= (1UL << bitsToKeep))
                            {
                                significandBits = 0;
                                actualExponent++;
                            }
                        }
                        else
                        {
                            significandBits >>= bitsToDiscard;
                        }

                        shifted = significandBits << (64 - bitsToKeep);
                    }
                    else
                    {
                        shifted = significandBits << (64 - mantissaBits);
                    }
                }
                else
                {
                    shifted = significandBits << (64 - mantissaBits);
                }

                vlb.Append(info.NumberDecimalSeparatorTChar<TChar>());

                // Emit real nibbles
                int realDigits = Math.Min(precision, defaultHexDigits);
                for (int i = 0; i < realDigits; i++)
                {
                    vlb.Append(TChar.CastFrom(fmt == 'X' ? HexConverter.ToCharUpper((int)(shifted >> 60)) : HexConverter.ToCharLower((int)(shifted >> 60))));
                    shifted <<= 4;
                }

                // Emit padding zeros (when precision > defaultHexDigits)
                int padCount = precision - realDigits;
                if (padCount > 0)
                {
                    vlb.AppendSpan(padCount).Fill(TChar.CastFrom('0'));
                }
            }
            else if (precision < 0)
            {
                // Default precision: emit significant hex digits, trimming trailing zeros.
                // Compute trailing zero nibbles from the nibble-aligned representation.
                if (significandBits != 0)
                {
                    // Align significand to nibble boundary (pad LSB so total bits = defaultHexDigits * 4),
                    // then count trailing zero nibbles via trailing zero bits.
                    int paddingBits = defaultHexDigits * 4 - mantissaBits;
                    ulong nibbleAligned = significandBits << paddingBits;
                    int trailingZeroBits = BitOperations.TrailingZeroCount(nibbleAligned);
                    int trimmedDigits = defaultHexDigits - (trailingZeroBits / 4);

                    if (trimmedDigits > 0)
                    {
                        vlb.Append(info.NumberDecimalSeparatorTChar<TChar>());

                        ulong shifted = significandBits << (64 - mantissaBits);
                        for (int i = 0; i < trimmedDigits; i++)
                        {
                            vlb.Append(TChar.CastFrom(fmt == 'X' ? HexConverter.ToCharUpper((int)(shifted >> 60)) : HexConverter.ToCharLower((int)(shifted >> 60))));
                            shifted <<= 4;
                        }
                    }
                }
            }

            // Emit exponent: p+NNN or p-NNN
            // The exponent sign is always ASCII '+'/'-' per IEEE 754 §5.12.3,
            // independent of NumberFormatInfo (which only governs the leading value sign).
            vlb.Append(TChar.CastFrom(fmt == 'X' ? 'P' : 'p'));

            if (actualExponent >= 0)
            {
                vlb.Append(TChar.CastFrom('+'));
            }
            else
            {
                vlb.Append(TChar.CastFrom('-'));
                actualExponent = -actualExponent;
            }

            // Write exponent digits
            Debug.Assert(actualExponent >= 0);
            int digitCount = FormattingHelpers.CountDigits((uint)actualExponent);
            Span<TChar> exponentBuffer = vlb.AppendSpan(digitCount);
            int exponentPos = UInt32ToDecChars<TChar>(exponentBuffer, digitCount, (uint)actualExponent);
            Debug.Assert(exponentPos == 0);
        }

        public static string FormatFloat<TNumber>(TNumber value, string? format, NumberFormatInfo info)
            where TNumber : unmanaged, IBinaryFloatParseAndFormatInfo<TNumber>
        {
            var vlb = new ValueListBuilder<char>(stackalloc char[CharStackBufferSize]);
            NumberBuffer number = new NumberBuffer(NumberBufferKind.FloatingPoint, stackalloc byte[TNumber.NumberBufferLength]);
            string result = FormatFloat(ref vlb, ref number, value, format, info) ?? vlb.AsSpan().ToString();
            vlb.Dispose();
            return result;
        }

        public static bool TryFormatFloat<TNumber, TChar>(TNumber value, ReadOnlySpan<char> format, NumberFormatInfo info, Span<TChar> destination, out int charsWritten)
            where TNumber : unmanaged, IBinaryFloatParseAndFormatInfo<TNumber>
            where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));

            var vlb = new ValueListBuilder<TChar>(stackalloc TChar[CharStackBufferSize]);
            NumberBuffer number = new NumberBuffer(NumberBufferKind.FloatingPoint, stackalloc byte[TNumber.NumberBufferLength]);
            string? s = FormatFloat(ref vlb, ref number, value, format, info);

            Debug.Assert(s is null || typeof(TChar) == typeof(char));
            bool success = s != null ?
                TryCopyTo(s, destination, out charsWritten) :
                vlb.TryCopyTo(destination, out charsWritten);

            vlb.Dispose();
            return success;
        }

        /// <summary>Formats the specified value according to the specified format and info.</summary>
        /// <returns>
        /// Non-null if an existing string can be returned, in which case the builder will be unmodified.
        /// Null if no existing string was returned, in which case the formatted output is in the builder.
        /// </returns>
        private static string? FormatFloat<TNumber, TChar>(ref ValueListBuilder<TChar> vlb, ref NumberBuffer number, TNumber value, ReadOnlySpan<char> format, NumberFormatInfo info)
            where TNumber : unmanaged, IBinaryFloatParseAndFormatInfo<TNumber>
            where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));

            if (!TNumber.IsFinite(value))
            {
                if (TNumber.IsNaN(value))
                {
                    if (typeof(TChar) == typeof(char))
                    {
                        return info.NaNSymbol;
                    }
                    else
                    {
                        vlb.Append(info.NaNSymbolTChar<TChar>());
                        return null;
                    }
                }

                if (typeof(TChar) == typeof(char))
                {
                    return TNumber.IsNegative(value) ? info.NegativeInfinitySymbol : info.PositiveInfinitySymbol;
                }
                else
                {
                    vlb.Append(TNumber.IsNegative(value) ? info.NegativeInfinitySymbolTChar<TChar>() : info.PositiveInfinitySymbolTChar<TChar>());
                    return null;
                }
            }

            char fmt = ParseFormatSpecifier(format, out int precision);

            // Handle hex float formatting (X/x format specifier)
            if ((fmt | 0x20) == 'x')
            {
                FormatFloatingPointAsHex(ref vlb, value, fmt, precision, info);
                return null;
            }

            if (fmt == '\0')
            {
                precision = TNumber.MaxPrecisionCustomFormat;
            }

            number.IsNegative = TNumber.IsNegative(value);

            // We need to track the original precision requested since some formats
            // accept values like 0 and others may require additional fixups.
            int nMaxDigits = GetFloatingPointMaxDigitsAndPrecision(fmt, ref precision, info, out bool isSignificantDigits);

            if ((value != default) && (!isSignificantDigits || !Grisu3.TryRun(value, precision, ref number)))
            {
                Dragon4(value, precision, isSignificantDigits, ref number);
            }

            number.CheckConsistency();

            // When the number is known to be roundtrippable (either because we requested it be, or
            // because we know we have enough digits to satisfy roundtrippability), we should validate
            // that the number actually roundtrips back to the original result.
            // This only applies to significant digits; fractional digits may include leading zeros.

            Debug.Assert(!isSignificantDigits || ((precision != -1) && (precision < TNumber.MaxRoundTripDigits)) || (TNumber.FloatToBits(value) == TNumber.FloatToBits(NumberToFloat<TNumber>(ref number))));

            if (fmt != 0)
            {
                if (precision == -1)
                {
                    Debug.Assert((fmt == 'G') || (fmt == 'g') || (fmt == 'R') || (fmt == 'r'));

                    // For the roundtrip and general format specifiers, when returning the shortest roundtrippable
                    // string, we need to update the maximum number of digits to be the greater of number.DigitsCount
                    // or SinglePrecision. This ensures that we continue returning "pretty" strings for values with
                    // less digits. One example this fixes is "-60", which would otherwise be formatted as "-6E+01"
                    // since DigitsCount would be 1 and the formatter would almost immediately switch to scientific notation.

                    nMaxDigits = Math.Max(number.DigitsCount, TNumber.MaxRoundTripDigits);
                }
                NumberToString(ref vlb, ref number, fmt, nMaxDigits, info);
            }
            else
            {
                Debug.Assert(precision == TNumber.MaxPrecisionCustomFormat);
                NumberToStringFormat(ref vlb, ref number, format, info);
            }
            return null;
        }

        private static bool TryCopyTo<TChar>(string source, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));
            Debug.Assert(source != null);

            if (typeof(TChar) == typeof(char))
            {
                if (source.TryCopyTo(Unsafe.BitCast<Span<TChar>, Span<char>>(destination)))
                {
                    charsWritten = source.Length;
                    return true;
                }

                charsWritten = 0;
                return false;
            }

            Debug.Assert(typeof(TChar) == typeof(byte));

            return Encoding.UTF8.TryGetBytes(source, Unsafe.BitCast<Span<TChar>, Span<byte>>(destination), out charsWritten);
        }

        internal static char GetHexBase(char fmt)
        {
            // The fmt-(X-A+10) hack has the effect of dictating whether we produce uppercase or lowercase
            // hex numbers for a-f. 'X' as the fmt code produces uppercase. 'x' as the format code produces lowercase.
            return (char)(fmt - ('X' - 'A' + 10));
        }

        public static string FormatInt32(int value, int hexMask, string? format, IFormatProvider? provider)
        {
            // Fast path for default format
            if (string.IsNullOrEmpty(format))
            {
                return value >= 0 ?
                    UInt32ToDecStr((uint)value) :
                    NegativeInt32ToDecStr(value, digits: -1, NumberFormatInfo.GetInstance(provider).NegativeSign);
            }

            return FormatInt32Slow(value, hexMask, format, provider);

            static string FormatInt32Slow(int value, int hexMask, string? format, IFormatProvider? provider)
            {
                ReadOnlySpan<char> formatSpan = format;
                char fmt = ParseFormatSpecifier(formatSpan, out int digits);
                char fmtUpper = (char)(fmt & 0xFFDF); // ensure fmt is upper-cased for purposes of comparison
                if (fmtUpper == 'G' ? digits < 1 : fmtUpper == 'D')
                {
                    return value >= 0 ?
                        UInt32ToDecStr((uint)value, digits) :
                        NegativeInt32ToDecStr(value, digits, NumberFormatInfo.GetInstance(provider).NegativeSign);
                }
                else if (fmtUpper == 'X')
                {
                    return Int32ToHexStr(value & hexMask, GetHexBase(fmt), digits);
                }
                else if (fmtUpper == 'B')
                {
                    return UInt32ToBinaryStr((uint)(value & hexMask), digits);
                }
                else
                {
                    NumberFormatInfo info = NumberFormatInfo.GetInstance(provider);

                    NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, stackalloc byte[Int32NumberBufferLength]);

                    Int32ToNumber(value, ref number);

                    var vlb = new ValueListBuilder<char>(stackalloc char[CharStackBufferSize]);

                    if (fmt != 0)
                    {
                        NumberToString(ref vlb, ref number, fmt, digits, info);
                    }
                    else
                    {
                        NumberToStringFormat(ref vlb, ref number, formatSpan, info);
                    }

                    string result = vlb.AsSpan().ToString();
                    vlb.Dispose();
                    return result;
                }
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)] // expose to caller's likely-const format to trim away slow path
        public static bool TryFormatInt32<TChar>(int value, int hexMask, ReadOnlySpan<char> format, IFormatProvider? provider, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
        {
            // Fast path for default format
            if (format.Length == 0)
            {
                return value >= 0 ?
                    TryUInt32ToDecStr((uint)value, destination, out charsWritten) :
                    TryNegativeInt32ToDecStr(value, digits: -1, NumberFormatInfo.GetInstance(provider).NegativeSignTChar<TChar>(), destination, out charsWritten);
            }

            return TryFormatInt32Slow(value, hexMask, format, provider, destination, out charsWritten);

            static bool TryFormatInt32Slow(int value, int hexMask, ReadOnlySpan<char> format, IFormatProvider? provider, Span<TChar> destination, out int charsWritten)
            {
                char fmt = ParseFormatSpecifier(format, out int digits);
                char fmtUpper = (char)(fmt & 0xFFDF); // ensure fmt is upper-cased for purposes of comparison
                if (fmtUpper == 'G' ? digits < 1 : fmtUpper == 'D')
                {
                    return value >= 0 ?
                        TryUInt32ToDecStr((uint)value, digits, destination, out charsWritten) :
                        TryNegativeInt32ToDecStr(value, digits, NumberFormatInfo.GetInstance(provider).NegativeSignTChar<TChar>(), destination, out charsWritten);
                }
                else if (fmtUpper == 'X')
                {
                    return TryInt32ToHexStr(value & hexMask, GetHexBase(fmt), digits, destination, out charsWritten);
                }
                else if (fmtUpper == 'B')
                {
                    return TryUInt32ToBinaryStr((uint)(value & hexMask), digits, destination, out charsWritten);
                }
                else
                {
                    NumberFormatInfo info = NumberFormatInfo.GetInstance(provider);

                    NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, stackalloc byte[Int32NumberBufferLength]);

                    Int32ToNumber(value, ref number);

                    var vlb = new ValueListBuilder<TChar>(stackalloc TChar[CharStackBufferSize]);

                    if (fmt != 0)
                    {
                        NumberToString(ref vlb, ref number, fmt, digits, info);
                    }
                    else
                    {
                        NumberToStringFormat(ref vlb, ref number, format, info);
                    }

                    bool success = vlb.TryCopyTo(destination, out charsWritten);
                    vlb.Dispose();
                    return success;
                }
            }
        }

        public static string FormatUInt32(uint value, string? format, IFormatProvider? provider)
        {
            // Fast path for default format
            if (string.IsNullOrEmpty(format))
            {
                return UInt32ToDecStr(value);
            }

            return FormatUInt32Slow(value, format, provider);

            static string FormatUInt32Slow(uint value, string? format, IFormatProvider? provider)
            {
                ReadOnlySpan<char> formatSpan = format;
                char fmt = ParseFormatSpecifier(formatSpan, out int digits);
                char fmtUpper = (char)(fmt & 0xFFDF); // ensure fmt is upper-cased for purposes of comparison
                if (fmtUpper == 'G' ? digits < 1 : fmtUpper == 'D')
                {
                    return UInt32ToDecStr(value, digits);
                }
                else if (fmtUpper == 'X')
                {
                    return Int32ToHexStr((int)value, GetHexBase(fmt), digits);
                }
                else if (fmtUpper == 'B')
                {
                    return UInt32ToBinaryStr(value, digits);
                }
                else
                {
                    NumberFormatInfo info = NumberFormatInfo.GetInstance(provider);

                    NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, stackalloc byte[UInt32NumberBufferLength]);

                    UInt32ToNumber(value, ref number);

                    var vlb = new ValueListBuilder<char>(stackalloc char[CharStackBufferSize]);

                    if (fmt != 0)
                    {
                        NumberToString(ref vlb, ref number, fmt, digits, info);
                    }
                    else
                    {
                        NumberToStringFormat(ref vlb, ref number, formatSpan, info);
                    }

                    string result = vlb.AsSpan().ToString();
                    vlb.Dispose();
                    return result;
                }
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)] // expose to caller's likely-const format to trim away slow path
        public static bool TryFormatUInt32<TChar>(uint value, ReadOnlySpan<char> format, IFormatProvider? provider, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));

            // Fast path for default format
            if (format.Length == 0)
            {
                return TryUInt32ToDecStr(value, destination, out charsWritten);
            }

            return TryFormatUInt32Slow(value, format, provider, destination, out charsWritten);

            static bool TryFormatUInt32Slow(uint value, ReadOnlySpan<char> format, IFormatProvider? provider, Span<TChar> destination, out int charsWritten)
            {
                char fmt = ParseFormatSpecifier(format, out int digits);
                char fmtUpper = (char)(fmt & 0xFFDF); // ensure fmt is upper-cased for purposes of comparison
                if (fmtUpper == 'G' ? digits < 1 : fmtUpper == 'D')
                {
                    return TryUInt32ToDecStr(value, digits, destination, out charsWritten);
                }
                else if (fmtUpper == 'X')
                {
                    return TryInt32ToHexStr((int)value, GetHexBase(fmt), digits, destination, out charsWritten);
                }
                else if (fmtUpper == 'B')
                {
                    return TryUInt32ToBinaryStr(value, digits, destination, out charsWritten);
                }
                else
                {
                    NumberFormatInfo info = NumberFormatInfo.GetInstance(provider);

                    NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, stackalloc byte[UInt32NumberBufferLength]);

                    UInt32ToNumber(value, ref number);

                    var vlb = new ValueListBuilder<TChar>(stackalloc TChar[CharStackBufferSize]);

                    if (fmt != 0)
                    {
                        NumberToString(ref vlb, ref number, fmt, digits, info);
                    }
                    else
                    {
                        NumberToStringFormat(ref vlb, ref number, format, info);
                    }

                    bool success = vlb.TryCopyTo(destination, out charsWritten);
                    vlb.Dispose();
                    return success;
                }
            }
        }

        public static string FormatInt64(long value, string? format, IFormatProvider? provider)
        {
            // Fast path for default format
            if (string.IsNullOrEmpty(format))
            {
                return value >= 0 ?
                    UInt64ToDecStr((ulong)value) :
                    NegativeInt64ToDecStr(value, digits: -1, NumberFormatInfo.GetInstance(provider).NegativeSign);
            }

            return FormatInt64Slow(value, format, provider);

            static string FormatInt64Slow(long value, string? format, IFormatProvider? provider)
            {
                ReadOnlySpan<char> formatSpan = format;
                char fmt = ParseFormatSpecifier(formatSpan, out int digits);
                char fmtUpper = (char)(fmt & 0xFFDF); // ensure fmt is upper-cased for purposes of comparison
                if (fmtUpper == 'G' ? digits < 1 : fmtUpper == 'D')
                {
                    return value >= 0 ?
                        UInt64ToDecStr((ulong)value, digits) :
                        NegativeInt64ToDecStr(value, digits, NumberFormatInfo.GetInstance(provider).NegativeSign);
                }
                else if (fmtUpper == 'X')
                {
                    return Int64ToHexStr(value, GetHexBase(fmt), digits);
                }
                else if (fmtUpper == 'B')
                {
                    return UInt64ToBinaryStr((ulong)value, digits);
                }
                else
                {
                    NumberFormatInfo info = NumberFormatInfo.GetInstance(provider);

                    NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, stackalloc byte[Int64NumberBufferLength]);

                    Int64ToNumber(value, ref number);

                    var vlb = new ValueListBuilder<char>(stackalloc char[CharStackBufferSize]);

                    if (fmt != 0)
                    {
                        NumberToString(ref vlb, ref number, fmt, digits, info);
                    }
                    else
                    {
                        NumberToStringFormat(ref vlb, ref number, formatSpan, info);
                    }

                    string result = vlb.AsSpan().ToString();
                    vlb.Dispose();
                    return result;
                }
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)] // expose to caller's likely-const format to trim away slow path
        public static bool TryFormatInt64<TChar>(long value, ReadOnlySpan<char> format, IFormatProvider? provider, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));

            // Fast path for default format
            if (format.Length == 0)
            {
                return value >= 0 ?
                    TryUInt64ToDecStr((ulong)value, destination, out charsWritten) :
                    TryNegativeInt64ToDecStr(value, digits: -1, NumberFormatInfo.GetInstance(provider).NegativeSignTChar<TChar>(), destination, out charsWritten);
            }

            return TryFormatInt64Slow(value, format, provider, destination, out charsWritten);

            static bool TryFormatInt64Slow(long value, ReadOnlySpan<char> format, IFormatProvider? provider, Span<TChar> destination, out int charsWritten)
            {
                char fmt = ParseFormatSpecifier(format, out int digits);
                char fmtUpper = (char)(fmt & 0xFFDF); // ensure fmt is upper-cased for purposes of comparison
                if (fmtUpper == 'G' ? digits < 1 : fmtUpper == 'D')
                {
                    return value >= 0 ?
                        TryUInt64ToDecStr((ulong)value, digits, destination, out charsWritten) :
                        TryNegativeInt64ToDecStr(value, digits, NumberFormatInfo.GetInstance(provider).NegativeSignTChar<TChar>(), destination, out charsWritten);
                }
                else if (fmtUpper == 'X')
                {
                    return TryInt64ToHexStr(value, GetHexBase(fmt), digits, destination, out charsWritten);
                }
                else if (fmtUpper == 'B')
                {
                    return TryUInt64ToBinaryStr((ulong)value, digits, destination, out charsWritten);
                }
                else
                {
                    NumberFormatInfo info = NumberFormatInfo.GetInstance(provider);

                    NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, stackalloc byte[Int64NumberBufferLength]);

                    Int64ToNumber(value, ref number);

                    var vlb = new ValueListBuilder<TChar>(stackalloc TChar[CharStackBufferSize]);

                    if (fmt != 0)
                    {
                        NumberToString(ref vlb, ref number, fmt, digits, info);
                    }
                    else
                    {
                        NumberToStringFormat(ref vlb, ref number, format, info);
                    }

                    bool success = vlb.TryCopyTo(destination, out charsWritten);
                    vlb.Dispose();
                    return success;
                }
            }
        }

        public static string FormatUInt64(ulong value, string? format, IFormatProvider? provider)
        {
            // Fast path for default format
            if (string.IsNullOrEmpty(format))
            {
                return UInt64ToDecStr(value);
            }

            return FormatUInt64Slow(value, format, provider);

            static string FormatUInt64Slow(ulong value, string? format, IFormatProvider? provider)
            {
                ReadOnlySpan<char> formatSpan = format;
                char fmt = ParseFormatSpecifier(formatSpan, out int digits);
                char fmtUpper = (char)(fmt & 0xFFDF); // ensure fmt is upper-cased for purposes of comparison
                if (fmtUpper == 'G' ? digits < 1 : fmtUpper == 'D')
                {
                    return UInt64ToDecStr(value, digits);
                }
                else if (fmtUpper == 'X')
                {
                    return Int64ToHexStr((long)value, GetHexBase(fmt), digits);
                }
                else if (fmtUpper == 'B')
                {
                    return UInt64ToBinaryStr(value, digits);
                }
                else
                {
                    NumberFormatInfo info = NumberFormatInfo.GetInstance(provider);

                    NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, stackalloc byte[UInt64NumberBufferLength]);

                    UInt64ToNumber(value, ref number);

                    var vlb = new ValueListBuilder<char>(stackalloc char[CharStackBufferSize]);

                    if (fmt != 0)
                    {
                        NumberToString(ref vlb, ref number, fmt, digits, info);
                    }
                    else
                    {
                        NumberToStringFormat(ref vlb, ref number, formatSpan, info);
                    }

                    string result = vlb.AsSpan().ToString();
                    vlb.Dispose();
                    return result;
                }
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)] // expose to caller's likely-const format to trim away slow path
        public static bool TryFormatUInt64<TChar>(ulong value, ReadOnlySpan<char> format, IFormatProvider? provider, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));

            // Fast path for default format
            if (format.Length == 0)
            {
                return TryUInt64ToDecStr(value, destination, out charsWritten);
            }

            return TryFormatUInt64Slow(value, format, provider, destination, out charsWritten);

            static bool TryFormatUInt64Slow(ulong value, ReadOnlySpan<char> format, IFormatProvider? provider, Span<TChar> destination, out int charsWritten)
            {
                char fmt = ParseFormatSpecifier(format, out int digits);
                char fmtUpper = (char)(fmt & 0xFFDF); // ensure fmt is upper-cased for purposes of comparison
                if (fmtUpper == 'G' ? digits < 1 : fmtUpper == 'D')
                {
                    return TryUInt64ToDecStr(value, digits, destination, out charsWritten);
                }
                else if (fmtUpper == 'X')
                {
                    return TryInt64ToHexStr((long)value, GetHexBase(fmt), digits, destination, out charsWritten);
                }
                else if (fmtUpper == 'B')
                {
                    return TryUInt64ToBinaryStr(value, digits, destination, out charsWritten);
                }
                else
                {
                    NumberFormatInfo info = NumberFormatInfo.GetInstance(provider);

                    NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, stackalloc byte[UInt64NumberBufferLength]);

                    UInt64ToNumber(value, ref number);

                    var vlb = new ValueListBuilder<TChar>(stackalloc TChar[CharStackBufferSize]);

                    if (fmt != 0)
                    {
                        NumberToString(ref vlb, ref number, fmt, digits, info);
                    }
                    else
                    {
                        NumberToStringFormat(ref vlb, ref number, format, info);
                    }

                    bool success = vlb.TryCopyTo(destination, out charsWritten);
                    vlb.Dispose();
                    return success;
                }
            }
        }

        public static string FormatInt128(Int128 value, string? format, IFormatProvider? provider)
        {
            // Fast path for default format
            if (string.IsNullOrEmpty(format))
            {
                return Int128.IsPositive(value)
                     ? UInt128ToDecStr((UInt128)value, digits: -1)
                     : NegativeInt128ToDecStr(value, digits: -1, NumberFormatInfo.GetInstance(provider).NegativeSign);
            }

            return FormatInt128Slow(value, format, provider);

            static string FormatInt128Slow(Int128 value, string? format, IFormatProvider? provider)
            {
                ReadOnlySpan<char> formatSpan = format;

                char fmt = ParseFormatSpecifier(formatSpan, out int digits);
                char fmtUpper = (char)(fmt & 0xFFDF); // ensure fmt is upper-cased for purposes of comparison

                if (fmtUpper == 'G' ? digits < 1 : fmtUpper == 'D')
                {
                    return Int128.IsPositive(value)
                        ? UInt128ToDecStr((UInt128)value, digits)
                        : NegativeInt128ToDecStr(value, digits, NumberFormatInfo.GetInstance(provider).NegativeSign);
                }
                else if (fmtUpper == 'X')
                {
                    return Int128ToHexStr(value, GetHexBase(fmt), digits);
                }
                else if (fmtUpper == 'B')
                {
                    return UInt128ToBinaryStr(value, digits);
                }
                else
                {
                    NumberFormatInfo info = NumberFormatInfo.GetInstance(provider);

                    NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, stackalloc byte[Int128NumberBufferLength]);

                    Int128ToNumber(value, ref number);

                    var vlb = new ValueListBuilder<char>(stackalloc char[CharStackBufferSize]);

                    if (fmt != 0)
                    {
                        NumberToString(ref vlb, ref number, fmt, digits, info);
                    }
                    else
                    {
                        NumberToStringFormat(ref vlb, ref number, formatSpan, info);
                    }

                    string result = vlb.AsSpan().ToString();
                    vlb.Dispose();
                    return result;
                }
            }
        }

        public static bool TryFormatInt128<TChar>(Int128 value, ReadOnlySpan<char> format, IFormatProvider? provider, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));

            // Fast path for default format
            if (format.Length == 0)
            {
                return Int128.IsPositive(value)
                     ? TryUInt128ToDecStr((UInt128)value, digits: -1, destination, out charsWritten)
                     : TryNegativeInt128ToDecStr(value, digits: -1, NumberFormatInfo.GetInstance(provider).NegativeSignTChar<TChar>(), destination, out charsWritten);
            }

            return TryFormatInt128Slow(value, format, provider, destination, out charsWritten);

            static bool TryFormatInt128Slow(Int128 value, ReadOnlySpan<char> format, IFormatProvider? provider, Span<TChar> destination, out int charsWritten)
            {
                char fmt = ParseFormatSpecifier(format, out int digits);
                char fmtUpper = (char)(fmt & 0xFFDF); // ensure fmt is upper-cased for purposes of comparison

                if (fmtUpper == 'G' ? digits < 1 : fmtUpper == 'D')
                {
                    return Int128.IsPositive(value)
                        ? TryUInt128ToDecStr((UInt128)value, digits, destination, out charsWritten)
                        : TryNegativeInt128ToDecStr(value, digits, NumberFormatInfo.GetInstance(provider).NegativeSignTChar<TChar>(), destination, out charsWritten);
                }
                else if (fmtUpper == 'X')
                {
                    return TryInt128ToHexStr(value, GetHexBase(fmt), digits, destination, out charsWritten);
                }
                else if (fmtUpper == 'B')
                {
                    return TryUInt128ToBinaryStr(value, digits, destination, out charsWritten);
                }
                else
                {
                    NumberFormatInfo info = NumberFormatInfo.GetInstance(provider);

                    NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, stackalloc byte[Int128NumberBufferLength]);

                    Int128ToNumber(value, ref number);

                    var vlb = new ValueListBuilder<TChar>(stackalloc TChar[CharStackBufferSize]);

                    if (fmt != 0)
                    {
                        NumberToString(ref vlb, ref number, fmt, digits, info);
                    }
                    else
                    {
                        NumberToStringFormat(ref vlb, ref number, format, info);
                    }

                    bool success = vlb.TryCopyTo(destination, out charsWritten);
                    vlb.Dispose();
                    return success;
                }
            }
        }

        public static string FormatUInt128(UInt128 value, string? format, IFormatProvider? provider)
        {
            // Fast path for default format
            if (string.IsNullOrEmpty(format))
            {
                return UInt128ToDecStr(value, digits: -1);
            }

            return FormatUInt128Slow(value, format, provider);

            static string FormatUInt128Slow(UInt128 value, string? format, IFormatProvider? provider)
            {
                ReadOnlySpan<char> formatSpan = format;

                char fmt = ParseFormatSpecifier(formatSpan, out int digits);
                char fmtUpper = (char)(fmt & 0xFFDF); // ensure fmt is upper-cased for purposes of comparison

                if (fmtUpper == 'G' ? digits < 1 : fmtUpper == 'D')
                {
                    return UInt128ToDecStr(value, digits);
                }
                else if (fmtUpper == 'X')
                {
                    return Int128ToHexStr((Int128)value, GetHexBase(fmt), digits);
                }
                else if (fmtUpper == 'B')
                {
                    return UInt128ToBinaryStr((Int128)value, digits);
                }
                else
                {
                    NumberFormatInfo info = NumberFormatInfo.GetInstance(provider);

                    NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, stackalloc byte[UInt128NumberBufferLength]);

                    UInt128ToNumber(value, ref number);

                    var vlb = new ValueListBuilder<char>(stackalloc char[CharStackBufferSize]);

                    if (fmt != 0)
                    {
                        NumberToString(ref vlb, ref number, fmt, digits, info);
                    }
                    else
                    {
                        NumberToStringFormat(ref vlb, ref number, formatSpan, info);
                    }

                    string result = vlb.AsSpan().ToString();
                    vlb.Dispose();
                    return result;
                }
            }
        }

        public static bool TryFormatUInt128<TChar>(UInt128 value, ReadOnlySpan<char> format, IFormatProvider? provider, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));

            // Fast path for default format
            if (format.Length == 0)
            {
                return TryUInt128ToDecStr(value, digits: -1, destination, out charsWritten);
            }

            return TryFormatUInt128Slow(value, format, provider, destination, out charsWritten);

            static bool TryFormatUInt128Slow(UInt128 value, ReadOnlySpan<char> format, IFormatProvider? provider, Span<TChar> destination, out int charsWritten)
            {
                char fmt = ParseFormatSpecifier(format, out int digits);
                char fmtUpper = (char)(fmt & 0xFFDF); // ensure fmt is upper-cased for purposes of comparison

                if (fmtUpper == 'G' ? digits < 1 : fmtUpper == 'D')
                {
                    return TryUInt128ToDecStr(value, digits, destination, out charsWritten);
                }
                else if (fmtUpper == 'X')
                {
                    return TryInt128ToHexStr((Int128)value, GetHexBase(fmt), digits, destination, out charsWritten);
                }
                else if (fmtUpper == 'B')
                {
                    return TryUInt128ToBinaryStr((Int128)value, digits, destination, out charsWritten);
                }
                else
                {
                    NumberFormatInfo info = NumberFormatInfo.GetInstance(provider);

                    NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, stackalloc byte[UInt128NumberBufferLength]);

                    UInt128ToNumber(value, ref number);

                    var vlb = new ValueListBuilder<TChar>(stackalloc TChar[CharStackBufferSize]);

                    if (fmt != 0)
                    {
                        NumberToString(ref vlb, ref number, fmt, digits, info);
                    }
                    else
                    {
                        NumberToStringFormat(ref vlb, ref number, format, info);
                    }

                    bool success = vlb.TryCopyTo(destination, out charsWritten);
                    vlb.Dispose();
                    return success;
                }
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static void Int32ToNumber(int value, ref NumberBuffer number)
        {
            if (value >= 0)
            {
                number.IsNegative = false;
            }
            else
            {
                number.IsNegative = true;
                value = -value;
            }

            // Pre-compute the exact digit count so we can write directly into digits[0..i) — no shift.
            int i = value != 0 ? FormattingHelpers.CountDigits((uint)value) : 0;
            number.DigitsCount = i;
            number.Scale = i;

            Span<byte> digits = number.Digits;
            UInt32ToDecChars(digits, i, (uint)value, 0);
            digits[i] = (byte)'\0';

            number.CheckConsistency();
        }

        public static string Int32ToDecStr(int value) =>
            value >= 0 ?
                UInt32ToDecStr((uint)value) :
                NegativeInt32ToDecStr(value, -1, NumberFormatInfo.CurrentInfo.NegativeSign);

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static void UInt32ToDecChars(uint value, Span<char> buffer)
        {
            Debug.Assert(!buffer.IsEmpty);

            int leadingDigits = 2 - (buffer.Length & 1);
            Span<DigitPair> pairs = MemoryMarshal.Cast<char, DigitPair>(buffer.Slice(leadingDigits));

            for (int i = pairs.Length - 1; (uint)i < (uint)pairs.Length; i--)
            {
                (value, uint remainder) = Math.DivRem(value, 100);
                pairs[i] = new DigitPair(GetTwoDigitsChars(remainder));
            }

            if (leadingDigits == 1)
            {
                Debug.Assert(value < 10);
                buffer[0] = (char)(value + '0');
            }
            else
            {
                Debug.Assert(value < 100);
                WriteTwoDigits(value, buffer.Slice(0, 2));
            }
        }

        private static string NegativeInt32ToDecStr(int value, int digits, string sNegative)
        {
            Debug.Assert(value < 0);

            if (digits < 1)
            {
                digits = 1;
            }

            int bufferLength = Math.Max(digits, FormattingHelpers.CountDigits((uint)(-value))) + sNegative.Length;
            string result = string.FastAllocateString(bufferLength);
            Span<char> buffer = GetFreshStringSpan(result);
            UInt32ToDecChars((uint)(-value), buffer.Slice(sNegative.Length));
            CopyNegativeSign(sNegative, buffer);
            return result;
        }

        internal static bool TryNegativeInt32ToDecStr<TChar>(int value, int digits, ReadOnlySpan<TChar> sNegative, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));
            Debug.Assert(value < 0);

            if (digits < 1)
            {
                digits = 1;
            }

            int bufferLength = Math.Max(digits, FormattingHelpers.CountDigits((uint)(-value))) + sNegative.Length;
            if (bufferLength > destination.Length)
            {
                charsWritten = 0;
                return false;
            }

            charsWritten = bufferLength;
            int pos = UInt32ToDecChars<TChar>(destination, bufferLength, (uint)(-value), digits);
            Debug.Assert(pos == sNegative.Length);
            CopyNegativeSign(sNegative, destination);
            return true;
        }

        private static string Int32ToHexStr(int value, char hexBase, int digits)
        {
            if (digits < 1)
            {
                digits = 1;
            }

            int bufferLength = Math.Max(digits, FormattingHelpers.CountHexDigits((uint)value));
            string result = string.FastAllocateString(bufferLength);
            Span<char> buffer = GetFreshStringSpan(result);
            Int32ToHexChars(buffer, (uint)value, hexBase);
            return result;
        }

        internal static bool TryInt32ToHexStr<TChar>(int value, char hexBase, int digits, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));

            if (digits < 1)
            {
                digits = 1;
            }

            int bufferLength = Math.Max(digits, FormattingHelpers.CountHexDigits((uint)value));
            if (bufferLength > destination.Length)
            {
                charsWritten = 0;
                return false;
            }

            charsWritten = bufferLength;
            Int32ToHexChars(destination.Slice(0, bufferLength), (uint)value, hexBase);
            return true;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static void Int32ToHexChars<TChar>(Span<TChar> buffer, uint value, int hexBase) where TChar : unmanaged, IUtfChar<TChar>
        {
            for (int i = buffer.Length - 1; (uint)i < (uint)buffer.Length; i--)
            {
                byte digit = (byte)(value & 0xF);
                buffer[i] = TChar.CastFrom(digit + (digit < 10 ? (byte)'0' : hexBase));
                value >>= 4;
            }

            Debug.Assert(value == 0);
        }

        private static string UInt32ToBinaryStr(uint value, int digits)
        {
            if (digits < 1)
            {
                digits = 1;
            }

            int bufferLength = Math.Max(digits, 32 - (int)uint.LeadingZeroCount(value));
            string result = string.FastAllocateString(bufferLength);
            Span<char> buffer = GetFreshStringSpan(result);
            UInt32ToBinaryChars(buffer, value);
            return result;
        }

        private static bool TryUInt32ToBinaryStr<TChar>(uint value, int digits, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));

            if (digits < 1)
            {
                digits = 1;
            }

            int bufferLength = Math.Max(digits, 32 - (int)uint.LeadingZeroCount(value));
            if (bufferLength > destination.Length)
            {
                charsWritten = 0;
                return false;
            }

            charsWritten = bufferLength;
            UInt32ToBinaryChars(destination.Slice(0, bufferLength), value);
            return true;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static void UInt32ToBinaryChars<TChar>(Span<TChar> buffer, uint value) where TChar : unmanaged, IUtfChar<TChar>
        {
            for (int i = buffer.Length - 1; (uint)i < (uint)buffer.Length; i--)
            {
                buffer[i] = TChar.CastFrom('0' + (byte)(value & 0x1));
                value >>= 1;
            }

            Debug.Assert(value == 0);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void UInt32ToNumber(uint value, ref NumberBuffer number)
        {
            number.IsNegative = false;

            int i = value != 0 ? FormattingHelpers.CountDigits(value) : 0;
            number.DigitsCount = i;
            number.Scale = i;

            Span<byte> digits = number.Digits;
            UInt32ToDecChars(digits, i, value, 0);
            digits[i] = (byte)'\0';

            number.CheckConsistency();
        }


        /// <summary>
        /// Writes a value [ 0000 .. 9999 ] to the start of a pre-sliced 4-element span.
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void WriteFourDigits<TChar>(uint value, Span<TChar> destination) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(destination.Length >= 4);
            (value, uint remainder) = Math.DivRem(value, 100);
            WriteTwoDigits(value, destination.Slice(0, 2));
            WriteTwoDigits(remainder, destination.Slice(2, 2));
        }

        /// <summary>Writes exactly <c>destination.Length</c> digits for <paramref name="value"/> into <paramref name="destination"/>.</summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void WriteDigits<TChar>(uint value, Span<TChar> destination) where TChar : unmanaged, IUtfChar<TChar>
        {
            int cur = destination.Length - 1;
            while (cur > 0)
            {
                uint temp = '0' + value;
                value /= 10;
                destination[cur--] = TChar.CastFrom(temp - value * 10);
            }
            Debug.Assert(value < 10);
            destination[0] = TChar.CastFrom('0' + value);
        }

        internal static string UInt32ToDecStr(uint value)
        {
            // For small numbers, consult a lazily-populated cache.
            if (value < SmallNumberCacheLength)
            {
                return UInt32ToDecStrForKnownSmallNumber(value);
            }

            return UInt32ToDecStr_NoSmallNumberCheck(value);
        }

        internal static string UInt32ToDecStrForKnownSmallNumber(uint value)
        {
            Debug.Assert(value < SmallNumberCacheLength);
            return SmallNumberCache.Value[value] ?? CreateAndCacheString(value);

            [MethodImpl(MethodImplOptions.NoInlining)] // keep rare usage out of fast path
            static string CreateAndCacheString(uint value) =>
                SmallNumberCache.Value[value] = UInt32ToDecStr_NoSmallNumberCheck(value);
        }

        private static string UInt32ToDecStr_NoSmallNumberCheck(uint value)
        {
            int bufferLength = FormattingHelpers.CountDigits(value);
            string result = string.FastAllocateString(bufferLength);
            Span<char> buffer = GetFreshStringSpan(result);
            UInt32ToDecChars(value, buffer);
            return result;
        }

        private static string UInt32ToDecStr(uint value, int digits)
        {
            if (digits <= 1)
                return UInt32ToDecStr(value);

            int bufferLength = Math.Max(digits, FormattingHelpers.CountDigits(value));
            string result = string.FastAllocateString(bufferLength);
            Span<char> buffer = GetFreshStringSpan(result);
            UInt32ToDecChars(value, buffer);
            return result;
        }

        internal static bool TryUInt32ToDecStr<TChar>(uint value, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));

            int bufferLength = FormattingHelpers.CountDigits(value);
            if (bufferLength <= destination.Length)
            {
                charsWritten = bufferLength;
                int pos = UInt32ToDecChars<TChar>(destination, bufferLength, value);
                Debug.Assert(pos == 0);
                return true;
            }

            charsWritten = 0;
            return false;
        }

        internal static bool TryUInt32ToDecStr<TChar>(uint value, int digits, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));

            int countedDigits = FormattingHelpers.CountDigits(value);
            int bufferLength = Math.Max(digits, countedDigits);
            if (bufferLength <= destination.Length)
            {
                charsWritten = bufferLength;
                int pos = digits > countedDigits ?
                    UInt32ToDecChars<TChar>(destination, bufferLength, value, digits) :
                    UInt32ToDecChars<TChar>(destination, bufferLength, value);
                Debug.Assert(pos == 0);
                return true;
            }

            charsWritten = 0;
            return false;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static void Int64ToNumber(long value, ref NumberBuffer number)
        {
            if (value >= 0)
            {
                number.IsNegative = false;
            }
            else
            {
                number.IsNegative = true;
                value = -value;
            }

            int i = value != 0 ? FormattingHelpers.CountDigits((ulong)value) : 0;
            number.DigitsCount = i;
            number.Scale = i;

            Span<byte> digits = number.Digits;
            UInt64ToDecChars(digits, i, (ulong)value, 0);
            digits[i] = (byte)'\0';

            number.CheckConsistency();
        }

        public static string Int64ToDecStr(long value)
        {
            return value >= 0 ?
                UInt64ToDecStr((ulong)value) :
                NegativeInt64ToDecStr(value, -1, NumberFormatInfo.CurrentInfo.NegativeSign);
        }

        private static string NegativeInt64ToDecStr(long value, int digits, string sNegative)
        {
            Debug.Assert(value < 0);

            if (digits < 1)
            {
                digits = 1;
            }

            int bufferLength = Math.Max(digits, FormattingHelpers.CountDigits((ulong)(-value))) + sNegative.Length;
            string result = string.FastAllocateString(bufferLength);
            Span<char> buffer = GetFreshStringSpan(result);
            UInt64ToDecChars((ulong)(-value), buffer.Slice(sNegative.Length));
            CopyNegativeSign(sNegative, buffer);
            return result;
        }

        internal static bool TryNegativeInt64ToDecStr<TChar>(long value, int digits, ReadOnlySpan<TChar> sNegative, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));
            Debug.Assert(value < 0);

            if (digits < 1)
            {
                digits = 1;
            }

            int bufferLength = Math.Max(digits, FormattingHelpers.CountDigits((ulong)(-value))) + sNegative.Length;
            if (bufferLength > destination.Length)
            {
                charsWritten = 0;
                return false;
            }

            charsWritten = bufferLength;
            int pos = UInt64ToDecChars<TChar>(destination, bufferLength, (ulong)(-value), digits);
            Debug.Assert(pos == sNegative.Length);
            CopyNegativeSign(sNegative, destination);
            return true;
        }

        private static string Int64ToHexStr(long value, char hexBase, int digits)
        {
            if (digits < 1)
            {
                digits = 1;
            }

            int bufferLength = Math.Max(digits, FormattingHelpers.CountHexDigits((ulong)value));
            string result = string.FastAllocateString(bufferLength);
            Span<char> buffer = GetFreshStringSpan(result);
            Int64ToHexChars(buffer, (ulong)value, hexBase);
            return result;
        }

        internal static bool TryInt64ToHexStr<TChar>(long value, char hexBase, int digits, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));

            if (digits < 1)
            {
                digits = 1;
            }

            int bufferLength = Math.Max(digits, FormattingHelpers.CountHexDigits((ulong)value));
            if (bufferLength > destination.Length)
            {
                charsWritten = 0;
                return false;
            }

            charsWritten = bufferLength;
            Int64ToHexChars(destination.Slice(0, bufferLength), (ulong)value, hexBase);
            return true;
        }

#if TARGET_64BIT
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
        private static void Int64ToHexChars<TChar>(Span<TChar> buffer, ulong value, int hexBase) where TChar : unmanaged, IUtfChar<TChar>
        {
#if TARGET_32BIT
            if (buffer.Length > 8)
            {
                int upperLength = buffer.Length - 8;
                Int32ToHexChars(buffer.Slice(upperLength), (uint)value, hexBase);
                Int32ToHexChars(buffer.Slice(0, upperLength), (uint)(value >> 32), hexBase);
            }
            else
            {
                Debug.Assert((uint)(value >> 32) == 0);
                Int32ToHexChars(buffer, (uint)value, hexBase);
            }
#else
            for (int i = buffer.Length - 1; (uint)i < (uint)buffer.Length; i--)
            {
                byte digit = (byte)(value & 0xF);
                buffer[i] = TChar.CastFrom(digit + (digit < 10 ? (byte)'0' : hexBase));
                value >>= 4;
            }

            Debug.Assert(value == 0);
#endif
        }

        private static string UInt64ToBinaryStr(ulong value, int digits)
        {
            if (digits < 1)
            {
                digits = 1;
            }

            int bufferLength = Math.Max(digits, 64 - (int)ulong.LeadingZeroCount(value));
            string result = string.FastAllocateString(bufferLength);
            Span<char> buffer = GetFreshStringSpan(result);
            UInt64ToBinaryChars(buffer, value);
            return result;
        }

        private static bool TryUInt64ToBinaryStr<TChar>(ulong value, int digits, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));

            if (digits < 1)
            {
                digits = 1;
            }

            int bufferLength = Math.Max(digits, 64 - (int)ulong.LeadingZeroCount(value));
            if (bufferLength > destination.Length)
            {
                charsWritten = 0;
                return false;
            }

            charsWritten = bufferLength;
            UInt64ToBinaryChars(destination.Slice(0, bufferLength), value);
            return true;
        }

#if TARGET_64BIT
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
        private static void UInt64ToBinaryChars<TChar>(Span<TChar> buffer, ulong value) where TChar : unmanaged, IUtfChar<TChar>
        {
#if TARGET_32BIT
            if (buffer.Length > 32)
            {
                int upperLength = buffer.Length - 32;
                UInt32ToBinaryChars(buffer.Slice(upperLength), (uint)value);
                UInt32ToBinaryChars(buffer.Slice(0, upperLength), (uint)(value >> 32));
            }
            else
            {
                Debug.Assert((uint)(value >> 32) == 0);
                UInt32ToBinaryChars(buffer, (uint)value);
            }
#else
            for (int i = buffer.Length - 1; (uint)i < (uint)buffer.Length; i--)
            {
                buffer[i] = TChar.CastFrom('0' + (byte)(value & 0x1));
                value >>= 1;
            }

            Debug.Assert(value == 0);
#endif
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void UInt64ToNumber(ulong value, ref NumberBuffer number)
        {
            number.IsNegative = false;

            int i = value != 0 ? FormattingHelpers.CountDigits(value) : 0;
            number.DigitsCount = i;
            number.Scale = i;

            Span<byte> digits = number.Digits;
            UInt64ToDecChars(digits, i, value, 0);
            digits[i] = (byte)'\0';

            number.CheckConsistency();
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static uint Int64DivMod1E9(ref ulong value)
        {
            uint rem = (uint)(value % 1_000_000_000);
            value /= 1_000_000_000;
            return rem;
        }

#if TARGET_64BIT
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
        private static void UInt64ToDecChars(ulong value, Span<char> buffer)
        {
            Debug.Assert(!buffer.IsEmpty);

#if TARGET_32BIT
            while ((uint)(value >> 32) != 0)
            {
                Debug.Assert(buffer.Length > 9);
                int index = buffer.Length - 9;
                UInt32ToDecChars(Int64DivMod1E9(ref value), buffer.Slice(index));
                buffer = buffer.Slice(0, index);
            }
            UInt32ToDecChars((uint)value, buffer);
#else
            int leadingDigits = 2 - (buffer.Length & 1);
            Span<DigitPair> pairs = MemoryMarshal.Cast<char, DigitPair>(buffer.Slice(leadingDigits));

            for (int i = pairs.Length - 1; (uint)i < (uint)pairs.Length; i--)
            {
                (value, ulong remainder) = Math.DivRem(value, 100);
                pairs[i] = new DigitPair(GetTwoDigitsChars((uint)remainder));
            }

            if (leadingDigits == 1)
            {
                Debug.Assert(value < 10);
                buffer[0] = (char)(value + '0');
            }
            else
            {
                Debug.Assert(value < 100);
                WriteTwoDigits((uint)value, buffer.Slice(0, 2));
            }
#endif
        }

#if TARGET_64BIT
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
        internal static int UInt64ToDecChars<TChar>(Span<TChar> buffer, int index, ulong value) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));

#if TARGET_32BIT
            while ((uint)(value >> 32) != 0)
            {
                index = UInt32ToDecChars(buffer, index, Int64DivMod1E9(ref value), 9);
            }
            return UInt32ToDecChars(buffer, index, (uint)value);
#else
            if (value >= 10)
            {
                while (value >= 100)
                {
                    index -= 2;
                    (value, ulong remainder) = Math.DivRem(value, 100);
                    WriteTwoDigits((uint)remainder, buffer, index);
                }
                if (value >= 10)
                {
                    index -= 2;
                    WriteTwoDigits((uint)value, buffer, index);
                    return index;
                }
            }
            buffer[--index] = TChar.CastFrom(value + '0');
            return index;
#endif
        }

#if TARGET_64BIT
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
        internal static int UInt64ToDecChars<TChar>(Span<TChar> buffer, int index, ulong value, int digits) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));

#if TARGET_32BIT
            while ((uint)(value >> 32) != 0)
            {
                index = UInt32ToDecChars(buffer, index, Int64DivMod1E9(ref value), 9);
                digits -= 9;
            }
            return UInt32ToDecChars(buffer, index, (uint)value, digits);
#else
            ulong remainder;
            while (value >= 100)
            {
                index -= 2;
                digits -= 2;
                (value, remainder) = Math.DivRem(value, 100);
                WriteTwoDigits((uint)remainder, buffer, index);
            }
            while (value != 0 || digits > 0)
            {
                digits--;
                (value, remainder) = Math.DivRem(value, 10);
                buffer[--index] = TChar.CastFrom(remainder + '0');
            }
            return index;
#endif
        }

        internal static string UInt64ToDecStr(ulong value)
        {
            // For small numbers, consult a lazily-populated cache.
            if (value < SmallNumberCacheLength)
            {
                return UInt32ToDecStrForKnownSmallNumber((uint)value);
            }

            int bufferLength = FormattingHelpers.CountDigits(value);
            string result = string.FastAllocateString(bufferLength);
            Span<char> buffer = GetFreshStringSpan(result);
            UInt64ToDecChars(value, buffer);
            return result;
        }

        internal static string UInt64ToDecStr(ulong value, int digits)
        {
            if (digits <= 1)
            {
                return UInt64ToDecStr(value);
            }

            int bufferLength = Math.Max(digits, FormattingHelpers.CountDigits(value));
            string result = string.FastAllocateString(bufferLength);
            Span<char> buffer = GetFreshStringSpan(result);
            UInt64ToDecChars(value, buffer);
            return result;
        }

        internal static bool TryUInt64ToDecStr<TChar>(ulong value, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));

            int bufferLength = FormattingHelpers.CountDigits(value);
            if (bufferLength <= destination.Length)
            {
                charsWritten = bufferLength;
                int pos = UInt64ToDecChars<TChar>(destination, bufferLength, value);
                Debug.Assert(pos == 0);
                return true;
            }

            charsWritten = 0;
            return false;
        }

        internal static bool TryUInt64ToDecStr<TChar>(ulong value, int digits, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
        {
            int countedDigits = FormattingHelpers.CountDigits(value);
            int bufferLength = Math.Max(digits, countedDigits);
            if (bufferLength <= destination.Length)
            {
                charsWritten = bufferLength;
                int pos = digits > countedDigits ?
                    UInt64ToDecChars<TChar>(destination, bufferLength, value, digits) :
                    UInt64ToDecChars<TChar>(destination, bufferLength, value);
                Debug.Assert(pos == 0);
                return true;
            }

            charsWritten = 0;
            return false;
        }

        private static void Int128ToNumber(Int128 value, ref NumberBuffer number)
        {
            number.DigitsCount = Int128Precision;

            if (Int128.IsPositive(value))
            {
                number.IsNegative = false;
            }
            else
            {
                number.IsNegative = true;
                value = -value;
            }

            Span<byte> digits = number.Digits;
            int start = UInt128ToDecChars(digits, Int128Precision, (UInt128)value, 0);

            int i = Int128Precision - start;

            number.DigitsCount = i;
            number.Scale = i;

            if (start != 0)
            {
                digits.Slice(start, i).CopyTo(digits);
            }
            digits[i] = (byte)'\0';

            number.CheckConsistency();
        }

        public static string Int128ToDecStr(Int128 value)
        {
            return Int128.IsPositive(value)
                 ? UInt128ToDecStr((UInt128)value, -1)
                 : NegativeInt128ToDecStr(value, -1, NumberFormatInfo.CurrentInfo.NegativeSign);
        }

        private static string NegativeInt128ToDecStr(Int128 value, int digits, string sNegative)
        {
            Debug.Assert(Int128.IsNegative(value));

            if (digits < 1)
            {
                digits = 1;
            }

            UInt128 absValue = (UInt128)(-value);

            int bufferLength = Math.Max(digits, FormattingHelpers.CountDigits(absValue)) + sNegative.Length;
            string result = string.FastAllocateString(bufferLength);
            Span<char> buffer = GetFreshStringSpan(result);
            UInt128ToDecChars(absValue, buffer.Slice(sNegative.Length));
            CopyNegativeSign(sNegative, buffer);
            return result;
        }

        private static bool TryNegativeInt128ToDecStr<TChar>(Int128 value, int digits, ReadOnlySpan<TChar> sNegative, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));
            Debug.Assert(Int128.IsNegative(value));

            if (digits < 1)
            {
                digits = 1;
            }

            UInt128 absValue = (UInt128)(-value);

            int bufferLength = Math.Max(digits, FormattingHelpers.CountDigits(absValue)) + sNegative.Length;
            if (bufferLength > destination.Length)
            {
                charsWritten = 0;
                return false;
            }

            charsWritten = bufferLength;
            int pos = UInt128ToDecChars<TChar>(destination, bufferLength, absValue, digits);
            Debug.Assert(pos == sNegative.Length);
            CopyNegativeSign(sNegative, destination);
            return true;
        }

        private static string Int128ToHexStr(Int128 value, char hexBase, int digits)
        {
            if (digits < 1)
            {
                digits = 1;
            }

            UInt128 uValue = (UInt128)value;

            int bufferLength = Math.Max(digits, FormattingHelpers.CountHexDigits(uValue));
            string result = string.FastAllocateString(bufferLength);
            Span<char> buffer = GetFreshStringSpan(result);
            Int128ToHexChars(buffer, uValue, hexBase);
            return result;
        }

        private static bool TryInt128ToHexStr<TChar>(Int128 value, char hexBase, int digits, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));

            if (digits < 1)
            {
                digits = 1;
            }

            UInt128 uValue = (UInt128)value;

            int bufferLength = Math.Max(digits, FormattingHelpers.CountHexDigits(uValue));
            if (bufferLength > destination.Length)
            {
                charsWritten = 0;
                return false;
            }

            charsWritten = bufferLength;
            Int128ToHexChars(destination.Slice(0, bufferLength), uValue, hexBase);
            return true;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static void Int128ToHexChars<TChar>(Span<TChar> buffer, UInt128 value, int hexBase) where TChar : unmanaged, IUtfChar<TChar>
        {
            if (buffer.Length > 16)
            {
                int upperLength = buffer.Length - 16;
                Int64ToHexChars(buffer.Slice(upperLength), value.Lower, hexBase);
                Int64ToHexChars(buffer.Slice(0, upperLength), value.Upper, hexBase);
            }
            else
            {
                Debug.Assert(value.Upper == 0);
                Int64ToHexChars(buffer, value.Lower, hexBase);
            }
        }

        private static string UInt128ToBinaryStr(Int128 value, int digits)
        {
            if (digits < 1)
            {
                digits = 1;
            }

            UInt128 uValue = (UInt128)value;

            int bufferLength = Math.Max(digits, 128 - (int)UInt128.LeadingZeroCount((UInt128)value));
            string result = string.FastAllocateString(bufferLength);
            Span<char> buffer = GetFreshStringSpan(result);
            UInt128ToBinaryChars(buffer, uValue);
            return result;
        }

        private static bool TryUInt128ToBinaryStr<TChar>(Int128 value, int digits, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));

            if (digits < 1)
            {
                digits = 1;
            }

            UInt128 uValue = (UInt128)value;

            int bufferLength = Math.Max(digits, 128 - (int)UInt128.LeadingZeroCount((UInt128)value));
            if (bufferLength > destination.Length)
            {
                charsWritten = 0;
                return false;
            }

            charsWritten = bufferLength;
            UInt128ToBinaryChars(destination.Slice(0, bufferLength), uValue);
            return true;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static void UInt128ToBinaryChars<TChar>(Span<TChar> buffer, UInt128 value) where TChar : unmanaged, IUtfChar<TChar>
        {
            if (buffer.Length > 64)
            {
                int upperLength = buffer.Length - 64;
                UInt64ToBinaryChars(buffer.Slice(upperLength), value.Lower);
                UInt64ToBinaryChars(buffer.Slice(0, upperLength), value.Upper);
            }
            else
            {
                Debug.Assert(value.Upper == 0);
                UInt64ToBinaryChars(buffer, value.Lower);
            }
        }

        internal static void UInt128ToNumber(UInt128 value, ref NumberBuffer number)
        {
            number.DigitsCount = UInt128Precision;
            number.IsNegative = false;

            Span<byte> digits = number.Digits;
            int start = UInt128ToDecChars(digits, UInt128Precision, value, 0);

            int i = UInt128Precision - start;

            number.DigitsCount = i;
            number.Scale = i;

            if (start != 0)
            {
                digits.Slice(start, i).CopyTo(digits);
            }
            digits[i] = (byte)'\0';

            number.CheckConsistency();
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static ulong Int128DivMod1E19(ref UInt128 value)
        {
            UInt128 divisor = new UInt128(0, 10_000_000_000_000_000_000);
            (value, UInt128 remainder) = UInt128.DivRem(value, divisor);
            return remainder.Lower;
        }

#if TARGET_64BIT
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
        private static void UInt128ToDecChars(UInt128 value, Span<char> buffer)
        {
            Debug.Assert(!buffer.IsEmpty);

#if TARGET_32BIT
            while (value.Upper != 0)
#else
            while (buffer.Length > 19)
#endif
            {
                Debug.Assert(buffer.Length > 19);
                int index = buffer.Length - 19;
                UInt64ToDecChars(Int128DivMod1E19(ref value), buffer.Slice(index));
                buffer = buffer.Slice(0, index);
            }
            Debug.Assert(value.Upper == 0);
            UInt64ToDecChars(value.Lower, buffer);
        }

        internal static int UInt128ToDecChars<TChar>(Span<TChar> buffer, int index, UInt128 value) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));

            while (value.Upper != 0)
            {
                index = UInt64ToDecChars(buffer, index, Int128DivMod1E19(ref value), 19);
            }
            return UInt64ToDecChars(buffer, index, value.Lower);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static int UInt128ToDecChars<TChar>(Span<TChar> buffer, int index, UInt128 value, int digits) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));

            while (value.Upper != 0)
            {
                index = UInt64ToDecChars(buffer, index, Int128DivMod1E19(ref value), 19);
                digits -= 19;
            }
            return UInt64ToDecChars(buffer, index, value.Lower, digits);
        }

        internal static string UInt128ToDecStr(UInt128 value)
        {
            if (value.Upper == 0)
            {
                return UInt64ToDecStr(value.Lower);
            }

            int bufferLength = FormattingHelpers.CountDigits(value);
            string result = string.FastAllocateString(bufferLength);
            Span<char> buffer = GetFreshStringSpan(result);
            UInt128ToDecChars(value, buffer);
            return result;
        }

        internal static string UInt128ToDecStr(UInt128 value, int digits)
        {
            if (digits <= 1)
            {
                return UInt128ToDecStr(value);
            }

            int bufferLength = Math.Max(digits, FormattingHelpers.CountDigits(value));
            string result = string.FastAllocateString(bufferLength);
            Span<char> buffer = GetFreshStringSpan(result);
            UInt128ToDecChars(value, buffer);
            return result;
        }

        private static bool TryUInt128ToDecStr<TChar>(UInt128 value, int digits, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
        {
            int countedDigits = FormattingHelpers.CountDigits(value);
            int bufferLength = Math.Max(digits, countedDigits);
            if (bufferLength <= destination.Length)
            {
                charsWritten = bufferLength;
                int pos = digits > countedDigits ?
                    UInt128ToDecChars<TChar>(destination, bufferLength, value, digits) :
                    UInt128ToDecChars<TChar>(destination, bufferLength, value);
                Debug.Assert(pos == 0);
                return true;
            }

            charsWritten = 0;
            return false;
        }

        private static ulong ExtractFractionAndBiasedExponent<TNumber>(TNumber value, out int exponent)
            where TNumber : unmanaged, IBinaryFloatParseAndFormatInfo<TNumber>
        {
            ulong bits = TNumber.FloatToBits(value);
            ulong fraction = (bits & TNumber.DenormalMantissaMask);
            exponent = ((int)(bits >> TNumber.DenormalMantissaBits) & TNumber.InfinityExponent);

            if (exponent != 0)
            {
                // For normalized value,
                // value = 1.fraction * 2^(exp - ExponentBias)
                //       = (1 + mantissa / 2^TrailingSignificandLength) * 2^(exp - ExponentBias)
                //       = (2^TrailingSignificandLength + mantissa) * 2^(exp - ExponentBias - TrailingSignificandLength)
                //
                // So f = (2^TrailingSignificandLength + mantissa), e = exp - ExponentBias - TrailingSignificandLength;

                fraction |= (1UL << TNumber.DenormalMantissaBits);
                exponent -= TNumber.ExponentBias + TNumber.DenormalMantissaBits;
            }
            else
            {
                // For denormalized value,
                // value = 0.fraction * 2^(MinBinaryExponent)
                //       = (mantissa / 2^TrailingSignificandLength) * 2^(MinBinaryExponent)
                //       = mantissa * 2^(MinBinaryExponent - TrailingSignificandLength)
                //       = mantissa * 2^(MinBinaryExponent - TrailingSignificandLength)
                // So f = mantissa, e = MinBinaryExponent - TrailingSignificandLength
                exponent = TNumber.MinBinaryExponent - TNumber.DenormalMantissaBits;
            }

            return fraction;
        }
    }
}