File: src\libraries\Common\src\System\Number.Formatting.Common.cs
Web Access
Project: src\src\coreclr\System.Private.CoreLib\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.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
 
namespace System
{
    internal static partial class Number
    {
        private const int CharStackBufferSize = 32;
 
        private const int DefaultPrecisionExponentialFormat = 6;
 
        private const int MaxUInt32DecDigits = 10;
        private const string PosNumberFormat = "#";
 
        private static readonly string[] s_posCurrencyFormats =
        [
            "$#", "#$", "$ #", "# $"
        ];
 
        private static readonly string[] s_negCurrencyFormats =
        [
            "($#)", "-$#", "$-#", "$#-",
            "(#$)", "-#$", "#-$", "#$-",
            "-# $", "-$ #", "# $-", "$ #-",
            "$ -#", "#- $", "($ #)", "(# $)",
            "$- #"
        ];
 
        private static readonly string[] s_posPercentFormats =
        [
            "# %", "#%", "%#", "% #"
        ];
 
        private static readonly string[] s_negPercentFormats =
        [
            "-# %", "-#%", "-%#",
            "%-#", "%#-",
            "#-%", "#%-",
            "-% #", "# %-", "% #-",
            "% -#", "#- %"
        ];
 
        private static readonly string[] s_negNumberFormats =
        [
            "(#)", "-#", "- #", "#-", "# -",
        ];
 
        internal static char ParseFormatSpecifier(ReadOnlySpan<char> format, out int digits)
        {
            char c = default;
            if (format.Length > 0)
            {
                // If the format begins with a symbol, see if it's a standard format
                // with or without a specified number of digits.
                c = format[0];
                if (char.IsAsciiLetter(c))
                {
                    // Fast path for sole symbol, e.g. "D"
                    if (format.Length == 1)
                    {
                        digits = -1;
                        return c;
                    }
 
                    if (format.Length == 2)
                    {
                        // Fast path for symbol and single digit, e.g. "X4"
                        int d = format[1] - '0';
                        if ((uint)d < 10)
                        {
                            digits = d;
                            return c;
                        }
                    }
                    else if (format.Length == 3)
                    {
                        // Fast path for symbol and double digit, e.g. "F12"
                        int d1 = format[1] - '0', d2 = format[2] - '0';
                        if ((uint)d1 < 10 && (uint)d2 < 10)
                        {
                            digits = d1 * 10 + d2;
                            return c;
                        }
                    }
 
                    // Fallback for symbol and any length digits.  The digits value must be >= 0 && <= 999_999_999,
                    // but it can begin with any number of 0s, and thus we may need to check more than 9
                    // digits.  Further, for compat, we need to stop when we hit a null char.
                    int n = 0;
                    int i = 1;
                    while ((uint)i < (uint)format.Length && char.IsAsciiDigit(format[i]))
                    {
                        // Check if we are about to overflow past our limit of 9 digits
                        if (n >= 100_000_000)
                        {
                            ThrowHelper.ThrowFormatException_BadFormatSpecifier();
                        }
                        n = (n * 10) + format[i++] - '0';
                    }
 
                    // If we're at the end of the digits rather than having stopped because we hit something
                    // other than a digit or overflowed, return the standard format info.
                    if ((uint)i >= (uint)format.Length || format[i] == '\0')
                    {
                        digits = n;
                        return c;
                    }
                }
            }
 
            // Default empty format to be "G"; custom format is signified with '\0'.
            digits = -1;
            return format.Length == 0 || c == '\0' ? // For compat, treat '\0' as the end of the specifier, even if the specifier extends beyond it.
                'G' :
                '\0';
        }
 
#if !SYSTEM_PRIVATE_CORELIB
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static unsafe TChar* UInt32ToDecChars<TChar>(TChar* bufferEnd, uint value, int digits) where TChar : unmanaged, IUtfChar<TChar>
        {
            // TODO: Consider to bring optimized implementation from CoreLib
 
            while (value != 0 || digits > 0)
            {
                digits--;
                (value, uint remainder) = Math.DivRem(value, 10);
                *(--bufferEnd) = TChar.CastFrom(remainder + '0');
            }
 
            return bufferEnd;
        }
#endif
 
        internal static unsafe void NumberToString<TChar>(ref ValueListBuilder<TChar> vlb, ref NumberBuffer number, char format, int nMaxDigits, NumberFormatInfo info) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(sizeof(TChar) == sizeof(char) || sizeof(TChar) == sizeof(byte));
 
            number.CheckConsistency();
            bool isCorrectlyRounded = (number.Kind == NumberBufferKind.FloatingPoint);
 
