Optimized decimal parsing (#1204)

This commit is contained in:
Tyler Brinkley
2017-02-25 10:31:50 +13:00
committed by James Newton-King
parent 1a67b34deb
commit 4600bc54b8
3 changed files with 342 additions and 9 deletions
@@ -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()
{
+6 -8
View File
@@ -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()));
}
}
+260 -1
View File
@@ -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;
}
}
}
}