diff --git a/src/main/java/com/fasterxml/jackson/core/JsonParser.java b/src/main/java/com/fasterxml/jackson/core/JsonParser.java index 335fd434e7..fc76afb59b 100644 --- a/src/main/java/com/fasterxml/jackson/core/JsonParser.java +++ b/src/main/java/com/fasterxml/jackson/core/JsonParser.java @@ -325,6 +325,17 @@ public enum Feature { */ INCLUDE_SOURCE_IN_LOCATION(true), + /** + * Feature that determines whether we use the built-in {@link Double#parseDouble(String)} code to parse + * doubles or if we use {@link com.fasterxml.jackson.core.io.doubleparser} + * instead. + *
+ * This setting is disabled by default. + * + * @since 2.14 + */ + USE_FAST_DOUBLE_PARSER(false), + ; /** diff --git a/src/main/java/com/fasterxml/jackson/core/base/ParserBase.java b/src/main/java/com/fasterxml/jackson/core/base/ParserBase.java index f744b97b2e..62c1e27e6c 100644 --- a/src/main/java/com/fasterxml/jackson/core/base/ParserBase.java +++ b/src/main/java/com/fasterxml/jackson/core/base/ParserBase.java @@ -893,11 +893,11 @@ private void _parseSlowFloat(int expType) throws IOException _numberBigDecimal = _textBuffer.contentsAsDecimal(); _numTypesValid = NR_BIGDECIMAL; } else if (expType == NR_FLOAT) { - _numberFloat = _textBuffer.contentsAsFloat(); + _numberFloat = _textBuffer.contentsAsFloat(isEnabled(Feature.USE_FAST_DOUBLE_PARSER)); _numTypesValid = NR_FLOAT; } else { // Otherwise double has to do - _numberDouble = _textBuffer.contentsAsDouble(); + _numberDouble = _textBuffer.contentsAsDouble(isEnabled(Feature.USE_FAST_DOUBLE_PARSER)); _numTypesValid = NR_DOUBLE; } } catch (NumberFormatException nex) { @@ -927,7 +927,7 @@ private void _parseSlowInt(int expType) throws IOException _reportTooLongIntegral(expType, numStr); } if ((expType == NR_DOUBLE) || (expType == NR_FLOAT)) { - _numberDouble = NumberInput.parseDouble(numStr); + _numberDouble = NumberInput.parseDouble(numStr, isEnabled(Feature.USE_FAST_DOUBLE_PARSER)); _numTypesValid = NR_DOUBLE; } else { // nope, need the heavy guns... (rare case) diff --git a/src/main/java/com/fasterxml/jackson/core/io/NumberInput.java b/src/main/java/com/fasterxml/jackson/core/io/NumberInput.java index 7ed502937f..754b4bf8f5 100644 --- a/src/main/java/com/fasterxml/jackson/core/io/NumberInput.java +++ b/src/main/java/com/fasterxml/jackson/core/io/NumberInput.java @@ -1,5 +1,8 @@ package com.fasterxml.jackson.core.io; +import com.fasterxml.jackson.core.io.doubleparser.FastDoubleParser; +import com.fasterxml.jackson.core.io.doubleparser.FastFloatParser; + import java.math.BigDecimal; public final class NumberInput @@ -239,7 +242,9 @@ public static int parseAsInt(String s, int def) // if other symbols, parse as Double, coerce if (c > '9' || c < '0') { try { - return (int) parseDouble(s); + //useFastParser=true is used because there is a lot less risk that small changes in result will have an affect + //and performance benefit is useful + return (int) parseDouble(s, true); } catch (NumberFormatException e) { return def; } @@ -276,7 +281,9 @@ public static long parseAsLong(String s, long def) // if other symbols, parse as Double, coerce if (c > '9' || c < '0') { try { - return (long) parseDouble(s); + //useFastParser=true is used because there is a lot less risk that small changes in result will have an affect + //and performance benefit is useful + return (long) parseDouble(s, true); } catch (NumberFormatException e) { return def; } @@ -287,8 +294,26 @@ public static long parseAsLong(String s, long def) } catch (NumberFormatException e) { } return def; } - - public static double parseAsDouble(String s, double def) + + /** + * @param s a string representing a number to parse + * @param def the default to return if `s` is not a parseable number + * @return closest matching double (or `def` if there is an issue with `s`) where useFastParser=false + * @see #parseAsDouble(String, double, boolean) + */ + public static double parseAsDouble(final String s, final double def) + { + return parseAsDouble(s, def, false); + } + + /** + * @param s a string representing a number to parse + * @param def the default to return if `s` is not a parseable number + * @param useFastParser whether to use {@link com.fasterxml.jackson.core.io.doubleparser} + * @return closest matching double (or `def` if there is an issue with `s`) + * @since 2.14 + */ + public static double parseAsDouble(String s, final double def, final boolean useFastParser) { if (s == null) { return def; } s = s.trim(); @@ -297,23 +322,52 @@ public static double parseAsDouble(String s, double def) return def; } try { - return parseDouble(s); + return parseDouble(s, useFastParser); } catch (NumberFormatException e) { } return def; } - public static double parseDouble(String s) throws NumberFormatException { - return Double.parseDouble(s); + /** + * @param s a string representing a number to parse + * @return closest matching double + * @throws NumberFormatException if string cannot be represented by a double where useFastParser=false + * @see #parseDouble(String, boolean) + */ + public static double parseDouble(final String s) throws NumberFormatException { + return parseDouble(s, false); + } + + /** + * @param s a string representing a number to parse + * @param useFastParser whether to use {@link com.fasterxml.jackson.core.io.doubleparser} + * @return closest matching double + * @throws NumberFormatException if string cannot be represented by a double + * @since v2.14 + */ + public static double parseDouble(final String s, final boolean useFastParser) throws NumberFormatException { + return useFastParser ? FastDoubleParser.parseDouble(s) : Double.parseDouble(s); + } + + /** + * @param s a string representing a number to parse + * @return closest matching float + * @throws NumberFormatException if string cannot be represented by a float where useFastParser=false + * @see #parseFloat(String, boolean) + * @since v2.14 + */ + public static float parseFloat(final String s) throws NumberFormatException { + return parseFloat(s, false); } /** * @param s a string representing a number to parse + * @param useFastParser whether to use {@link com.fasterxml.jackson.core.io.doubleparser} * @return closest matching float * @throws NumberFormatException if string cannot be represented by a float * @since v2.14 */ - public static float parseFloat(String s) throws NumberFormatException { - return Float.parseFloat(s); + public static float parseFloat(final String s, final boolean useFastParser) throws NumberFormatException { + return useFastParser ? FastFloatParser.parseFloat(s) : Float.parseFloat(s); } public static BigDecimal parseBigDecimal(String s) throws NumberFormatException { diff --git a/src/main/java/com/fasterxml/jackson/core/json/JsonReadFeature.java b/src/main/java/com/fasterxml/jackson/core/json/JsonReadFeature.java index 304b320a12..e02291ce10 100644 --- a/src/main/java/com/fasterxml/jackson/core/json/JsonReadFeature.java +++ b/src/main/java/com/fasterxml/jackson/core/json/JsonReadFeature.java @@ -182,6 +182,17 @@ public enum JsonReadFeature */ @SuppressWarnings("deprecation") ALLOW_TRAILING_COMMA(false, JsonParser.Feature.ALLOW_TRAILING_COMMA), + + /** + * Feature that determines whether we use the built-in {@link Double#parseDouble(String)} code to parse + * doubles or if we use {@link com.fasterxml.jackson.core.io.doubleparser} + * instead. + *
+ * This setting is disabled by default. + * + * @since 2.14 + */ + USE_FAST_DOUBLE_PARSER(false, JsonParser.Feature.USE_FAST_DOUBLE_PARSER) ; final private boolean _defaultState; diff --git a/src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java b/src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java index 2d98a05073..c1b1a9c17a 100644 --- a/src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java +++ b/src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java @@ -512,9 +512,25 @@ public BigDecimal contentsAsDecimal() throws NumberFormatException * @return Buffered text value parsed as a {@link Double}, if possible * * @throws NumberFormatException if contents are not a valid Java number + * @deprecated use {@link #contentsAsDouble(boolean)} */ + @Deprecated public double contentsAsDouble() throws NumberFormatException { - return NumberInput.parseDouble(contentsAsString()); + return contentsAsDouble(false); + } + + /** + * Convenience method for converting contents of the buffer + * into a Double value. + * + * @param useFastParser whether to use {@link com.fasterxml.jackson.core.io.doubleparser} + * @return Buffered text value parsed as a {@link Double}, if possible + * + * @throws NumberFormatException if contents are not a valid Java number + * @since 2.14 + */ + public double contentsAsDouble(final boolean useFastParser) throws NumberFormatException { + return NumberInput.parseDouble(contentsAsString(), useFastParser); } /** @@ -525,9 +541,25 @@ public double contentsAsDouble() throws NumberFormatException { * * @throws NumberFormatException if contents are not a valid Java number * @since 2.14 + * @deprecated use {@link #contentsAsFloat(boolean)} */ + @Deprecated public float contentsAsFloat() throws NumberFormatException { - return NumberInput.parseFloat(contentsAsString()); + return contentsAsFloat(false); + } + + /** + * Convenience method for converting contents of the buffer + * into a Float value. + * + * @param useFastParser whether to use {@link com.fasterxml.jackson.core.io.doubleparser} + * @return Buffered text value parsed as a {@link Float}, if possible + * + * @throws NumberFormatException if contents are not a valid Java number + * @since 2.14 + */ + public float contentsAsFloat(final boolean useFastParser) throws NumberFormatException { + return NumberInput.parseFloat(contentsAsString(), useFastParser); } /** diff --git a/src/test/java/com/fasterxml/jackson/core/io/TestNumberInput.java b/src/test/java/com/fasterxml/jackson/core/io/TestNumberInput.java index 1035ac6c8d..bf1f80c247 100644 --- a/src/test/java/com/fasterxml/jackson/core/io/TestNumberInput.java +++ b/src/test/java/com/fasterxml/jackson/core/io/TestNumberInput.java @@ -9,13 +9,20 @@ public void testNastySmallDouble() //prior to jackson v2.14, this value used to be returned as Double.MIN_VALUE final String nastySmallDouble = "2.2250738585072012e-308"; assertEquals(Double.parseDouble(nastySmallDouble), NumberInput.parseDouble(nastySmallDouble)); + assertEquals(Double.parseDouble(nastySmallDouble), NumberInput.parseDouble(nastySmallDouble, true)); } public void testParseFloat() { final String exampleFloat = "1.199999988079071"; assertEquals(1.1999999f, NumberInput.parseFloat(exampleFloat)); + assertEquals(1.1999999f, NumberInput.parseFloat(exampleFloat, true)); assertEquals(1.2f, (float)NumberInput.parseDouble(exampleFloat)); + assertEquals(1.2f, (float)NumberInput.parseDouble(exampleFloat, true)); + + final String exampleFloat2 = "7.006492321624086e-46"; + assertEquals("1.4E-45", Float.toString(NumberInput.parseFloat(exampleFloat2))); + assertEquals("1.4E-45", Float.toString(NumberInput.parseFloat(exampleFloat2, true))); } } diff --git a/src/test/java/com/fasterxml/jackson/core/read/FastParserNonStandardNumberParsingTest.java b/src/test/java/com/fasterxml/jackson/core/read/FastParserNonStandardNumberParsingTest.java new file mode 100644 index 0000000000..35034ea344 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/core/read/FastParserNonStandardNumberParsingTest.java @@ -0,0 +1,17 @@ +package com.fasterxml.jackson.core.read; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.json.JsonReadFeature; + +public class FastParserNonStandardNumberParsingTest extends NonStandardNumberParsingTest { + private final JsonFactory fastFactory = + JsonFactory.builder() + .enable(JsonReadFeature.ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS) + .enable(JsonReadFeature.USE_FAST_DOUBLE_PARSER) + .build(); + + @Override + protected JsonFactory jsonFactory() { + return fastFactory; + } +} diff --git a/src/test/java/com/fasterxml/jackson/core/read/FastParserNumberParsingTest.java b/src/test/java/com/fasterxml/jackson/core/read/FastParserNumberParsingTest.java new file mode 100644 index 0000000000..23260459b4 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/core/read/FastParserNumberParsingTest.java @@ -0,0 +1,14 @@ +package com.fasterxml.jackson.core.read; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; + +public class FastParserNumberParsingTest extends NumberParsingTest { + + private final JsonFactory fastFactory = new JsonFactory().enable(JsonParser.Feature.USE_FAST_DOUBLE_PARSER); + + @Override + protected JsonFactory jsonFactory() { + return fastFactory; + } +} diff --git a/src/test/java/com/fasterxml/jackson/core/read/FloatParsingTest.java b/src/test/java/com/fasterxml/jackson/core/read/FloatParsingTest.java index 6d2762b038..159c158483 100644 --- a/src/test/java/com/fasterxml/jackson/core/read/FloatParsingTest.java +++ b/src/test/java/com/fasterxml/jackson/core/read/FloatParsingTest.java @@ -12,24 +12,41 @@ public class FloatParsingTest extends BaseTest public void testFloatArrayViaInputStream() throws Exception { - _testFloatArray(MODE_INPUT_STREAM); - _testFloatArray(MODE_INPUT_STREAM_THROTTLED); + _testFloatArray(MODE_INPUT_STREAM, false); + _testFloatArray(MODE_INPUT_STREAM_THROTTLED, false); + } + + public void testFloatArrayViaInputStreamWithFastParser() throws Exception + { + _testFloatArray(MODE_INPUT_STREAM, true); + _testFloatArray(MODE_INPUT_STREAM_THROTTLED, true); } public void testFloatArrayViaReader() throws Exception { - _testFloatArray(MODE_READER); + _testFloatArray(MODE_READER, false); + } + + public void testFloatArrayViaReaderWithFastParser() throws Exception { + _testFloatArray(MODE_READER, true); } public void testFloatArrayViaDataInput() throws Exception { - _testFloatArray(MODE_DATA_INPUT); + _testFloatArray(MODE_DATA_INPUT, false); + } + + public void testFloatArrayViaDataInputWithFasrtParser() throws Exception { + _testFloatArray(MODE_DATA_INPUT, true); } - private void _testFloatArray(int mode) throws Exception + private void _testFloatArray(int mode, boolean useFastParser) throws Exception { // construct new instance to reduce buffer recycling etc: TokenStreamFactory jsonF = newStreamFactory(); JsonParser p = createParser(jsonF, mode, FLOATS_DOC); + if (useFastParser) { + p.enable(JsonParser.Feature.USE_FAST_DOUBLE_PARSER); + } assertToken(JsonToken.START_ARRAY, p.nextToken()); diff --git a/src/test/java/com/fasterxml/jackson/core/read/NonStandardNumberParsingTest.java b/src/test/java/com/fasterxml/jackson/core/read/NonStandardNumberParsingTest.java index 82537b1211..7cdd194909 100644 --- a/src/test/java/com/fasterxml/jackson/core/read/NonStandardNumberParsingTest.java +++ b/src/test/java/com/fasterxml/jackson/core/read/NonStandardNumberParsingTest.java @@ -10,6 +10,10 @@ public class NonStandardNumberParsingTest .enable(JsonReadFeature.ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS) .build(); + protected JsonFactory jsonFactory() { + return JSON_F; + } + /** * The format ".NNN" (as opposed to "0.NNN") is not valid JSON, so this should fail */ @@ -27,16 +31,16 @@ public void testLeadingDotInDecimal() throws Exception { } public void testLeadingDotInDecimalAllowedAsync() throws Exception { - _testLeadingDotInDecimalAllowed(JSON_F, MODE_DATA_INPUT); + _testLeadingDotInDecimalAllowed(jsonFactory(), MODE_DATA_INPUT); } public void testLeadingDotInDecimalAllowedBytes() throws Exception { - _testLeadingDotInDecimalAllowed(JSON_F, MODE_INPUT_STREAM); - _testLeadingDotInDecimalAllowed(JSON_F, MODE_INPUT_STREAM_THROTTLED); + _testLeadingDotInDecimalAllowed(jsonFactory(), MODE_INPUT_STREAM); + _testLeadingDotInDecimalAllowed(jsonFactory(), MODE_INPUT_STREAM_THROTTLED); } public void testLeadingDotInDecimalAllowedReader() throws Exception { - _testLeadingDotInDecimalAllowed(JSON_F, MODE_READER); + _testLeadingDotInDecimalAllowed(jsonFactory(), MODE_READER); } private void _testLeadingDotInDecimalAllowed(JsonFactory f, int mode) throws Exception diff --git a/src/test/java/com/fasterxml/jackson/core/read/NumberParsingTest.java b/src/test/java/com/fasterxml/jackson/core/read/NumberParsingTest.java index 898dec8969..a5c024c1b3 100644 --- a/src/test/java/com/fasterxml/jackson/core/read/NumberParsingTest.java +++ b/src/test/java/com/fasterxml/jackson/core/read/NumberParsingTest.java @@ -20,7 +20,10 @@ public class NumberParsingTest extends com.fasterxml.jackson.core.BaseTest { - private final JsonFactory FACTORY = sharedStreamFactory(); + + protected JsonFactory jsonFactory() { + return sharedStreamFactory(); + } /* /********************************************************************** @@ -414,7 +417,7 @@ public void testFloatBoundary146Chars() throws Exception arr[i + 3] = '-'; arr[i + 4] = '1'; CharArrayReader r = new CharArrayReader(arr, 0, i+5); - JsonParser p = FACTORY.createParser(r); + JsonParser p = jsonFactory().createParser(r); assertToken(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken()); p.close(); } @@ -431,7 +434,7 @@ public void testFloatBoundary146Bytes() throws Exception arr[i + 3] = '-'; arr[i + 4] = '1'; ByteArrayInputStream in = new ByteArrayInputStream(arr, 0, i+5); - JsonParser p = FACTORY.createParser(in); + JsonParser p = jsonFactory().createParser(in); assertToken(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken()); p.close(); } @@ -612,7 +615,7 @@ public void testParsingOfLongerSequences() throws Exception if (input == 0) { p = createParserUsingStream(DOC, "UTF-8"); } else { - p = FACTORY.createParser(DOC); + p = jsonFactory().createParser(DOC); } assertToken(JsonToken.START_ARRAY, p.nextToken()); @@ -671,8 +674,8 @@ public void testLongNumbers2() throws Exception private void _testIssue160LongNumbers(JsonFactory f, String doc, boolean useStream) throws Exception { JsonParser p = useStream - ? FACTORY.createParser(doc.getBytes("UTF-8")) - : FACTORY.createParser(doc); + ? jsonFactory().createParser(doc.getBytes("UTF-8")) + : jsonFactory().createParser(doc); assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken()); BigInteger v = p.getBigIntegerValue(); assertNull(p.nextToken()); @@ -686,7 +689,7 @@ private void _testIssue160LongNumbers(JsonFactory f, String doc, boolean useStre */ public void testParsingOfLongerSequencesWithNonNumeric() throws Exception { - JsonFactory f = JsonFactory.builder() + JsonFactory f = jsonFactory().builder() .enable(JsonReadFeature.ALLOW_NON_NUMERIC_NUMBERS) .build(); _testParsingOfLongerSequencesWithNonNumeric(f, MODE_INPUT_STREAM); @@ -816,11 +819,11 @@ public void testLongerFloatingPoint() throws Exception // test out with both Reader and ByteArrayInputStream JsonParser p; - p = FACTORY.createParser(new StringReader(DOC)); + p = jsonFactory().createParser(new StringReader(DOC)); _testLongerFloat(p, DOC); p.close(); - p = FACTORY.createParser(new ByteArrayInputStream(DOC.getBytes("UTF-8"))); + p = jsonFactory().createParser(new ByteArrayInputStream(DOC.getBytes("UTF-8"))); _testLongerFloat(p, DOC); p.close(); }