            switch (format)
            {
                case 'C':
                case 'c':
                    {
                        if (nMaxDigits < 0)
                        {
                            nMaxDigits = info.CurrencyDecimalDigits;
                        }
 
                        RoundNumber(ref number, number.Scale + nMaxDigits, isCorrectlyRounded); // Don't change this line to use digPos since digCount could have its sign changed.
 
                        FormatCurrency(ref vlb, ref number, nMaxDigits, info);
 
                        break;
                    }
 
                case 'F':
                case 'f':
                    {
                        if (nMaxDigits < 0)
                        {
                            nMaxDigits = info.NumberDecimalDigits;
                        }
 
                        RoundNumber(ref number, number.Scale + nMaxDigits, isCorrectlyRounded);
 
                        if (number.IsNegative)
                        {
                            vlb.Append(info.NegativeSignTChar<TChar>());
                        }
 
                        FormatFixed(ref vlb, ref number, nMaxDigits, null, info.NumberDecimalSeparatorTChar<TChar>(), null);
 
                        break;
                    }
 
                case 'N':
                case 'n':
                    {
                        if (nMaxDigits < 0)
                        {
                            nMaxDigits = info.NumberDecimalDigits; // Since we are using digits in our calculation
                        }
 
                        RoundNumber(ref number, number.Scale + nMaxDigits, isCorrectlyRounded);
 
                        FormatNumber(ref vlb, ref number, nMaxDigits, info);
 
                        break;
                    }
 
                case 'E':
                case 'e':
                    {
                        if (nMaxDigits < 0)
                        {
                            nMaxDigits = DefaultPrecisionExponentialFormat;
                        }
                        nMaxDigits++;
 
                        RoundNumber(ref number, nMaxDigits, isCorrectlyRounded);
 
                        if (number.IsNegative)
                        {
                            vlb.Append(info.NegativeSignTChar<TChar>());
                        }
 
                        FormatScientific(ref vlb, ref number, nMaxDigits, info, format);
 
                        break;
                    }
 
                case 'G':
                case 'g':
                    {
                        bool noRounding = false;
                        if (nMaxDigits < 1)
                        {
                            if ((number.Kind == NumberBufferKind.Decimal) && (nMaxDigits == -1))
                            {
                                noRounding = true;  // Turn off rounding for ECMA compliance to output trailing 0's after decimal as significant
 
                                if (number.Digits[0] == 0)
                                {
                                    // -0 should be formatted as 0 for decimal. This is normally handled by RoundNumber (which we are skipping)
                                    goto SkipSign;
                                }
 
                                goto SkipRounding;
                            }
                            else
                            {
                                // This ensures that the PAL code pads out to the correct place even when we use the default precision
                                nMaxDigits = number.DigitsCount;
                            }
                        }
 
                        RoundNumber(ref number, nMaxDigits, isCorrectlyRounded);
 
                    SkipRounding:
                        if (number.IsNegative)
                        {
                            vlb.Append(info.NegativeSignTChar<TChar>());
                        }
 
                    SkipSign:
                        FormatGeneral(ref vlb, ref number, nMaxDigits, info, (char)(format - ('G' - 'E')), noRounding);
 
                        break;
                    }
 
                case 'P':
                case 'p':
                    {
                        if (nMaxDigits < 0)
                        {
                            nMaxDigits = info.PercentDecimalDigits;
                        }
                        number.Scale += 2;
 
                        RoundNumber(ref number, number.Scale + nMaxDigits, isCorrectlyRounded);
 
                        FormatPercent(ref vlb, ref number, nMaxDigits, info);
 
                        break;
                    }
 
                case 'R':
                case 'r':
                    {
                        format = (char)(format - ('R' - 'G'));
                        Debug.Assert(format is 'G' or 'g');
                        goto case 'G';
                    }
 
                default:
                    ThrowHelper.ThrowFormatException_BadFormatSpecifier();
                    break;
            }
        }
 
        internal static unsafe void NumberToStringFormat<TChar>(ref ValueListBuilder<TChar> vlb, ref NumberBuffer number, ReadOnlySpan<char> format, NumberFormatInfo info) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(sizeof(TChar) == sizeof(char) || sizeof(TChar) == sizeof(byte));
 
            number.CheckConsistency();
 
            int digitCount;
            int decimalPos;
            int firstDigit;
            int lastDigit;
            int digPos;
            bool scientific;
            int thousandPos;
            int thousandCount = 0;
            bool thousandSeps;
            int scaleAdjust;
            int adjust;
 
