diff --git a/Src/Newtonsoft.Json.Tests/Utilities/ConvertUtilsTests.cs b/Src/Newtonsoft.Json.Tests/Utilities/ConvertUtilsTests.cs index 525b63b1..f6b48cf4 100644 --- a/Src/Newtonsoft.Json.Tests/Utilities/ConvertUtilsTests.cs +++ b/Src/Newtonsoft.Json.Tests/Utilities/ConvertUtilsTests.cs @@ -158,6 +158,82 @@ namespace Newtonsoft.Json.Tests.Utilities AssertDoubleTryParse("4.94065645841247E+555", ParseResult.Overflow, null); } + private void AssertDecimalTryParse(string s, ParseResult expectedResult, decimal? expectedValue) + { + decimal d; + char[] c = s.ToCharArray(); + ParseResult result = ConvertUtils.DecimalTryParse(c, 0, c.Length, out d); + + decimal d2; + bool result2 = decimal.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out d2) + && !s.StartsWith(".") + && !s.EndsWith(".") + && !(s.StartsWith("0") && s.Length > 1 && !s.StartsWith("0.") && !s.StartsWith("0e", StringComparison.OrdinalIgnoreCase)) + && !(s.StartsWith("-0") && s.Length > 2 && !s.StartsWith("-0.") && !s.StartsWith("-0e", StringComparison.OrdinalIgnoreCase)) + && s.IndexOf(".e", StringComparison.OrdinalIgnoreCase) == -1; + + Assert.AreEqual(expectedResult, result); + Assert.AreEqual(expectedResult == ParseResult.Success, result2); + + if (result2) + { + Assert.IsTrue(expectedValue.HasValue); + + Assert.AreEqual(expectedValue.Value, d, "Input string: " + s); + + Assert.AreEqual(expectedValue.Value, d2, "DecimalTryParse result is not equal to decimal.Parse. Input string: " + s); + } + } + + [Test] + public void DecimalTryParse() + { + AssertDecimalTryParse("1", ParseResult.Success, 1M); + AssertDecimalTryParse("1E1", ParseResult.Success, 10M); + AssertDecimalTryParse("1E28", ParseResult.Success, 10000000000000000000000000000M); + + AssertDecimalTryParse("1.2345678901234567890123456789", ParseResult.Success, 1.2345678901234567890123456789M); + + AssertDecimalTryParse(decimal.MaxValue.ToString(), ParseResult.Success, decimal.MaxValue); + AssertDecimalTryParse(decimal.MinValue.ToString(), ParseResult.Success, decimal.MinValue); + + AssertDecimalTryParse("12345678901234567890123456789", ParseResult.Success, 12345678901234567890123456789M); + AssertDecimalTryParse("12345678901234567890123456789.4", ParseResult.Success, 12345678901234567890123456789M); + AssertDecimalTryParse("12345678901234567890123456789.5", ParseResult.Success, 12345678901234567890123456790M); + AssertDecimalTryParse("-12345678901234567890123456789", ParseResult.Success, -12345678901234567890123456789M); + AssertDecimalTryParse("-12345678901234567890123456789.4", ParseResult.Success, -12345678901234567890123456789M); + AssertDecimalTryParse("-12345678901234567890123456789.5", ParseResult.Success, -12345678901234567890123456790M); + + AssertDecimalTryParse("1.2345678901234567890123456789e-25", ParseResult.Success, 0.0000000000000000000000001235M); + AssertDecimalTryParse("1.2345678901234567890123456789e-26", ParseResult.Success, 0.0000000000000000000000000123M); + AssertDecimalTryParse("1.2345678901234567890123456789e-28", ParseResult.Success, 0.0000000000000000000000000001M); + AssertDecimalTryParse("1.2345678901234567890123456789e-29", ParseResult.Success, 0M); + AssertDecimalTryParse("1E-999", ParseResult.Success, 0M); + + AssertDecimalTryParse("1E+29", ParseResult.Overflow, null); + AssertDecimalTryParse("-1E+29", ParseResult.Overflow, null); + + AssertDecimalTryParse("01E28", ParseResult.Invalid, null); + AssertDecimalTryParse("-01E28", ParseResult.Invalid, null); + AssertDecimalTryParse("1.", ParseResult.Invalid, null); + AssertDecimalTryParse("0.", ParseResult.Invalid, null); + AssertDecimalTryParse(".1E23", ParseResult.Invalid, null); + AssertDecimalTryParse("1..1E23", ParseResult.Invalid, null); + AssertDecimalTryParse("1.E23", ParseResult.Invalid, null); + AssertDecimalTryParse("1E2.3", ParseResult.Invalid, null); + AssertDecimalTryParse("1EE-10", ParseResult.Invalid, null); + AssertDecimalTryParse("1E-1-0", ParseResult.Invalid, null); + AssertDecimalTryParse("1-E10", ParseResult.Invalid, null); + AssertDecimalTryParse("", ParseResult.Invalid, null); + AssertDecimalTryParse("5.1231231E", ParseResult.Invalid, null); + AssertDecimalTryParse("1E+23i", ParseResult.Invalid, null); + AssertDecimalTryParse("1EE+23", ParseResult.Invalid, null); + AssertDecimalTryParse("1E++23", ParseResult.Invalid, null); + AssertDecimalTryParse("1E--23", ParseResult.Invalid, null); + AssertDecimalTryParse("E23", ParseResult.Invalid, null); + AssertDecimalTryParse("00", ParseResult.Invalid, null); + } + [Test] public void Int64TryParse() { diff --git a/Src/Newtonsoft.Json/JsonTextReader.cs b/Src/Newtonsoft.Json/JsonTextReader.cs index 3a6ec395..f15a685d 100644 --- a/Src/Newtonsoft.Json/JsonTextReader.cs +++ b/Src/Newtonsoft.Json/JsonTextReader.cs @@ -2019,10 +2019,9 @@ namespace Newtonsoft.Json } else { - string number = _stringReference.ToString(); - decimal value; - if (decimal.TryParse(number, NumberStyles.Number | NumberStyles.AllowExponent, CultureInfo.InvariantCulture, out value)) + ParseResult parseResult = ConvertUtils.DecimalTryParse(_stringReference.Chars, _stringReference.StartIndex, _stringReference.Length, out value); + if (parseResult == ParseResult.Success) { numberValue = value; } @@ -2123,18 +2122,17 @@ namespace Newtonsoft.Json } else { - string number = _stringReference.ToString(); - if (_floatParseHandling == FloatParseHandling.Decimal) { decimal d; - if (decimal.TryParse(number, NumberStyles.Number | NumberStyles.AllowExponent, CultureInfo.InvariantCulture, out d)) + parseResult = ConvertUtils.DecimalTryParse(_stringReference.Chars, _stringReference.StartIndex, _stringReference.Length, out d); + if (parseResult == ParseResult.Success) { numberValue = d; } else { - throw ThrowReaderError("Input string '{0}' is not a valid decimal.".FormatWith(CultureInfo.InvariantCulture, number)); + throw ThrowReaderError("Input string '{0}' is not a valid decimal.".FormatWith(CultureInfo.InvariantCulture, _stringReference.ToString())); } } else @@ -2147,7 +2145,7 @@ namespace Newtonsoft.Json } else { - throw ThrowReaderError("Input string '{0}' is not a valid number.".FormatWith(CultureInfo.InvariantCulture, number)); + throw ThrowReaderError("Input string '{0}' is not a valid number.".FormatWith(CultureInfo.InvariantCulture, _stringReference.ToString())); } } diff --git a/Src/Newtonsoft.Json/Utilities/ConvertUtils.cs b/Src/Newtonsoft.Json/Utilities/ConvertUtils.cs index 413ff0a3..51ec5f6f 100644 --- a/Src/Newtonsoft.Json/Utilities/ConvertUtils.cs +++ b/Src/Newtonsoft.Json/Utilities/ConvertUtils.cs @@ -1357,6 +1357,265 @@ namespace Newtonsoft.Json.Utilities return double.IsInfinity(value) ? ParseResult.Overflow : ParseResult.Success; } + public static ParseResult DecimalTryParse(char[] chars, int start, int length, out decimal value) + { + value = 0M; + const decimal decimalMaxValueHi28 = 7922816251426433759354395033M; + const ulong decimalMaxValueHi19 = 7922816251426433759UL; + const ulong decimalMaxValueLo9 = 354395033UL; + const char decimalMaxValueLo1 = '5'; + + if (length == 0) + { + return ParseResult.Invalid; + } + + bool isNegative = (chars[start] == '-'); + if (isNegative) + { + // text just a negative sign + if (length == 1) + { + return ParseResult.Invalid; + } + + start++; + length--; + } + + int i = start; + int end = start + length; + int numDecimalStart = end; + int numDecimalEnd = end; + int exponent = 0; + ulong hi19 = 0UL; + ulong lo10 = 0UL; + int mantissaDigits = 0; + int exponentFromMantissa = 0; + bool? roundUp = null; + bool? storeOnly28Digits = null; + for (; i < end; i++) + { + char c = chars[i]; + switch (c) + { + case '.': + if (i == start) + { + return ParseResult.Invalid; + } + if (i + 1 == end) + { + return ParseResult.Invalid; + } + + if (numDecimalStart != end) + { + // multiple decimal points + return ParseResult.Invalid; + } + + numDecimalStart = i + 1; + break; + case 'e': + case 'E': + if (i == start) + { + return ParseResult.Invalid; + } + if (i == numDecimalStart) + { + // E follows decimal point + return ParseResult.Invalid; + } + i++; + if (i == end) + { + return ParseResult.Invalid; + } + + if (numDecimalStart < end) + { + numDecimalEnd = i - 1; + } + + c = chars[i]; + bool exponentNegative = false; + switch (c) + { + case '-': + exponentNegative = true; + i++; + break; + case '+': + i++; + break; + } + + // parse 3 digit + for (; i < end; i++) + { + c = chars[i]; + if (c < '0' || c > '9') + { + return ParseResult.Invalid; + } + + int newExponent = (10 * exponent) + (c - '0'); + // stops updating exponent when overflowing + if (exponent < newExponent) + { + exponent = newExponent; + } + } + + if (exponentNegative) + { + exponent = -exponent; + } + break; + default: + if (c < '0' || c > '9') + { + return ParseResult.Invalid; + } + + if (i == start && c == '0') + { + i++; + if (i != end) + { + c = chars[i]; + if (c == '.') + { + goto case '.'; + } + if (c == 'e' || c == 'E') + { + goto case 'E'; + } + + return ParseResult.Invalid; + } + } + + if (mantissaDigits < 29 && (mantissaDigits != 28 || !(storeOnly28Digits ?? (storeOnly28Digits = (hi19 > decimalMaxValueHi19 || (hi19 == decimalMaxValueHi19 && (lo10 > decimalMaxValueLo9 || (lo10 == decimalMaxValueLo9 && c > decimalMaxValueLo1))))).GetValueOrDefault()))) + { + if (mantissaDigits < 19) + { + hi19 = (hi19 * 10UL) + (ulong)(c - '0'); + } + else + { + lo10 = (lo10 * 10UL) + (ulong)(c - '0'); + } + ++mantissaDigits; + } + else + { + if (!roundUp.HasValue) + { + roundUp = c >= '5'; + } + ++exponentFromMantissa; + } + break; + } + } + + exponent += exponentFromMantissa; + + // correct the decimal point + exponent -= (numDecimalEnd - numDecimalStart); + + if (mantissaDigits <= 19) + { + value = hi19; + } + else + { + value = (hi19 * DecimalFactors[mantissaDigits - 20]) + lo10; + } + + if (exponent > 0) + { + mantissaDigits += exponent; + if (mantissaDigits > 29) + { + return ParseResult.Overflow; + } + if (mantissaDigits == 29) + { + if (exponent > 1) + { + value *= DecimalFactors[exponent - 2]; + if (value > decimalMaxValueHi28) + { + return ParseResult.Overflow; + } + } + value *= 10M; + } + else + { + value *= DecimalFactors[exponent - 1]; + } + } + else + { + if (roundUp == true && exponent >= -28) + { + ++value; + } + if (exponent < 0) + { + if (mantissaDigits + exponent + 28 <= 0) + { + value = 0M; + return ParseResult.Success; + } + if (exponent >= -28) + { + value /= DecimalFactors[-exponent - 1]; + } + else + { + decimal[] decimalFactors = DecimalFactors; + value /= decimalFactors[27]; + value /= decimalFactors[-exponent - 29]; + } + } + } + + if (isNegative) + { + value = -value; + } + + return ParseResult.Success; + } + + private static decimal[] _decimalFactors; + + private static decimal[] DecimalFactors + { + get + { + decimal[] decimalFactors = _decimalFactors; + if (decimalFactors == null) + { + decimalFactors = new decimal[28]; + decimal last = 1M; + for (int i = 0; i < decimalFactors.Length; ++i) + { + decimalFactors[i] = last *= 10M; + } + _decimalFactors = decimalFactors; + } + return decimalFactors; + } + } + public static bool TryConvertGuid(string s, out Guid g) { // GUID has to have format 00000000-0000-0000-0000-000000000000 @@ -1414,4 +1673,4 @@ namespace Newtonsoft.Json.Utilities return true; } } -} \ No newline at end of file +}