File: src\runtime\src\libraries\Common\src\System\Number.Formatting.Common.cs
Web Access
Project: src\runtime\src\libraries\System.Runtime.Numerics\src\System.Runtime.Numerics.csproj (System.Runtime.Numerics)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers.Text;
using System.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 static ReadOnlySpan<byte> GetCurrencyFormat(bool isNegative, int index)
        {
            if (isNegative)
            {
                return index switch
                {
                    0 => "($#)"u8,
                    1 => "-$#"u8,
                    2 => "$-#"u8,
                    3 => "$#-"u8,
                    4 => "(#$)"u8,
                    5 => "-#$"u8,
                    6 => "#-$"u8,
                    7 => "#$-"u8,
                    8 => "-# $"u8,
                    9 => "-$ #"u8,
                    10 => "# $-"u8,
                    11 => "$ #-"u8,
                    12 => "$ -#"u8,
                    13 => "#- $"u8,
                    14 => "($ #)"u8,
                    15 => "(# $)"u8,
                    16 => "$- #"u8,
                    _ => throw new UnreachableException(),
                };
            }

            return index switch
            {
                0 => "$#"u8,
                1 => "#$"u8,
                2 => "$ #"u8,
                3 => "# $"u8,
                _ => throw new UnreachableException(),
            };
        }

        private static ReadOnlySpan<byte> GetPercentFormat(bool isNegative, int index)
        {
            if (isNegative)
            {
                return index switch
                {
                    0 => "-# %"u8,
                    1 => "-#%"u8,
                    2 => "-%#"u8,
                    3 => "%-#"u8,
                    4 => "%#-"u8,
                    5 => "#-%"u8,
                    6 => "#%-"u8,
                    7 => "-% #"u8,
                    8 => "# %-"u8,
                    9 => "% #-"u8,
                    10 => "% -#"u8,
                    11 => "#- %"u8,
                    _ => throw new UnreachableException(),
                };
            }

            return index switch
            {
                0 => "# %"u8,
                1 => "#%"u8,
                2 => "%#"u8,
                3 => "% #"u8,
                _ => throw new UnreachableException(),
            };
        }

        private static ReadOnlySpan<byte> GetNumberFormat(bool isNegative, int index)
        {
            if (!isNegative)
            {
                return "#"u8;
            }

            return index switch
            {
                0 => "(#)"u8,
                1 => "-#"u8,
                2 => "- #"u8,
                3 => "#-"u8,
                4 => "# -"u8,
                _ => throw new UnreachableException(),
            };
        }

        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';
        }

        // Optimizations using "TwoDigits" inspired by:
        // https://engineering.fb.com/2013/03/15/developer-tools/three-optimization-tips-for-c/
        // entry[v] = (byte)('0' + v/10) | ((byte)('0' + v%10) << 8), for writing two UTF-8 bytes as a single 2-byte store
        private static ReadOnlySpan<ushort> TwoDigitsBytesTable =>
        [
            0x3030, 0x3130, 0x3230, 0x3330, 0x3430, 0x3530, 0x3630, 0x3730, 0x3830, 0x3930,
            0x3031, 0x3131, 0x3231, 0x3331, 0x3431, 0x3531, 0x3631, 0x3731, 0x3831, 0x3931,
            0x3032, 0x3132, 0x3232, 0x3332, 0x3432, 0x3532, 0x3632, 0x3732, 0x3832, 0x3932,
            0x3033, 0x3133, 0x3233, 0x3333, 0x3433, 0x3533, 0x3633, 0x3733, 0x3833, 0x3933,
            0x3034, 0x3134, 0x3234, 0x3334, 0x3434, 0x3534, 0x3634, 0x3734, 0x3834, 0x3934,
            0x3035, 0x3135, 0x3235, 0x3335, 0x3435, 0x3535, 0x3635, 0x3735, 0x3835, 0x3935,
            0x3036, 0x3136, 0x3236, 0x3336, 0x3436, 0x3536, 0x3636, 0x3736, 0x3836, 0x3936,
            0x3037, 0x3137, 0x3237, 0x3337, 0x3437, 0x3537, 0x3637, 0x3737, 0x3837, 0x3937,
            0x3038, 0x3138, 0x3238, 0x3338, 0x3438, 0x3538, 0x3638, 0x3738, 0x3838, 0x3938,
            0x3039, 0x3139, 0x3239, 0x3339, 0x3439, 0x3539, 0x3639, 0x3739, 0x3839, 0x3939,
        ];

        // entry[v] = (char)('0' + v/10) | ((char)('0' + v%10) << 16), for writing two UTF-16 chars as a single 4-byte store
        private static ReadOnlySpan<uint> TwoDigitsCharsTable =>
        [
            0x00300030u, 0x00310030u, 0x00320030u, 0x00330030u, 0x00340030u, 0x00350030u, 0x00360030u, 0x00370030u, 0x00380030u, 0x00390030u,
            0x00300031u, 0x00310031u, 0x00320031u, 0x00330031u, 0x00340031u, 0x00350031u, 0x00360031u, 0x00370031u, 0x00380031u, 0x00390031u,
            0x00300032u, 0x00310032u, 0x00320032u, 0x00330032u, 0x00340032u, 0x00350032u, 0x00360032u, 0x00370032u, 0x00380032u, 0x00390032u,
            0x00300033u, 0x00310033u, 0x00320033u, 0x00330033u, 0x00340033u, 0x00350033u, 0x00360033u, 0x00370033u, 0x00380033u, 0x00390033u,
            0x00300034u, 0x00310034u, 0x00320034u, 0x00330034u, 0x00340034u, 0x00350034u, 0x00360034u, 0x00370034u, 0x00380034u, 0x00390034u,
            0x00300035u, 0x00310035u, 0x00320035u, 0x00330035u, 0x00340035u, 0x00350035u, 0x00360035u, 0x00370035u, 0x00380035u, 0x00390035u,
            0x00300036u, 0x00310036u, 0x00320036u, 0x00330036u, 0x00340036u, 0x00350036u, 0x00360036u, 0x00370036u, 0x00380036u, 0x00390036u,
            0x00300037u, 0x00310037u, 0x00320037u, 0x00330037u, 0x00340037u, 0x00350037u, 0x00360037u, 0x00370037u, 0x00380037u, 0x00390037u,
            0x00300038u, 0x00310038u, 0x00320038u, 0x00330038u, 0x00340038u, 0x00350038u, 0x00360038u, 0x00370038u, 0x00380038u, 0x00390038u,
            0x00300039u, 0x00310039u, 0x00320039u, 0x00330039u, 0x00340039u, 0x00350039u, 0x00360039u, 0x00370039u, 0x00380039u, 0x00390039u,
        ];

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static ushort GetTwoDigitsBytes(uint value)
        {
            ushort pair = TwoDigitsBytesTable[(int)value];
            if (!BitConverter.IsLittleEndian)
            {
                pair = (ushort)((pair << 8) | (pair >> 8));
            }
            return pair;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static uint GetTwoDigitsChars(uint value)
        {
            uint pair = TwoDigitsCharsTable[(int)value];
            if (!BitConverter.IsLittleEndian)
            {
                pair = uint.RotateRight(pair, 16);
            }
            return pair;
        }

        /// <summary>Writes a value [ 00 .. 99 ] to the start of a pre-sliced 2-element span, using a single store.</summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void WriteTwoDigits<TChar>(uint value, Span<TChar> destination) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(value <= 99);
            Debug.Assert(destination.Length >= 2);
            Debug.Assert(sizeof(TChar) is sizeof(char) or sizeof(byte));

            if (sizeof(TChar) == sizeof(char))
            {
                // TwoDigitsCharsTable[v] = (char)('0'+v/10) | ((char)('0'+v%10) << 16) — write both chars as one 4-byte store.
                uint pair = GetTwoDigitsChars(value);
                MemoryMarshal.Write(MemoryMarshal.AsBytes(Unsafe.BitCast<Span<TChar>, Span<char>>(destination)), in pair);
            }
            else
            {
                // Write both bytes as a single 2-byte store.
                ushort pair = GetTwoDigitsBytes(value);
                MemoryMarshal.Write(Unsafe.BitCast<Span<TChar>, Span<byte>>(destination), in pair);
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void WriteTwoDigits<TChar>(uint value, Span<TChar> buffer, int index) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(value <= 99);
            WriteTwoDigits(value, buffer.Slice(index, 2));
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void CopyNegativeSign<TChar>(ReadOnlySpan<TChar> sign, Span<TChar> destination)
        {
            if (sign.Length == 1)
            {
                destination[0] = sign[0];
            }
            else
            {
                sign.CopyTo(destination);
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static int UInt32ToDecChars<TChar>(Span<TChar> buffer, int index, uint value) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(sizeof(TChar) is sizeof(char) or sizeof(byte));

            if (value >= 10)
            {
                // Handle all values >= 100 two-digits at a time so as to avoid expensive integer division operations.
                while (value >= 100)
                {
                    index -= 2;
                    (value, uint remainder) = Math.DivRem(value, 100);
                    WriteTwoDigits(remainder, buffer, index);
                }

                // If there are two digits remaining, store them.
                if (value >= 10)
                {
                    index -= 2;
                    WriteTwoDigits(value, buffer, index);
                    return index;
                }
            }

            // Otherwise, store the single digit remaining.
            buffer[--index] = TChar.CastFrom(value + '0');
            return index;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static int UInt32ToDecChars<TChar>(Span<TChar> buffer, int index, uint value, int digits) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(sizeof(TChar) is sizeof(char) or sizeof(byte));

            uint remainder;
            while (value >= 100)
            {
                index -= 2;
                digits -= 2;
                (value, remainder) = Math.DivRem(value, 100);
                WriteTwoDigits(remainder, buffer, index);
            }
            while (value != 0 || digits > 0)
            {
                digits--;
                (value, remainder) = Math.DivRem(value, 10);
                buffer[--index] = TChar.CastFrom(remainder + '0');
            }
            return index;
        }

        internal static 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) is sizeof(char) or 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 void NumberToStringFormat<TChar>(ref ValueListBuilder<TChar> vlb, ref NumberBuffer number, ReadOnlySpan<char> format, NumberFormatInfo info) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(sizeof(TChar) is sizeof(char) or 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;
            char ch;

            section = FindSection(format, number.Digits[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;

                while (src < format.Length && (ch = format[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 && format[src] != 0 && format[src++] != ch) ;
                            break;

                        case '\\':
                            if (src < format.Length && format[src] != 0)
                            {
                                src++;
                            }
                            break;

                        case 'E':
                        case 'e':
                            if ((src < format.Length && format[src] == '0') ||
                                (src + 1 < format.Length && (format[src] == '+' || format[src] == '-') && format[src + 1] == '0'))
                            {
                                while (++src < format.Length && format[src] == '0') ;
                                scientific = true;
                            }
                            break;
                    }
                }

                if (decimalPos < 0)
                {
                    decimalPos = digitCount;
                }

                if (thousandPos >= 0)
                {
                    if (thousandPos == decimalPos)
                    {
                        scaleAdjust -= thousandCount * 3;
                    }
                    else
                    {
                        thousandSeps = true;
                    }
                }

                if (number.Digits[0] != 0)
                {
                    number.Scale += scaleAdjust;
                    int pos = scientific ? digitCount : number.Scale + digitCount - decimalPos;
                    RoundNumber(ref number, pos, isCorrectlyRounded: false);
                    if (number.Digits[0] == 0)
                    {
                        src = FindSection(format, 2);
                        if (src != section)
                        {
                            section = src;
                            continue;
                        }
                    }
                }
                else
                {
                    if (number.Kind is not (NumberBufferKind.FloatingPoint or NumberBufferKind.DecimalIeee754))
                    {
                        // 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 = [0, 0, 0, 0];
            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;
                    }
                }
            }

            // A dedicated negative section (the portion after the first ';') is responsible for
            // emitting the sign of negative values. When a negative value rounds to zero -- or is
            // negative zero -- it can fall back to the first section (for example -0.001 or -0.0
            // with "+0.00;-0.00"). In that case the first section already contains the caller's
            // desired representation and we must not emit an extra sign, which would otherwise
            // produce output such as "-+0.00". This only matters when 'section == 0', so
            // 'HasNegativeSection' is evaluated lazily behind that check to avoid an extra format
            // scan on the common path where the negative section is used directly ('section != 0').
            if (number.IsNegative && (section == 0) && (number.Scale != 0) && !HasNegativeSection(format))
            {
                vlb.Append(info.NegativeSignTChar<TChar>());
            }

            bool decimalWritten = false;

            // Slicing to DigitsCount lets the JIT prove digits[i] is in-bounds whenever i < digits.Length.
            // Math.Min proves the Slice length is within the buffer so the JIT can eliminate the cold throw.
            // digits itself is never mutated — curIndex tracks our position so digits.Length remains
            // loop-invariant across the outer format scan, letting the JIT hoist it once.
            ReadOnlySpan<byte> digits = number.Digits;
            digits = digits.Slice(0, Math.Min(number.DigitsCount, digits.Length));
            int curIndex = 0;

            while (src < format.Length && (ch = format[src++]) != 0 && ch != ';')
            {
                if (adjust > 0)
                {
                    switch (ch)
                    {
                        case '#':
                        case '0':
                        case '.':
                            // Emit real digits for the first min(adjust, digits.Length) positions,
                            // then '0' padding for any remaining. The adjust loop always fires before
                            // any main-switch digit consumption (curIndex == 0 at entry), so
                            // Math.Min(adjust, digits.Length) is the tight bound, and iterating the
                            // slice itself lets the JIT eliminate the per-element bounds checks.
                            ReadOnlySpan<byte> adjustDigits = digits.Slice(0, Math.Min(adjust, digits.Length));
                            for (int i = 0; i < adjustDigits.Length; i++)
                            {
                                // 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((char)adjustDigits[i]));
                                if (thousandSeps && digPos > 1 && thousandsSepCtr >= 0)
                                {
                                    if (digPos == thousandsSepPos[thousandsSepCtr] + 1)
                                    {
                                        vlb.Append(info.NumberGroupSeparatorTChar<TChar>());
                                        thousandsSepCtr--;
                                    }
                                }
                                digPos--;
                                adjust--;
                            }
                            curIndex = adjustDigits.Length;
                            while (adjust > 0)
                            {
                                vlb.Append(TChar.CastFrom('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 if (curIndex < digits.Length)
                            {
                                ch = (char)digits[curIndex++];
                            }
                            else
                            {
                                ch = 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 && curIndex < digits.Length))
                            {
                                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)
                        {
                            char quoted = format[src];
                            if (quoted == 0 || quoted == ch)
                            {
                                break;
                            }
                            src++;
                            if (char.IsHighSurrogate(quoted) && src < format.Length && char.IsLowSurrogate(format[src]))
                            {
                                AppendSurrogatePair(ref vlb, quoted, format[src++]);
                            }
                            else
                            {
                                AppendUnknownChar(ref vlb, quoted);
                            }
                        }

                        if (src < format.Length && format[src] != 0)
                        {
                            src++;
                        }
                        break;

                    case '\\':
                        if (src < format.Length && format[src] != 0)
                        {
                            char literal = format[src++];
                            if (char.IsHighSurrogate(literal) && src < format.Length && char.IsLowSurrogate(format[src]))
                            {
                                AppendSurrogatePair(ref vlb, literal, format[src++]);
                            }
                            else
                            {
                                AppendUnknownChar(ref vlb, literal);
                            }
                        }
                        break;

                    case 'E':
                    case 'e':
                        {
                            bool positiveSign = false;
                            int i = 0;
                            if (scientific)
                            {
                                char exponentChar = src < format.Length ? format[src] : '\0';
                                char exponentNext = src + 1 < format.Length ? format[src + 1] : '\0';

                                if (exponentChar == '0')
                                {
                                    // Handles E0, which should format the same as E-0
                                    i++;
                                }
                                else if (exponentChar is '+' or '-' && exponentNext == '0')
                                {
                                    // Handles E+0 and E-0; only E+0 emits a sign for positive exponents
                                    positiveSign = exponentChar == '+';
                                }
                                else
                                {
                                    vlb.Append(TChar.CastFrom(ch));
                                    break;
                                }

                                while (++src < format.Length && format[src] == '0')
                                {
                                    i++;
                                }

                                if (i > 10)
                                {
                                    i = 10;
                                }

                                int exp = number.Digits[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 (format[src] is '+' or '-')
                                    {
                                        vlb.Append(TChar.CastFrom(format[src++]));
                                    }

                                    while (src < format.Length && format[src] == '0')
                                    {
                                        vlb.Append(TChar.CastFrom(format[src++]));
                                    }
                                }
                            }
                            break;
                        }

                    default:
                        if (char.IsHighSurrogate(ch) && src < format.Length && char.IsLowSurrogate(format[src]))
                        {
                            AppendSurrogatePair(ref vlb, ch, format[src++]);
                        }
                        else
                        {
                            AppendUnknownChar(ref vlb, ch);
                        }
                        break;
                }
            }

            if (number.IsNegative && (section == 0) && (number.Scale == 0) && (vlb.Length > 0) && !HasNegativeSection(format))
            {
                vlb.Insert(0, info.NegativeSignTChar<TChar>());
            }
        }

        private static void FormatCurrency<TChar>(ref ValueListBuilder<TChar> vlb, ref NumberBuffer number, int nMaxDigits, NumberFormatInfo info) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(sizeof(TChar) is sizeof(char) or sizeof(byte));

            ReadOnlySpan<byte> fmt = GetCurrencyFormat(
                number.IsNegative,
                number.IsNegative ? info.CurrencyNegativePattern : info.CurrencyPositivePattern);

            foreach (byte ch in fmt)
            {
                switch (ch)
                {
                    case (byte)'#':
                        FormatFixed(ref vlb, ref number, nMaxDigits, info.CurrencyGroupSizes(), info.CurrencyDecimalSeparatorTChar<TChar>(), info.CurrencyGroupSeparatorTChar<TChar>());
                        break;

                    case (byte)'-':
                        vlb.Append(info.NegativeSignTChar<TChar>());
                        break;

                    case (byte)'$':
                        vlb.Append(info.CurrencySymbolTChar<TChar>());
                        break;

                    default:
                        vlb.Append(TChar.CastFrom(ch));
                        break;
                }
            }
        }

        private static 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) is sizeof(char) or sizeof(byte));

            int digPos = number.Scale;
            ReadOnlySpan<byte> dig = number.Digits;
            dig = dig.Slice(0, Math.Min(number.DigitsCount, dig.Length));
            int digIndex = 0;

            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;
                    ReadOnlySpan<byte> intDigits = dig.Slice(0, Math.Min(digPos, dig.Length));
                    Span<TChar> buffer = vlb.AppendSpan(bufferSize);
                    int writePos = bufferSize;
                    int remainingDigits = digPos;

                    while (remainingDigits > 0)
                    {
                        int digitsInGroup = (groupSize > 0) ? Math.Min(groupSize, remainingDigits) : remainingDigits;
                        int groupStartDigit = remainingDigits - digitsInGroup;
                        int groupStartWrite = writePos - digitsInGroup;

                        Span<TChar> groupBuffer = buffer.Slice(groupStartWrite, digitsInGroup);
                        for (int j = 0; j < groupBuffer.Length; j++)
                        {
                            int digitIndex = groupStartDigit + j;
                            groupBuffer[j] = TChar.CastFrom((uint)digitIndex < (uint)intDigits.Length ? (char)intDigits[digitIndex] : '0');
                        }

                        writePos = groupStartWrite;
                        remainingDigits -= digitsInGroup;

                        if ((remainingDigits > 0) && (groupSize > 0))
                        {
                            if (sGroup.Length == 1)
                            {
                                writePos--;
                                buffer[writePos] = sGroup[0];
                            }
                            else
                            {
                                writePos -= sGroup.Length;
                                sGroup.CopyTo(buffer.Slice(writePos, sGroup.Length));
                            }

                            if (groupSizeIndex < groupDigits.Length - 1)
                            {
                                groupSizeIndex++;
                                groupSize = groupDigits[groupSizeIndex];
                            }
                        }
                    }

                    Debug.Assert(writePos == 0, "Underflow");
                    digIndex = intDigits.Length;
                }
                else
                {
                    // Emit actual digits first, then trailing zeros.
                    // Split into two unconditional loops so the JIT can prove bounds safety
                    // for the digit loop (span iteration) and fully optimize the zero loop.
                    int actualDigits = Math.Min(digPos, dig.Length);
                    foreach (byte d in dig.Slice(0, actualDigits))
                    {
                        vlb.Append(TChar.CastFrom((char)d));
                    }
                    digIndex = actualDigits;
                    digPos -= actualDigits;
                    while (digPos > 0)
                    {
                        vlb.Append(TChar.CastFrom('0'));
                        digPos--;
                    }
                }
            }
            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'));
                    }
                    nMaxDigits -= zeroes;
                }

                int remainingDig = dig.Length - digIndex;
                int decActual = Math.Min(nMaxDigits, remainingDig);
                foreach (byte d in dig.Slice(digIndex, decActual))
                {
                    vlb.Append(TChar.CastFrom((char)d));
                }
                nMaxDigits -= decActual;
                while (nMaxDigits > 0)
                {
                    vlb.Append(TChar.CastFrom('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 void AppendUnknownChar<TChar>(ref ValueListBuilder<TChar> vlb, char ch) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(sizeof(TChar) is sizeof(char) or 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)
            {
                Rune rune = Rune.TryCreate(ch, out Rune result) ? result : Rune.ReplacementChar;
                rune.EncodeToUtf8(MemoryMarshal.AsBytes(vlb.AppendSpan(rune.Utf8SequenceLength)));
            }
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        private static void AppendSurrogatePair<TChar>(ref ValueListBuilder<TChar> vlb, char highSurrogate, char lowSurrogate) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(sizeof(TChar) is sizeof(char) or sizeof(byte));
            Debug.Assert(char.IsSurrogatePair(highSurrogate, lowSurrogate));

            if (sizeof(TChar) == sizeof(char))
            {
                vlb.Append(TChar.CastFrom(highSurrogate));
                vlb.Append(TChar.CastFrom(lowSurrogate));
            }
            else
            {
                Rune rune = new(highSurrogate, lowSurrogate);
                rune.EncodeToUtf8(MemoryMarshal.AsBytes(vlb.AppendSpan(rune.Utf8SequenceLength)));
            }
        }

        private static void FormatNumber<TChar>(ref ValueListBuilder<TChar> vlb, ref NumberBuffer number, int nMaxDigits, NumberFormatInfo info) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(sizeof(TChar) is sizeof(char) or sizeof(byte));

            ReadOnlySpan<byte> fmt = GetNumberFormat(number.IsNegative, info.NumberNegativePattern);

            foreach (byte ch in fmt)
            {
                switch (ch)
                {
                    case (byte)'#':
                        FormatFixed(ref vlb, ref number, nMaxDigits, info.NumberGroupSizes(), info.NumberDecimalSeparatorTChar<TChar>(), info.NumberGroupSeparatorTChar<TChar>());
                        break;

                    case (byte)'-':
                        vlb.Append(info.NegativeSignTChar<TChar>());
                        break;

                    default:
                        vlb.Append(TChar.CastFrom(ch));
                        break;
                }
            }
        }

        private static 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) is sizeof(char) or sizeof(byte));

            ReadOnlySpan<byte> dig = number.Digits;
            dig = dig.Slice(0, Math.Min(number.DigitsCount, dig.Length));

            // Emit the leading digit, or '0' when the value has no digits.
            vlb.Append(TChar.CastFrom(!dig.IsEmpty ? (char)dig[0] : '0'));

            if (nMaxDigits != 1) // For E0 we would like to suppress the decimal point
            {
                vlb.Append(info.NumberDecimalSeparatorTChar<TChar>());
            }

            // Emit the remaining nMaxDigits - 1 digits, padding with '0' once exhausted.
            int emitted = 1;
            if (dig.Length > 1)
            {
                foreach (byte b in dig.Slice(1, Math.Min(nMaxDigits - 1, dig.Length - 1)))
                {
                    vlb.Append(TChar.CastFrom((char)b));
                    emitted++;
                }
            }
            for (; emitted < nMaxDigits; emitted++)
            {
                vlb.Append(TChar.CastFrom('0'));
            }

            int e = number.Digits[0] == 0 ? 0 : number.Scale - 1;
            FormatExponent(ref vlb, info, e, expChar, 3, true);
        }

        private static 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) is sizeof(char) or 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>());
                }
            }

            int digitCount = Math.Max(minDigits, FormattingHelpers.CountDigits((uint)value));
            Span<TChar> digits = vlb.AppendSpan(digitCount);
            int pos = UInt32ToDecChars(digits, digitCount, (uint)value, minDigits);
            Debug.Assert(pos == 0);
        }

        private static 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) is sizeof(char) or 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;
                }
            }

            ReadOnlySpan<byte> dig = number.Digits;
            dig = dig.Slice(0, Math.Min(number.DigitsCount, dig.Length));

            if (digPos > 0)
            {
                // Emit the available integer digits, then pad with '0' up to digPos.
                int intCount = Math.Min(digPos, dig.Length);
                foreach (byte b in dig.Slice(0, intCount))
                {
                    vlb.Append(TChar.CastFrom((char)b));
                }
                for (int i = intCount; i < digPos; i++)
                {
                    vlb.Append(TChar.CastFrom('0'));
                }
                dig = dig.Slice(intCount);
            }
            else
            {
                vlb.Append(TChar.CastFrom('0'));
            }

            if (!dig.IsEmpty || digPos < 0)
            {
                vlb.Append(info.NumberDecimalSeparatorTChar<TChar>());

                while (digPos < 0)
                {
                    vlb.Append(TChar.CastFrom('0'));
                    digPos++;
                }

                foreach (byte b in dig)
                {
                    vlb.Append(TChar.CastFrom((char)b));
                }
            }

            if (scientific)
            {
                FormatExponent(ref vlb, info, number.Scale - 1, expChar, 2, true);
            }
        }

        private static void FormatPercent<TChar>(ref ValueListBuilder<TChar> vlb, ref NumberBuffer number, int nMaxDigits, NumberFormatInfo info) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(sizeof(TChar) is sizeof(char) or sizeof(byte));

            ReadOnlySpan<byte> fmt = GetPercentFormat(
                number.IsNegative,
                number.IsNegative ? info.PercentNegativePattern : info.PercentPositivePattern);

            foreach (byte ch in fmt)
            {
                switch (ch)
                {
                    case (byte)'#':
                        FormatFixed(ref vlb, ref number, nMaxDigits, info.PercentGroupSizes(), info.PercentDecimalSeparatorTChar<TChar>(), info.PercentGroupSeparatorTChar<TChar>());
                        break;

                    case (byte)'-':
                        vlb.Append(info.NegativeSignTChar<TChar>());
                        break;

                    case (byte)'%':
                        vlb.Append(info.PercentSymbolTChar<TChar>());
                        break;

                    default:
                        vlb.Append(TChar.CastFrom(ch));
                        break;
                }
            }
        }

        internal static void RoundNumber(ref NumberBuffer number, int pos, bool isCorrectlyRounded)
        {
            Span<byte> dig = number.Digits;

            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 is not (NumberBufferKind.FloatingPoint or NumberBufferKind.DecimalIeee754))
                {
                    // 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(ReadOnlySpan<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;
                }

                if (numberKind == NumberBufferKind.DecimalIeee754)
                {
                    // The buffer holds the exact coefficient, so a '5' followed by nothing but zeros is a
                    // true tie rather than an artifact of a truncated expansion. IEEE 754 §5.12.1 requires
                    // the conversion to be correctly rounded under the applicable rounding-direction
                    // attribute, which is roundTiesToEven.

                    if (digit != '5')
                    {
                        return digit > '5';
                    }

                    for (int j = i + 1; dig[j] != '\0'; j++)
                    {
                        if (dig[j] != '0')
                        {
                            return true;
                        }
                    }

                    // A tie with no preceding digit rounds toward the implicit leading zero, which is even.
                    return (i > 0) && (((dig[i - 1] - '0') & 1) != 0);
                }

                // 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';
            }
        }

        // A distinct negative section always begins after the first ';', so its offset is > 0.
        // FindSection returns 0 both for the first section and when no such section exists, so a
        // non-zero result reliably indicates the format defines a dedicated negative section.
        private static bool HasNegativeSection(ReadOnlySpan<char> format) => FindSection(format, 1) != 0;

        private static int FindSection(ReadOnlySpan<char> format, int section)
        {
            int src;
            char ch;

            if (section == 0)
            {
                return 0;
            }

            src = 0;
            while (true)
            {
                if (src >= format.Length)
                {
                    return 0;
                }

                switch (ch = format[src++])
                {
                    case '\'':
                    case '"':
                        while (src < format.Length && format[src] != 0 && format[src++] != ch) ;
                        break;

                    case '\\':
                        if (src < format.Length && format[src] != 0)
                        {
                            src++;
                        }
                        break;

                    case ';':
                        if (--section != 0)
                        {
                            break;
                        }

                        if (src < format.Length && format[src] is not ('\0' or ';'))
                        {
                            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
    }
}