            int section;
            int src;
            byte* dig = number.DigitsPtr;
            char ch;
 
            section = FindSection(format, dig[0] == 0 ? 2 : number.IsNegative ? 1 : 0);
 
            while (true)
            {
                digitCount = 0;
                decimalPos = -1;
                firstDigit = 0x7FFFFFFF;
                lastDigit = 0;
                scientific = false;
                thousandPos = -1;
                thousandSeps = false;
                scaleAdjust = 0;
                src = section;
 
                fixed (char* pFormat = &MemoryMarshal.GetReference(format))
                {
                    while (src < format.Length && (ch = pFormat[src++]) != 0 && ch != ';')
                    {
                        switch (ch)
                        {
                            case '#':
                                digitCount++;
                                break;
 
                            case '0':
                                if (firstDigit == 0x7FFFFFFF)
                                {
                                    firstDigit = digitCount;
                                }
                                digitCount++;
                                lastDigit = digitCount;
                                break;
 
                            case '.':
                                if (decimalPos < 0)
                                {
                                    decimalPos = digitCount;
                                }
                                break;
 
                            case ',':
                                if (digitCount > 0 && decimalPos < 0)
                                {
                                    if (thousandPos >= 0)
                                    {
                                        if (thousandPos == digitCount)
                                        {
                                            thousandCount++;
                                            break;
                                        }
                                        thousandSeps = true;
                                    }
                                    thousandPos = digitCount;
                                    thousandCount = 1;
                                }
                                break;
 
                            case '%':
                                scaleAdjust += 2;
                                break;
 
                            case '\x2030':
                                scaleAdjust += 3;
                                break;
 
                            case '\'':
                            case '"':
                                while (src < format.Length && pFormat[src] != 0 && pFormat[src++] != ch) ;
                                break;
 
                            case '\\':
                                if (src < format.Length && pFormat[src] != 0)
                                {
                                    src++;
                                }
                                break;
 
                            case 'E':
                            case 'e':
                                if ((src < format.Length && pFormat[src] == '0') ||
                                    (src + 1 < format.Length && (pFormat[src] == '+' || pFormat[src] == '-') && pFormat[src + 1] == '0'))
                                {
                                    while (++src < format.Length && pFormat[src] == '0') ;
                                    scientific = true;
                                }
                                break;
                        }
                    }
                }
 
                if (decimalPos < 0)
                {
                    decimalPos = digitCount;
                }
 
                if (thousandPos >= 0)
                {
                    if (thousandPos == decimalPos)
                    {
                        scaleAdjust -= thousandCount * 3;
                    }
                    else
                    {
                        thousandSeps = true;
                    }
                }
 
                if (dig[0] != 0)
                {
                    number.Scale += scaleAdjust;
                    int pos = scientific ? digitCount : number.Scale + digitCount - decimalPos;
                    RoundNumber(ref number, pos, isCorrectlyRounded: false);
                    if (dig[0] == 0)
                    {
                        src = FindSection(format, 2);
                        if (src != section)
                        {
                            section = src;
                            continue;
                        }
                    }
                }
                else
                {
                    if (number.Kind != NumberBufferKind.FloatingPoint)
                    {
                        // The integer types don't have a concept of -0 and decimal always format -0 as 0
                        number.IsNegative = false;
                    }
                    number.Scale = 0;      // Decimals with scale ('0.00') should be rounded.
                }
 
                break;
            }
 
            firstDigit = firstDigit < decimalPos ? decimalPos - firstDigit : 0;
            lastDigit = lastDigit > decimalPos ? decimalPos - lastDigit : 0;
            if (scientific)
            {
                digPos = decimalPos;
                adjust = 0;
            }
            else
            {
                digPos = number.Scale > decimalPos ? number.Scale : decimalPos;
                adjust = number.Scale - decimalPos;
            }
            src = section;
 
            // Adjust can be negative, so we make this an int instead of an unsigned int.
            // Adjust represents the number of characters over the formatting e.g. format string is "0000" and you are trying to
            // format 100000 (6 digits). Means adjust will be 2. On the other hand if you are trying to format 10 adjust will be
            // -2 and we'll need to fixup these digits with 0 padding if we have 0 formatting as in this example.
            Span<int> thousandsSepPos = stackalloc int[4];
            int thousandsSepCtr = -1;
 
