File: src\runtime\src\libraries\Common\src\System\Number.Parsing.Common.cs
Web Access
Project: src\runtime\src\coreclr\nativeaot\System.Private.CoreLib\src\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.Diagnostics;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace System
{
    internal static partial class Number
    {
        // Internal-only style bit used by the INumberBase.TryParsePartial implementations to signal that parsing
        // should stop at the first otherwise-invalid character rather than failing. This is deliberately not a
        // public NumberStyles value; it is layered on top of the user-provided style after validation. It uses the
        // highest bit (0x8000_0000) so public flags can keep growing upward without stomping it; see NumberStyles.
        internal const NumberStyles AllowTrailingInvalidCharacters = unchecked((NumberStyles)0x80000000);

        private static unsafe bool TryParseNumber<TChar>(TChar* str, TChar* strEnd, NumberStyles styles, ref NumberBuffer number, NumberFormatInfo info, out int elementsConsumed)
            where TChar : unmanaged, IUtfChar<TChar>
        {
            // str/strEnd may be null when the input is an empty span (e.g. default(ReadOnlySpan<TChar>)
            // originating from a null string), in which case they are both null and the range is empty.
            Debug.Assert((str != null) || (str == strEnd));
            Debug.Assert((strEnd != null) || (str == strEnd));
            Debug.Assert(str <= strEnd);
            Debug.Assert((styles & (NumberStyles.AllowHexSpecifier | NumberStyles.AllowBinarySpecifier)) == 0);

            const int StateSign = 0x0001;
            const int StateParens = 0x0002;
            const int StateDigits = 0x0004;
            const int StateNonZero = 0x0008;
            const int StateDecimal = 0x0010;
            const int StateCurrency = 0x0020;

            Debug.Assert(number.DigitsCount == 0);
            Debug.Assert(number.Scale == 0);
            Debug.Assert(!number.IsNegative);
            Debug.Assert(!number.HasNonZeroTail);

            number.CheckConsistency();

            ReadOnlySpan<TChar> decSep;                                 // decimal separator from NumberFormatInfo.
            ReadOnlySpan<TChar> groupSep;                               // group separator from NumberFormatInfo.
            ReadOnlySpan<TChar> currSymbol = ReadOnlySpan<TChar>.Empty; // currency symbol from NumberFormatInfo.

            bool parsingCurrency = false;
            if ((styles & NumberStyles.AllowCurrencySymbol) != 0)
            {
                currSymbol = info.CurrencySymbolTChar<TChar>();

                // The idea here is to match the currency separators and on failure match the number separators to keep the perf of VB's IsNumeric fast.
                // The values of decSep are setup to use the correct relevant separator (currency in the if part and decimal in the else part).
                decSep = info.CurrencyDecimalSeparatorTChar<TChar>();
                groupSep = info.CurrencyGroupSeparatorTChar<TChar>();
                parsingCurrency = true;
            }
            else
            {
                decSep = info.NumberDecimalSeparatorTChar<TChar>();
                groupSep = info.NumberGroupSeparatorTChar<TChar>();
            }

            int state = 0;
            TChar* p = str;
            uint ch = (p < strEnd) ? TChar.CastToUInt32(*p) : '\0';
            TChar* next;

            while (true)
            {
                // Eat whitespace unless we've found a sign which isn't followed by a currency symbol.
                // "-Kr 1231.47" is legal but "- 1231.47" is not.
                if (!IsWhite(ch) || (styles & NumberStyles.AllowLeadingWhite) == 0 || ((state & StateSign) != 0 && (state & StateCurrency) == 0 && info.NumberNegativePattern != 2))
                {
                    if (((styles & NumberStyles.AllowLeadingSign) != 0) && (state & StateSign) == 0 && ((next = MatchChars(p, strEnd, info.PositiveSignTChar<TChar>())) != null || ((next = MatchNegativeSignChars(p, strEnd, info)) != null && (number.IsNegative = true))))
                    {
                        state |= StateSign;
                        p = next - 1;
                    }
                    else if (ch == '(' && ((styles & NumberStyles.AllowParentheses) != 0) && ((state & StateSign) == 0))
                    {
                        state |= StateSign | StateParens;
                        number.IsNegative = true;
                    }
                    else if (!currSymbol.IsEmpty && (next = MatchChars(p, strEnd, currSymbol)) != null)
                    {
                        state |= StateCurrency;
                        currSymbol = ReadOnlySpan<TChar>.Empty;
                        // We already found the currency symbol. There should not be more currency symbols. Set
                        // currSymbol to NULL so that we won't search it again in the later code path.
                        p = next - 1;
                    }
                    else
                    {
                        break;
                    }
                }
                ch = ++p < strEnd ? TChar.CastToUInt32(*p) : '\0';
            }

            int digCount = 0;
            int digEnd = 0;
            int maxDigCount = number.Digits.Length - 1;
            int numberOfTrailingZeros = 0;

            while (true)
            {
                if (IsDigit(ch))
                {
                    state |= StateDigits;

                    if (ch != '0' || (state & StateNonZero) != 0)
                    {
                        if (digCount < maxDigCount)
                        {
                            number.Digits[digCount] = (byte)ch;
                            if ((ch != '0') || (number.Kind != NumberBufferKind.Integer))
                            {
                                digEnd = digCount + 1;
                            }
                        }
                        else if (ch != '0')
                        {
                            // For decimal and binary floating-point numbers, we only
                            // need to store digits up to maxDigCount. However, we still
                            // need to keep track of whether any additional digits past
                            // maxDigCount were non-zero, as that can impact rounding
                            // for an input that falls evenly between two representable
                            // results.

                            number.HasNonZeroTail = true;
                        }

                        if ((state & StateDecimal) == 0)
                        {
                            number.Scale++;
                        }

                        if (digCount < maxDigCount)
                        {
                            // Handle a case like "53.0". We need to ignore trailing zeros in the fractional part for floating point numbers, so we keep a count of the number of trailing zeros and update digCount later
                            if (ch == '0')
                            {
                                numberOfTrailingZeros++;
                            }
                            else
                            {
                                numberOfTrailingZeros = 0;
                            }
                        }
                        digCount++;
                        state |= StateNonZero;
                    }
                    else if ((state & StateDecimal) != 0)
                    {
                        number.Scale--;
                    }
                }
                else if (((styles & NumberStyles.AllowDecimalPoint) != 0) && ((state & StateDecimal) == 0) && ((next = MatchChars(p, strEnd, decSep)) != null || (parsingCurrency && (state & StateCurrency) == 0 && (next = MatchChars(p, strEnd, info.NumberDecimalSeparatorTChar<TChar>())) != null)))
                {
                    state |= StateDecimal;
                    p = next - 1;
                }
                else if (((styles & NumberStyles.AllowThousands) != 0) && ((state & StateDigits) != 0) && ((state & StateDecimal) == 0) && ((next = MatchChars(p, strEnd, groupSep)) != null || (parsingCurrency && (state & StateCurrency) == 0 && (next = MatchChars(p, strEnd, info.NumberGroupSeparatorTChar<TChar>())) != null)))
                {
                    p = next - 1;
                }
                else
                {
                    break;
                }
                ch = ++p < strEnd ? TChar.CastToUInt32(*p) : '\0';
            }

            bool negExp = false;
            number.DigitsCount = digEnd;
            number.Digits[digEnd] = (byte)'\0';
            if ((state & StateDigits) != 0)
            {
                if ((ch == 'E' || ch == 'e') && ((styles & NumberStyles.AllowExponent) != 0))
                {
                    TChar* temp = p;
                    ch = ++p < strEnd ? TChar.CastToUInt32(*p) : '\0';
                    if ((next = MatchChars(p, strEnd, info.PositiveSignTChar<TChar>())) != null)
                    {
                        ch = (p = next) < strEnd ? TChar.CastToUInt32(*p) : '\0';
                    }
                    else if ((next = MatchNegativeSignChars(p, strEnd, info)) != null)
                    {
                        ch = (p = next) < strEnd ? TChar.CastToUInt32(*p) : '\0';
                        negExp = true;
                    }
                    if (IsDigit(ch))
                    {
                        int exp = 0;
                        do
                        {
                            // Check if we are about to overflow past our limit of 9 digits
                            if (exp >= 100_000_000)
                            {
                                // Set exp to Int.MaxValue to signify the requested exponent is too large. This will lead to an OverflowException later.
                                exp = int.MaxValue;
                                number.Scale = 0;

                                // Finish parsing the number, a FormatException could still occur later on.
                                while (IsDigit(ch))
                                {
                                    ch = ++p < strEnd ? TChar.CastToUInt32(*p) : '\0';
                                }
                                break;
                            }

                            exp = (exp * 10) + (int)(ch - '0');
                            ch = ++p < strEnd ? TChar.CastToUInt32(*p) : '\0';
                        } while (IsDigit(ch));
                        if (negExp)
                        {
                            exp = -exp;
                        }
                        number.Scale += exp;
                    }
                    else
                    {
                        p = temp;
                        ch = p < strEnd ? TChar.CastToUInt32(*p) : '\0';
                    }
                }

                if (number.Kind == NumberBufferKind.FloatingPoint && !number.HasNonZeroTail)
                {
                    // Adjust the number buffer for trailing zeros
                    int numberOfFractionalDigits = digEnd - number.Scale;
                    if (numberOfFractionalDigits > 0)
                    {
                        numberOfTrailingZeros = Math.Min(numberOfTrailingZeros, numberOfFractionalDigits);
                        Debug.Assert(numberOfTrailingZeros >= 0);
                        number.DigitsCount = digEnd - numberOfTrailingZeros;
                        number.Digits[number.DigitsCount] = (byte)'\0';
                    }
                }

                while (true)
                {
                    if (!IsWhite(ch) || (styles & NumberStyles.AllowTrailingWhite) == 0)
                    {
                        if ((styles & NumberStyles.AllowTrailingSign) != 0 && ((state & StateSign) == 0) && ((next = MatchChars(p, strEnd, info.PositiveSignTChar<TChar>())) != null || (((next = MatchNegativeSignChars(p, strEnd, info)) != null) && (number.IsNegative = true))))
                        {
                            state |= StateSign;
                            p = next - 1;
                        }
                        else if (ch == ')' && ((state & StateParens) != 0))
                        {
                            state &= ~StateParens;
                        }
                        else if (!currSymbol.IsEmpty && (next = MatchChars(p, strEnd, currSymbol)) != null)
                        {
                            currSymbol = ReadOnlySpan<TChar>.Empty;
                            p = next - 1;
                        }
                        else
                        {
                            break;
                        }
                    }
                    ch = ++p < strEnd ? TChar.CastToUInt32(*p) : '\0';
                }
                if ((state & StateParens) == 0)
                {
                    if ((state & StateNonZero) == 0)
                    {
                        if (number.Kind is not (NumberBufferKind.Decimal or NumberBufferKind.DecimalIeee754))
                        {
                            number.Scale = 0;
                        }
                        if ((number.Kind == NumberBufferKind.Integer) && (state & StateDecimal) == 0)
                        {
                            number.IsNegative = false;
                        }
                    }

                    int index = (int)(p - str);
                    var value = new ReadOnlySpan<TChar>(str, (int)(strEnd - str));

                    // For compatibility we still need to process any trailing
                    // nulls that exist and report them as having been consumed.

                    index = ConsumeTrailingNulls(value, index);

                    if ((index == value.Length) || ((styles & AllowTrailingInvalidCharacters) != 0))
                    {
                        elementsConsumed = index;
                        return true;
                    }
                }
            }

            elementsConsumed = 0;
            return false;
        }

        internal static unsafe bool TryStringToNumber<TChar>(ReadOnlySpan<TChar> value, NumberStyles styles, ref NumberBuffer number, NumberFormatInfo info, out int elementsConsumed)
            where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(info != null);

            fixed (TChar* stringPointer = &MemoryMarshal.GetReference(value))
            {
                bool succeeded = TryParseNumber(stringPointer, stringPointer + value.Length, styles, ref number, info, out elementsConsumed);
                number.CheckConsistency();
                return succeeded;
            }
        }

        private static int ConsumeTrailingNulls<TChar>(ReadOnlySpan<TChar> value, int index)
            where TChar : unmanaged, IUtfChar<TChar>
        {
            // For compatibility, we need to allow trailing nulls at the end of a number string
            var remainder = value.Slice(index);

            var nullsToConsume = remainder.IndexOfAnyExcept(TChar.CastFrom('\0'));
            return index + ((nullsToConsume >= 0) ? nullsToConsume : remainder.Length);
        }

        private static bool IsWhite(uint ch) => (ch == 0x20) || ((ch - 0x09) <= (0x0D - 0x09));

        private static bool IsDigit(uint ch) => (ch - '0') <= 9;

        internal enum ParsingStatus
        {
            OK,
            Failed,
            Overflow
        }

        private static bool IsSpaceReplacingChar(uint c) => c is '\u00a0' or '\u202f';

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static uint NormalizeSpaceReplacingChar(uint c) => IsSpaceReplacingChar(c) ? '\u0020' : c;

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static unsafe TChar* MatchNegativeSignChars<TChar>(TChar* p, TChar* pEnd, NumberFormatInfo info)
            where TChar : unmanaged, IUtfChar<TChar>
        {
            TChar* ret = MatchChars(p, pEnd, info.NegativeSignTChar<TChar>());

            if ((ret is null) && info.AllowHyphenDuringParsing() && (p < pEnd) && (TChar.CastToUInt32(*p) == '-'))
            {
                ret = p + 1;
            }

            return ret;
        }

        private static unsafe TChar* MatchChars<TChar>(TChar* p, TChar* pEnd, ReadOnlySpan<TChar> value)
            where TChar : unmanaged, IUtfChar<TChar>
        {
            // p/pEnd may be null when the input being parsed is an empty span (e.g. from a null string),
            // in which case they are both null and the range is empty; the length check below then rejects
            // any non-empty pattern before the loop can dereference p.
            Debug.Assert((p != null) || (p == pEnd));
            Debug.Assert((pEnd != null) || (p == pEnd));
            Debug.Assert(p <= pEnd);

            // An empty pattern never matches, and one longer than the remaining input cannot match, so
            // the loop only has to bound itself by the pattern.
            if (value.IsEmpty || (value.Length > (pEnd - p)))
            {
                return null;
            }

            fixed (TChar* stringPointer = &MemoryMarshal.GetReference(value))
            {
                TChar* str = stringPointer;
                TChar* strEnd = stringPointer + value.Length;

                do
                {
                    uint cp = TChar.CastToUInt32(*p);
                    uint val = TChar.CastToUInt32(*str);

                    // We only hurt the failure case
                    // This fix is for cultures that use NBSP (U+00A0) or narrow NBSP (U+202F) as group/decimal separators
                    // (e.g., French, Kazakh, Ukrainian). Since a user cannot easily type these characters,
                    // we accept regular space (U+0020) as equivalent.
                    // We also need to handle the reverse case where the input has NBSP and the format string has space.
                    if (cp != val && NormalizeSpaceReplacingChar(cp) != NormalizeSpaceReplacingChar(val))
                    {
                        return null;
                    }

                    p++;
                    str++;
                }
                while (str != strEnd);
            }

            return p;
        }
    }
}