            if (thousandSeps)
            {
                // We need to precompute this outside the number formatting loop
                if (info.NumberGroupSeparator.Length > 0)
                {
                    // We need this array to figure out where to insert the thousands separator. We would have to traverse the string
                    // backwards. PIC formatting always traverses forwards. These indices are precomputed to tell us where to insert
                    // the thousands separator so we can get away with traversing forwards. Note we only have to compute up to digPos.
                    // The max is not bound since you can have formatting strings of the form "000,000..", and this
                    // should handle that case too.
 
                    int[] groupDigits = info.NumberGroupSizes();
 
                    int groupSizeIndex = 0;     // Index into the groupDigits array.
                    int groupTotalSizeCount = 0;
                    int groupSizeLen = groupDigits.Length;    // The length of groupDigits array.
                    if (groupSizeLen != 0)
                    {
                        groupTotalSizeCount = groupDigits[groupSizeIndex];   // The current running total of group size.
                    }
                    int groupSize = groupTotalSizeCount;
 
                    int totalDigits = digPos + ((adjust < 0) ? adjust : 0); // Actual number of digits in o/p
                    int numDigits = (firstDigit > totalDigits) ? firstDigit : totalDigits;
                    while (numDigits > groupTotalSizeCount)
                    {
                        if (groupSize == 0)
                        {
                            break;
                        }
 
                        ++thousandsSepCtr;
                        if (thousandsSepCtr >= thousandsSepPos.Length)
                        {
                            var newThousandsSepPos = new int[thousandsSepPos.Length * 2];
                            thousandsSepPos.CopyTo(newThousandsSepPos);
                            thousandsSepPos = newThousandsSepPos;
                        }
 
                        thousandsSepPos[thousandsSepCtr] = groupTotalSizeCount;
                        if (groupSizeIndex < groupSizeLen - 1)
                        {
                            groupSizeIndex++;
                            groupSize = groupDigits[groupSizeIndex];
                        }
                        groupTotalSizeCount += groupSize;
                    }
                }
            }
 
            if (number.IsNegative && (section == 0) && (number.Scale != 0))
            {
                vlb.Append(info.NegativeSignTChar<TChar>());
            }
 
            bool decimalWritten = false;
 
            fixed (char* pFormat = &MemoryMarshal.GetReference(format))
            {
                byte* cur = dig;
 
                while (src < format.Length && (ch = pFormat[src++]) != 0 && ch != ';')
                {
                    if (adjust > 0)
                    {
                        switch (ch)
                        {
                            case '#':
                            case '0':
                            case '.':
                                while (adjust > 0)
                                {
                                    // digPos will be one greater than thousandsSepPos[thousandsSepCtr] since we are at
                                    // the character after which the groupSeparator needs to be appended.
                                    vlb.Append(TChar.CastFrom(*cur != 0 ? (char)(*cur++) : '0'));
                                    if (thousandSeps && digPos > 1 && thousandsSepCtr >= 0)
                                    {
                                        if (digPos == thousandsSepPos[thousandsSepCtr] + 1)
                                        {
                                            vlb.Append(info.NumberGroupSeparatorTChar<TChar>());
                                            thousandsSepCtr--;
                                        }
                                    }
                                    digPos--;
                                    adjust--;
                                }
                                break;
                        }
                    }
 
                    switch (ch)
                    {
                        case '#':
                        case '0':
                            {
                                if (adjust < 0)
                                {
                                    adjust++;
                                    ch = digPos <= firstDigit ? '0' : '\0';
                                }
                                else
                                {
                                    ch = *cur != 0 ? (char)(*cur++) : digPos > lastDigit ? '0' : '\0';
                                }
 
                                if (ch != 0)
                                {
                                    vlb.Append(TChar.CastFrom(ch));
                                    if (thousandSeps && digPos > 1 && thousandsSepCtr >= 0)
                                    {
                                        if (digPos == thousandsSepPos[thousandsSepCtr] + 1)
                                        {
                                            vlb.Append(info.NumberGroupSeparatorTChar<TChar>());
                                            thousandsSepCtr--;
                                        }
                                    }
                                }
 
                                digPos--;
                                break;
                            }
 
                        case '.':
                            {
                                if (digPos != 0 || decimalWritten)
                                {
                                    // For compatibility, don't echo repeated decimals
                                    break;
                                }
 
                                // If the format has trailing zeros or the format has a decimal and digits remain
                                if (lastDigit < 0 || (decimalPos < digitCount && *cur != 0))
                                {
                                    vlb.Append(info.NumberDecimalSeparatorTChar<TChar>());
                                    decimalWritten = true;
                                }
                                break;
                            }
 
                        case '\x2030':
                            vlb.Append(info.PerMilleSymbolTChar<TChar>());
                            break;
 
                        case '%':
                            vlb.Append(info.PercentSymbolTChar<TChar>());
                            break;
 
                        case ',':
                            break;
 
                        case '\'':
                        case '"':
                            while (src < format.Length && pFormat[src] != 0 && pFormat[src] != ch)
                            {
                                AppendUnknownChar(ref vlb, pFormat[src++]);
                            }
 
                            if (src < format.Length && pFormat[src] != 0)
                            {
                                src++;
                            }
                            break;
 
                        case '\\':
                            if (src < format.Length && pFormat[src] != 0)
                            {
                                AppendUnknownChar(ref vlb, pFormat[src++]);
                            }
                            break;
 
                        case 'E':
                        case 'e':
                            {
                                bool positiveSign = false;
                                int i = 0;
                                if (scientific)
                                {
                                    if (src < format.Length && pFormat[src] == '0')
                                    {
                                        // Handles E0, which should format the same as E-0
                                        i++;
                                    }
                                    else if (src + 1 < format.Length && pFormat[src] == '+' && pFormat[src + 1] == '0')
                                    {
                                        // Handles E+0
                                        positiveSign = true;
                                    }
                                    else if (src + 1 < format.Length && pFormat[src] == '-' && pFormat[src + 1] == '0')
                                    {
                                        // Handles E-0
                                        // Do nothing, this is just a place holder s.t. we don't break out of the loop.
                                    }
                                    else
                                    {
                                        vlb.Append(TChar.CastFrom(ch));
                                        break;
                                    }
 
                                    while (++src < format.Length && pFormat[src] == '0')
                                    {
                                        i++;
                                    }
 
                                    if (i > 10)
                                    {
                                        i = 10;
                                    }
 
                                    int exp = dig[0] == 0 ? 0 : number.Scale - decimalPos;
                                    FormatExponent(ref vlb, info, exp, ch, i, positiveSign);
                                    scientific = false;
                                }
                                else
                                {
                                    vlb.Append(TChar.CastFrom(ch));
                                    if (src < format.Length)
                                    {
                                        if (pFormat[src] == '+' || pFormat[src] == '-')
                                        {
                                            AppendUnknownChar(ref vlb, pFormat[src++]);
                                        }
 
                                        while (src < format.Length && pFormat[src] == '0')
                                        {
                                            AppendUnknownChar(ref vlb, pFormat[src++]);
                                        }
                                    }
                                }
                                break;
                            }
 
                        default:
                            AppendUnknownChar(ref vlb, ch);
                            break;
                    }
                }
            }
 
            if (number.IsNegative && (section == 0) && (number.Scale == 0) && (vlb.Length > 0))
            {
                vlb.Insert(0, info.NegativeSignTChar<TChar>());
            }
        }
 
        private static unsafe void FormatCurrency<TChar>(ref ValueListBuilder<TChar> vlb, ref NumberBuffer number, int nMaxDigits, NumberFormatInfo info) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(sizeof(TChar) == sizeof(char) || sizeof(TChar) == sizeof(byte));
 
            string fmt = number.IsNegative ?
                s_negCurrencyFormats[info.CurrencyNegativePattern] :
                s_posCurrencyFormats[info.CurrencyPositivePattern];
 
            foreach (char ch in fmt)
            {
                switch (ch)
                {
                    case '#':
                        FormatFixed(ref vlb, ref number, nMaxDigits, info.CurrencyGroupSizes(), info.CurrencyDecimalSeparatorTChar<TChar>(), info.CurrencyGroupSeparatorTChar<TChar>());
                        break;
 
                    case '-':
                        vlb.Append(info.NegativeSignTChar<TChar>());
                        break;
 
                    case '$':
                        vlb.Append(info.CurrencySymbolTChar<TChar>());
                        break;
 
                    default:
                        vlb.Append(TChar.CastFrom(ch));
                        break;
                }
            }
        }
 
        private static unsafe void FormatFixed<TChar>(
            ref ValueListBuilder<TChar> vlb, ref NumberBuffer number,
            int nMaxDigits, int[]? groupDigits,
            ReadOnlySpan<TChar> sDecimal, ReadOnlySpan<TChar> sGroup) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(sizeof(TChar) == sizeof(char) || sizeof(TChar) == sizeof(byte));
 
            int digPos = number.Scale;
            byte* dig = number.DigitsPtr;
 
            if (digPos > 0)
            {
                if (groupDigits != null)
                {
                    int groupSizeIndex = 0;                             // Index into the groupDigits array.
                    int bufferSize = digPos;                            // The length of the result buffer string.
                    int groupSize = 0;                                  // The current group size.
 
                    // Find out the size of the string buffer for the result.
                    if (groupDigits.Length != 0) // You can pass in 0 length arrays
                    {
                        int groupSizeCount = groupDigits[groupSizeIndex];   // The current total of group size.
 
                        while (digPos > groupSizeCount)
                        {
                            groupSize = groupDigits[groupSizeIndex];
                            if (groupSize == 0)
                            {
                                break;
                            }
 
                            bufferSize += sGroup.Length;
                            if (groupSizeIndex < groupDigits.Length - 1)
                            {
                                groupSizeIndex++;
                            }
 
                            groupSizeCount += groupDigits[groupSizeIndex];
                            ArgumentOutOfRangeException.ThrowIfNegative(groupSizeCount | bufferSize, string.Empty); // If we overflow
                        }
 
                        groupSize = groupSizeCount == 0 ? 0 : groupDigits[0]; // If you passed in an array with one entry as 0, groupSizeCount == 0
                    }
 
                    groupSizeIndex = 0;
                    int digitCount = 0;
                    int digLength = number.DigitsCount;
                    int digStart = (digPos < digLength) ? digPos : digLength;
                    fixed (TChar* spanPtr = &MemoryMarshal.GetReference(vlb.AppendSpan(bufferSize)))
                    {
                        TChar* p = spanPtr + bufferSize - 1;
                        for (int i = digPos - 1; i >= 0; i--)
                        {
                            *(p--) = TChar.CastFrom((i < digStart) ? (char)dig[i] : '0');
 
                            if (groupSize > 0)
                            {
                                digitCount++;
                                if ((digitCount == groupSize) && (i != 0))
                                {
                                    for (int j = sGroup.Length - 1; j >= 0; j--)
                                    {
                                        *(p--) = sGroup[j];
                                    }
 
                                    if (groupSizeIndex < groupDigits.Length - 1)
                                    {
                                        groupSizeIndex++;
                                        groupSize = groupDigits[groupSizeIndex];
                                    }
                                    digitCount = 0;
                                }
                            }
                        }
 
                        Debug.Assert(p >= spanPtr - 1, "Underflow");
                        dig += digStart;
                    }
                }
                else
                {
                    do
                    {
                        vlb.Append(TChar.CastFrom(*dig != 0 ? (char)(*dig++) : '0'));
                    }
                    while (--digPos > 0);
                }
            }
            else
            {
                vlb.Append(TChar.CastFrom('0'));
            }
 
            if (nMaxDigits > 0)
            {
                vlb.Append(sDecimal);
                if ((digPos < 0) && (nMaxDigits > 0))
                {
                    int zeroes = Math.Min(-digPos, nMaxDigits);
                    for (int i = 0; i < zeroes; i++)
                    {
                        vlb.Append(TChar.CastFrom('0'));
                    }
                    digPos += zeroes;
                    nMaxDigits -= zeroes;
                }
 
                while (nMaxDigits > 0)
                {
                    vlb.Append(TChar.CastFrom((*dig != 0) ? (char)(*dig++) : '0'));
                    nMaxDigits--;
                }
            }
        }
 
        /// <summary>Appends a char to the builder when the char is not known to be ASCII.</summary>
        /// <remarks>This requires a helper as if the character isn't ASCII, for UTF-8 encoding it will result in multiple bytes added.</remarks>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static unsafe void AppendUnknownChar<TChar>(ref ValueListBuilder<TChar> vlb, char ch) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(sizeof(TChar) == sizeof(char) || sizeof(TChar) == sizeof(byte));
 
            if (sizeof(TChar) == sizeof(char) || char.IsAscii(ch))
            {
                vlb.Append(TChar.CastFrom(ch));
            }
            else
            {
                AppendNonAsciiBytes(ref vlb, ch);
            }
 
            [MethodImpl(MethodImplOptions.NoInlining)]
            static void AppendNonAsciiBytes(ref ValueListBuilder<TChar> vlb, char ch)
            {
                var r = new Rune(ch);
                r.EncodeToUtf8(MemoryMarshal.AsBytes(vlb.AppendSpan(r.Utf8SequenceLength)));
            }
        }
 
        private static unsafe void FormatNumber<TChar>(ref ValueListBuilder<TChar> vlb, ref NumberBuffer number, int nMaxDigits, NumberFormatInfo info) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(sizeof(TChar) == sizeof(char) || sizeof(TChar) == sizeof(byte));
 
            string fmt = number.IsNegative ?
                s_negNumberFormats[info.NumberNegativePattern] :
                PosNumberFormat;
 
            foreach (char ch in fmt)
            {
                switch (ch)
                {
                    case '#':
                        FormatFixed(ref vlb, ref number, nMaxDigits, info.NumberGroupSizes(), info.NumberDecimalSeparatorTChar<TChar>(), info.NumberGroupSeparatorTChar<TChar>());
                        break;
 
                    case '-':
                        vlb.Append(info.NegativeSignTChar<TChar>());
                        break;
 
                    default:
                        vlb.Append(TChar.CastFrom(ch));
                        break;
                }
            }
        }
 
        private static unsafe void FormatScientific<TChar>(ref ValueListBuilder<TChar> vlb, ref NumberBuffer number, int nMaxDigits, NumberFormatInfo info, char expChar) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(sizeof(TChar) == sizeof(char) || sizeof(TChar) == sizeof(byte));
 
            byte* dig = number.DigitsPtr;
 
            vlb.Append(TChar.CastFrom((*dig != 0) ? (char)(*dig++) : '0'));
 
            if (nMaxDigits != 1) // For E0 we would like to suppress the decimal point
            {
                vlb.Append(info.NumberDecimalSeparatorTChar<TChar>());
            }
 
            while (--nMaxDigits > 0)
            {
                vlb.Append(TChar.CastFrom((*dig != 0) ? (char)(*dig++) : '0'));
            }
 
            int e = number.Digits[0] == 0 ? 0 : number.Scale - 1;
            FormatExponent(ref vlb, info, e, expChar, 3, true);
        }
 
        private static unsafe void FormatExponent<TChar>(ref ValueListBuilder<TChar> vlb, NumberFormatInfo info, int value, char expChar, int minDigits, bool positiveSign) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(sizeof(TChar) == sizeof(char) || sizeof(TChar) == sizeof(byte));
 
            vlb.Append(TChar.CastFrom(expChar));
 
            if (value < 0)
            {
                vlb.Append(info.NegativeSignTChar<TChar>());
                value = -value;
            }
            else
            {
                if (positiveSign)
                {
                    vlb.Append(info.PositiveSignTChar<TChar>());
                }
            }
 
            TChar* digits = stackalloc TChar[MaxUInt32DecDigits];
            TChar* p = UInt32ToDecChars(digits + MaxUInt32DecDigits, (uint)value, minDigits);
            vlb.Append(new ReadOnlySpan<TChar>(p, (int)(digits + MaxUInt32DecDigits - p)));
        }
 
        private static unsafe void FormatGeneral<TChar>(ref ValueListBuilder<TChar> vlb, ref NumberBuffer number, int nMaxDigits, NumberFormatInfo info, char expChar, bool suppressScientific) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(sizeof(TChar) == sizeof(char) || sizeof(TChar) == sizeof(byte));
 
            int digPos = number.Scale;
            bool scientific = false;
 
            if (!suppressScientific)
            {
                // Don't switch to scientific notation
                if (digPos > nMaxDigits || digPos < -3)
                {
                    digPos = 1;
                    scientific = true;
                }
            }
 
            byte* dig = number.DigitsPtr;
 
            if (digPos > 0)
            {
                do
                {
                    vlb.Append(TChar.CastFrom((*dig != 0) ? (char)(*dig++) : '0'));
                }
                while (--digPos > 0);
            }
            else
            {
                vlb.Append(TChar.CastFrom('0'));
            }
 
            if (*dig != 0 || digPos < 0)
            {
                vlb.Append(info.NumberDecimalSeparatorTChar<TChar>());
 
                while (digPos < 0)
                {
                    vlb.Append(TChar.CastFrom('0'));
                    digPos++;
                }
 
                while (*dig != 0)
                {
                    vlb.Append(TChar.CastFrom(*dig++));
                }
            }
 
            if (scientific)
            {
                FormatExponent(ref vlb, info, number.Scale - 1, expChar, 2, true);
            }
        }
 
        private static unsafe void FormatPercent<TChar>(ref ValueListBuilder<TChar> vlb, ref NumberBuffer number, int nMaxDigits, NumberFormatInfo info) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(sizeof(TChar) == sizeof(char) || sizeof(TChar) == sizeof(byte));
 
            string fmt = number.IsNegative ?
                s_negPercentFormats[info.PercentNegativePattern] :
                s_posPercentFormats[info.PercentPositivePattern];
 
            foreach (char ch in fmt)
            {
                switch (ch)
                {
                    case '#':
                        FormatFixed(ref vlb, ref number, nMaxDigits, info.PercentGroupSizes(), info.PercentDecimalSeparatorTChar<TChar>(), info.PercentGroupSeparatorTChar<TChar>());
                        break;
 
                    case '-':
                        vlb.Append(info.NegativeSignTChar<TChar>());
                        break;
 
                    case '%':
                        vlb.Append(info.PercentSymbolTChar<TChar>());
                        break;
 
                    default:
                        vlb.Append(TChar.CastFrom(ch));
                        break;
                }
            }
        }
 
        internal static unsafe void RoundNumber(ref NumberBuffer number, int pos, bool isCorrectlyRounded)
        {
            byte* dig = number.DigitsPtr;
 
            int i = 0;
            while (i < pos && dig[i] != '\0')
            {
                i++;
            }
 
            if ((i == pos) && ShouldRoundUp(dig, i, number.Kind, isCorrectlyRounded))
            {
                while (i > 0 && dig[i - 1] == '9')
                {
                    i--;
                }
 
                if (i > 0)
                {
                    dig[i - 1]++;
                }
                else
                {
                    number.Scale++;
                    dig[0] = (byte)('1');
                    i = 1;
                }
            }
            else
            {
                while (i > 0 && dig[i - 1] == '0')
                {
                    i--;
                }
            }
 
            if (i == 0)
            {
                if (number.Kind != NumberBufferKind.FloatingPoint)
                {
                    // The integer types don't have a concept of -0 and decimal always format -0 as 0
                    number.IsNegative = false;
                }
                number.Scale = 0;      // Decimals with scale ('0.00') should be rounded.
            }
 
            dig[i] = (byte)('\0');
            number.DigitsCount = i;
            number.CheckConsistency();
 
            static bool ShouldRoundUp(byte* dig, int i, NumberBufferKind numberKind, bool isCorrectlyRounded)
            {
                // We only want to round up if the digit is greater than or equal to 5 and we are
                // not rounding a floating-point number. If we are rounding a floating-point number
                // we have one of two cases.
                //
                // In the case of a standard numeric-format specifier, the exact and correctly rounded
                // string will have been produced. In this scenario, pos will have pointed to the
                // terminating null for the buffer and so this will return false.
                //
                // However, in the case of a custom numeric-format specifier, we currently fall back
                // to generating Single/DoublePrecisionCustomFormat digits and then rely on this
                // function to round correctly instead. This can unfortunately lead to double-rounding
                // bugs but is the best we have right now due to back-compat concerns.
 
                byte digit = dig[i];
 
                if ((digit == '\0') || isCorrectlyRounded)
                {
                    // Fast path for the common case with no rounding
                    return false;
                }
 
                // Values greater than or equal to 5 should round up, otherwise we round down. The IEEE
                // 754 spec actually dictates that ties (exactly 5) should round to the nearest even number
                // but that can have undesired behavior for custom numeric format strings. This probably
                // needs further thought for .NET 5 so that we can be spec compliant and so that users
                // can get the desired rounding behavior for their needs.
 
                return digit >= '5';
            }
        }
 
        private static unsafe int FindSection(ReadOnlySpan<char> format, int section)
        {
            int src;
            char ch;
 
            if (section == 0)
            {
                return 0;
            }
 
            fixed (char* pFormat = &MemoryMarshal.GetReference(format))
            {
                src = 0;
                while (true)
                {
                    if (src >= format.Length)
                    {
                        return 0;
                    }
 
                    switch (ch = pFormat[src++])
                    {
                        case '\'':
                        case '"':
                            while (src < format.Length && pFormat[src] != 0 && pFormat[src++] != ch) ;
                            break;
 
                        case '\\':
                            if (src < format.Length && pFormat[src] != 0)
                            {
                                src++;
                            }
                            break;
 
                        case ';':
                            if (--section != 0)
                            {
                                break;
                            }
 
                            if (src < format.Length && pFormat[src] != 0 && pFormat[src] != ';')
                            {
                                return src;
                            }
                            goto case '\0';
 
                        case '\0':
                            return 0;
                    }
                }
            }
        }
 
#if SYSTEM_PRIVATE_CORELIB
        private static int[] NumberGroupSizes(this NumberFormatInfo info) => info._numberGroupSizes;
 
        private static int[] CurrencyGroupSizes(this NumberFormatInfo info) => info._currencyGroupSizes;
 
        private static int[] PercentGroupSizes(this NumberFormatInfo info) => info._percentGroupSizes;
#else

        private static int[] NumberGroupSizes(this NumberFormatInfo info) => info.NumberGroupSizes;
 
        private static int[] CurrencyGroupSizes(this NumberFormatInfo info) => info.CurrencyGroupSizes;
 
        private static int[] PercentGroupSizes(this NumberFormatInfo info) => info.PercentGroupSizes;
#endif
    }
}