From a4febb8d2acbd497807d24da7c4cea6635c2deaa Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Fri, 1 Mar 2024 23:55:17 +0100 Subject: [PATCH 1/7] improve perf of float and double parsing in TextBuffer --- .../jackson/core/io/NumberInput.java | 52 ++++++++++++ .../core/util/ReadConstrainedTextBuffer.java | 9 +++ .../jackson/core/util/TextBuffer.java | 79 +++++++++++++++++++ 3 files changed, 140 insertions(+) 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 bde9e32a63..f1f0940a31 100644 --- a/src/main/java/com/fasterxml/jackson/core/io/NumberInput.java +++ b/src/main/java/com/fasterxml/jackson/core/io/NumberInput.java @@ -400,6 +400,32 @@ public static double parseDouble(final String s, final boolean useFastParser) th return useFastParser ? JavaDoubleParser.parseDouble(s) : Double.parseDouble(s); } + /** + * @param array a char array containing a number to parse + * @param useFastParser whether to use {@code FastDoubleParser} + * @return closest matching double + * @throws NumberFormatException if string cannot be represented by a double + * @since v2.18 + */ + public static double parseDouble(final char[] array, final boolean useFastParser) throws NumberFormatException { + return parseDouble(array, 0, array.length, useFastParser); + } + + /** + * @param array a char array containing a number to parse + * @param offset the offset to apply when parsing the number in the char array + * @param len the length of the number in the char array + * @param useFastParser whether to use {@code FastDoubleParser} + * @return closest matching double + * @throws NumberFormatException if string cannot be represented by a double + * @since v2.18 + */ + public static double parseDouble(final char[] array, final int offset, + final int len, final boolean useFastParser) throws NumberFormatException { + return useFastParser ? JavaDoubleParser.parseDouble(array, offset, len) : + Double.parseDouble(new String(array, offset, len)); + } + /** * @param s a string representing a number to parse * @return closest matching float @@ -428,6 +454,32 @@ public static float parseFloat(final String s, final boolean useFastParser) thro return Float.parseFloat(s); } + /** + * @param array a char array containing a number to parse + * @param useFastParser whether to use {@code FastDoubleParser} + * @return closest matching float + * @throws NumberFormatException if string cannot be represented by a float + * @since v2.18 + */ + public static float parseFloat(final char[] array, final boolean useFastParser) throws NumberFormatException { + return parseFloat(array, 0, array.length, useFastParser); + } + + /** + * @param array a char array containing a number to parse + * @param offset the offset to apply when parsing the number in the char array + * @param len the length of the number in the char array + * @param useFastParser whether to use {@code FastDoubleParser} + * @return closest matching float + * @throws NumberFormatException if string cannot be represented by a float + * @since v2.18 + */ + public static float parseFloat(final char[] array, final int offset, + final int len, final boolean useFastParser) throws NumberFormatException { + return useFastParser ? JavaFloatParser.parseFloat(array, offset, len) : + Float.parseFloat(new String(array, offset, len)); + } + /** * @param s a string representing a number to parse * @return a BigDecimal diff --git a/src/main/java/com/fasterxml/jackson/core/util/ReadConstrainedTextBuffer.java b/src/main/java/com/fasterxml/jackson/core/util/ReadConstrainedTextBuffer.java index 40f98253c6..fcc739d5c7 100644 --- a/src/main/java/com/fasterxml/jackson/core/util/ReadConstrainedTextBuffer.java +++ b/src/main/java/com/fasterxml/jackson/core/util/ReadConstrainedTextBuffer.java @@ -26,4 +26,13 @@ protected void validateStringLength(int length) throws StreamConstraintsExceptio { _streamReadConstraints.validateStringLength(length); } + + /** + * {@inheritDoc} + */ + @Override + protected void validateFPLength(int length) throws StreamConstraintsException + { + _streamReadConstraints.validateFPLength(length); + } } 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 4822506687..edab4a68ce 100644 --- a/src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java +++ b/src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java @@ -536,6 +536,37 @@ public char[] contentsAsArray() throws IOException { */ public double contentsAsDouble(final boolean useFastParser) throws NumberFormatException { try { + if (_resultString == null) { + // Has array been requested? Can make a shortcut, if so: + if (_resultArray != null) { + validateFPLength(_resultArray.length); + return NumberInput.parseDouble(_resultArray, useFastParser); + } else { + // Do we use shared array? + if (_inputStart >= 0) { + if (_inputLen < 1) { + _resultString = ""; + return 0.0d; + } + validateFPLength(_inputLen); + return NumberInput.parseDouble(_inputBuffer, _inputStart, _inputLen, useFastParser); + } else { // nope... need to copy + // But first, let's see if we have just one buffer + int segLen = _segmentSize; + int currLen = _currentSize; + + if (segLen == 0) { // yup + if (currLen == 0) { + _resultString = ""; + return 0.0d; + } else { + validateFPLength(currLen); + return NumberInput.parseDouble(_currentSegment, 0, currLen, useFastParser); + } + } + } + } + } return NumberInput.parseDouble(contentsAsString(), useFastParser); } catch (IOException e) { // JsonParseException is used to denote a string that is too long @@ -584,6 +615,37 @@ public float contentsAsFloat() throws NumberFormatException { */ public float contentsAsFloat(final boolean useFastParser) throws NumberFormatException { try { + if (_resultString == null) { + // Has array been requested? Can make a shortcut, if so: + if (_resultArray != null) { + validateFPLength(_resultArray.length); + return NumberInput.parseFloat(_resultArray, useFastParser); + } else { + // Do we use shared array? + if (_inputStart >= 0) { + if (_inputLen < 1) { + _resultString = ""; + return 0.0f; + } + validateFPLength(_inputLen); + return NumberInput.parseFloat(_inputBuffer, _inputStart, _inputLen, useFastParser); + } else { // nope... need to copy + // But first, let's see if we have just one buffer + int segLen = _segmentSize; + int currLen = _currentSize; + + if (segLen == 0) { // yup + if (currLen == 0) { + _resultString = ""; + return 0.0f; + } else { + validateFPLength(currLen); + return NumberInput.parseFloat(_currentSegment, 0, currLen, useFastParser); + } + } + } + } + } return NumberInput.parseFloat(contentsAsString(), useFastParser); } catch (IOException e) { // JsonParseException is used to denote a string that is too long @@ -1191,4 +1253,21 @@ protected void validateStringLength(int length) throws IOException { // no-op } + + /** + * Convenience method that can be used to verify that a number + * of specified length does not exceed maximum specific by this + * constraints object: if it does, a + * {@link JsonParseException} + * is thrown. + * + * @param length Length of number in input units + * + * @throws IOException If length exceeds maximum + * @since 2.15 + */ + protected void validateFPLength(int length) throws IOException + { + // no-op + } } From 58a934246b2bacc0e5488b340766ee68a16be36e Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Sat, 2 Mar 2024 00:07:56 +0100 Subject: [PATCH 2/7] throw exception if content is empty --- .../java/com/fasterxml/jackson/core/util/TextBuffer.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 edab4a68ce..5122961875 100644 --- a/src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java +++ b/src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java @@ -546,7 +546,7 @@ public double contentsAsDouble(final boolean useFastParser) throws NumberFormatE if (_inputStart >= 0) { if (_inputLen < 1) { _resultString = ""; - return 0.0d; + throw new NumberFormatException("empty content"); } validateFPLength(_inputLen); return NumberInput.parseDouble(_inputBuffer, _inputStart, _inputLen, useFastParser); @@ -558,7 +558,7 @@ public double contentsAsDouble(final boolean useFastParser) throws NumberFormatE if (segLen == 0) { // yup if (currLen == 0) { _resultString = ""; - return 0.0d; + throw new NumberFormatException("empty content"); } else { validateFPLength(currLen); return NumberInput.parseDouble(_currentSegment, 0, currLen, useFastParser); @@ -625,7 +625,7 @@ public float contentsAsFloat(final boolean useFastParser) throws NumberFormatExc if (_inputStart >= 0) { if (_inputLen < 1) { _resultString = ""; - return 0.0f; + throw new NumberFormatException("empty content"); } validateFPLength(_inputLen); return NumberInput.parseFloat(_inputBuffer, _inputStart, _inputLen, useFastParser); @@ -637,7 +637,7 @@ public float contentsAsFloat(final boolean useFastParser) throws NumberFormatExc if (segLen == 0) { // yup if (currLen == 0) { _resultString = ""; - return 0.0f; + throw new NumberFormatException("empty content"); } else { validateFPLength(currLen); return NumberInput.parseFloat(_currentSegment, 0, currLen, useFastParser); From 2ffce6ed3bcc1225e619e13464caffb42e70d637 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Sat, 2 Mar 2024 00:10:55 +0100 Subject: [PATCH 3/7] update javadoc --- .../java/com/fasterxml/jackson/core/io/NumberInput.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 f1f0940a31..047a832874 100644 --- a/src/main/java/com/fasterxml/jackson/core/io/NumberInput.java +++ b/src/main/java/com/fasterxml/jackson/core/io/NumberInput.java @@ -404,7 +404,7 @@ public static double parseDouble(final String s, final boolean useFastParser) th * @param array a char array containing a number to parse * @param useFastParser whether to use {@code FastDoubleParser} * @return closest matching double - * @throws NumberFormatException if string cannot be represented by a double + * @throws NumberFormatException if value cannot be represented by a double * @since v2.18 */ public static double parseDouble(final char[] array, final boolean useFastParser) throws NumberFormatException { @@ -417,7 +417,7 @@ public static double parseDouble(final char[] array, final boolean useFastParser * @param len the length of the number in the char array * @param useFastParser whether to use {@code FastDoubleParser} * @return closest matching double - * @throws NumberFormatException if string cannot be represented by a double + * @throws NumberFormatException if value cannot be represented by a double * @since v2.18 */ public static double parseDouble(final char[] array, final int offset, @@ -458,7 +458,7 @@ public static float parseFloat(final String s, final boolean useFastParser) thro * @param array a char array containing a number to parse * @param useFastParser whether to use {@code FastDoubleParser} * @return closest matching float - * @throws NumberFormatException if string cannot be represented by a float + * @throws NumberFormatException if value cannot be represented by a float * @since v2.18 */ public static float parseFloat(final char[] array, final boolean useFastParser) throws NumberFormatException { @@ -471,7 +471,7 @@ public static float parseFloat(final char[] array, final boolean useFastParser) * @param len the length of the number in the char array * @param useFastParser whether to use {@code FastDoubleParser} * @return closest matching float - * @throws NumberFormatException if string cannot be represented by a float + * @throws NumberFormatException if value cannot be represented by a float * @since v2.18 */ public static float parseFloat(final char[] array, final int offset, From 64e25b2b4019a7973b5eb5d089dfba22e6c56d1b Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Sat, 2 Mar 2024 21:00:27 +0100 Subject: [PATCH 4/7] review comments --- .../jackson/core/io/NumberInput.java | 8 +-- .../core/util/ReadConstrainedTextBuffer.java | 9 --- .../jackson/core/util/TextBuffer.java | 23 ------- .../jackson/core/util/TestTextBuffer.java | 60 +++++++++++++++++++ 4 files changed, 64 insertions(+), 36 deletions(-) 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 047a832874..a0a1cef1ce 100644 --- a/src/main/java/com/fasterxml/jackson/core/io/NumberInput.java +++ b/src/main/java/com/fasterxml/jackson/core/io/NumberInput.java @@ -405,7 +405,7 @@ public static double parseDouble(final String s, final boolean useFastParser) th * @param useFastParser whether to use {@code FastDoubleParser} * @return closest matching double * @throws NumberFormatException if value cannot be represented by a double - * @since v2.18 + * @since 2.18 */ public static double parseDouble(final char[] array, final boolean useFastParser) throws NumberFormatException { return parseDouble(array, 0, array.length, useFastParser); @@ -418,7 +418,7 @@ public static double parseDouble(final char[] array, final boolean useFastParser * @param useFastParser whether to use {@code FastDoubleParser} * @return closest matching double * @throws NumberFormatException if value cannot be represented by a double - * @since v2.18 + * @since 2.18 */ public static double parseDouble(final char[] array, final int offset, final int len, final boolean useFastParser) throws NumberFormatException { @@ -459,7 +459,7 @@ public static float parseFloat(final String s, final boolean useFastParser) thro * @param useFastParser whether to use {@code FastDoubleParser} * @return closest matching float * @throws NumberFormatException if value cannot be represented by a float - * @since v2.18 + * @since 2.18 */ public static float parseFloat(final char[] array, final boolean useFastParser) throws NumberFormatException { return parseFloat(array, 0, array.length, useFastParser); @@ -472,7 +472,7 @@ public static float parseFloat(final char[] array, final boolean useFastParser) * @param useFastParser whether to use {@code FastDoubleParser} * @return closest matching float * @throws NumberFormatException if value cannot be represented by a float - * @since v2.18 + * @since 2.18 */ public static float parseFloat(final char[] array, final int offset, final int len, final boolean useFastParser) throws NumberFormatException { diff --git a/src/main/java/com/fasterxml/jackson/core/util/ReadConstrainedTextBuffer.java b/src/main/java/com/fasterxml/jackson/core/util/ReadConstrainedTextBuffer.java index fcc739d5c7..40f98253c6 100644 --- a/src/main/java/com/fasterxml/jackson/core/util/ReadConstrainedTextBuffer.java +++ b/src/main/java/com/fasterxml/jackson/core/util/ReadConstrainedTextBuffer.java @@ -26,13 +26,4 @@ protected void validateStringLength(int length) throws StreamConstraintsExceptio { _streamReadConstraints.validateStringLength(length); } - - /** - * {@inheritDoc} - */ - @Override - protected void validateFPLength(int length) throws StreamConstraintsException - { - _streamReadConstraints.validateFPLength(length); - } } 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 5122961875..70479c681a 100644 --- a/src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java +++ b/src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java @@ -539,7 +539,6 @@ public double contentsAsDouble(final boolean useFastParser) throws NumberFormatE if (_resultString == null) { // Has array been requested? Can make a shortcut, if so: if (_resultArray != null) { - validateFPLength(_resultArray.length); return NumberInput.parseDouble(_resultArray, useFastParser); } else { // Do we use shared array? @@ -548,7 +547,6 @@ public double contentsAsDouble(final boolean useFastParser) throws NumberFormatE _resultString = ""; throw new NumberFormatException("empty content"); } - validateFPLength(_inputLen); return NumberInput.parseDouble(_inputBuffer, _inputStart, _inputLen, useFastParser); } else { // nope... need to copy // But first, let's see if we have just one buffer @@ -560,7 +558,6 @@ public double contentsAsDouble(final boolean useFastParser) throws NumberFormatE _resultString = ""; throw new NumberFormatException("empty content"); } else { - validateFPLength(currLen); return NumberInput.parseDouble(_currentSegment, 0, currLen, useFastParser); } } @@ -618,7 +615,6 @@ public float contentsAsFloat(final boolean useFastParser) throws NumberFormatExc if (_resultString == null) { // Has array been requested? Can make a shortcut, if so: if (_resultArray != null) { - validateFPLength(_resultArray.length); return NumberInput.parseFloat(_resultArray, useFastParser); } else { // Do we use shared array? @@ -627,7 +623,6 @@ public float contentsAsFloat(final boolean useFastParser) throws NumberFormatExc _resultString = ""; throw new NumberFormatException("empty content"); } - validateFPLength(_inputLen); return NumberInput.parseFloat(_inputBuffer, _inputStart, _inputLen, useFastParser); } else { // nope... need to copy // But first, let's see if we have just one buffer @@ -639,7 +634,6 @@ public float contentsAsFloat(final boolean useFastParser) throws NumberFormatExc _resultString = ""; throw new NumberFormatException("empty content"); } else { - validateFPLength(currLen); return NumberInput.parseFloat(_currentSegment, 0, currLen, useFastParser); } } @@ -1253,21 +1247,4 @@ protected void validateStringLength(int length) throws IOException { // no-op } - - /** - * Convenience method that can be used to verify that a number - * of specified length does not exceed maximum specific by this - * constraints object: if it does, a - * {@link JsonParseException} - * is thrown. - * - * @param length Length of number in input units - * - * @throws IOException If length exceeds maximum - * @since 2.15 - */ - protected void validateFPLength(int length) throws IOException - { - // no-op - } } diff --git a/src/test/java/com/fasterxml/jackson/core/util/TestTextBuffer.java b/src/test/java/com/fasterxml/jackson/core/util/TestTextBuffer.java index 6daf5a2ffc..5cf145b333 100644 --- a/src/test/java/com/fasterxml/jackson/core/util/TestTextBuffer.java +++ b/src/test/java/com/fasterxml/jackson/core/util/TestTextBuffer.java @@ -2,6 +2,8 @@ import org.junit.jupiter.api.Test; +import java.io.IOException; + import static org.junit.jupiter.api.Assertions.*; class TestTextBuffer @@ -211,4 +213,62 @@ void getSizeFinishCurrentSegmentAndResetWith() throws Exception { assertEquals(2, textBuffer.size()); } + public void testEmptyContentsAsFloat() { + TextBuffer textBuffer = new TextBuffer(null); + assertThrows(NumberFormatException.class, () -> { + textBuffer.contentsAsFloat(false); + }); + } + + public void testEmptyContentsAsFloatFastParser() { + TextBuffer textBuffer = new TextBuffer(null); + assertThrows(NumberFormatException.class, () -> { + textBuffer.contentsAsFloat(true); + }); + } + + public void testContentsAsFloat() throws IOException { + TextBuffer textBuffer = new TextBuffer(null); + textBuffer.resetWithString("1.2345678"); + assertEquals(1.2345678f, textBuffer.contentsAsFloat(false)); + } + + public void testContentsAsFloatFastParser() throws IOException { + TextBuffer textBuffer = new TextBuffer(null); + textBuffer.resetWithString("1.2345678"); + assertEquals(1.2345678f, textBuffer.contentsAsFloat(true)); + } + + public void testEmptyContentsAsDouble() { + TextBuffer textBuffer = new TextBuffer(null); + assertThrows(NumberFormatException.class, () -> { + textBuffer.contentsAsDouble(false); + }); + } + + public void testEmptyContentsAsDoubleFastParser() { + TextBuffer textBuffer = new TextBuffer(null); + assertThrows(NumberFormatException.class, () -> { + textBuffer.contentsAsDouble(true); + }); + } + + public void testContentsAsDouble() throws IOException { + TextBuffer textBuffer = new TextBuffer(null); + textBuffer.resetWithString("1.234567890123456789"); + assertEquals(1.234567890123456789d, textBuffer.contentsAsDouble(false)); + } + + public void testContentsAsDoubleFastParser() throws IOException { + TextBuffer textBuffer = new TextBuffer(null); + textBuffer.resetWithString("1.234567890123456789"); + assertEquals(1.234567890123456789d, textBuffer.contentsAsDouble(true)); + } + + public void testEmptyContentsAsDecimal() { + TextBuffer textBuffer = new TextBuffer(null); + assertThrows(NumberFormatException.class, () -> { + textBuffer.contentsAsDecimal(); + }); + } } From bab93695b4521cd3440128267a10c0731bf8cc51 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 19 Apr 2024 20:52:20 -0700 Subject: [PATCH 5/7] Simplify implementation slightly --- .../jackson/core/util/TextBuffer.java | 126 +++++++++--------- .../jackson/core/util/TestTextBuffer.java | 37 +---- 2 files changed, 61 insertions(+), 102 deletions(-) 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 70479c681a..56d21a9504 100644 --- a/src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java +++ b/src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java @@ -355,12 +355,11 @@ private char[] buf(int needed) private void clearSegments() { _hasSegments = false; - /* Let's start using _last_ segment from list; for one, it's - * the biggest one, and it's also most likely to be cached - */ - /* 28-Aug-2009, tatu: Actually, the current segment should - * be the biggest one, already - */ + // Let's start using _last_ segment from list; for one, it's + // the biggest one, and it's also most likely to be cached + + // 28-Aug-2009, tatu: Actually, the current segment should + // be the biggest one, already //_currentSegment = _segments.get(_segments.size() - 1); _segments.clear(); _currentSize = _segmentSize = 0; @@ -526,44 +525,42 @@ public char[] contentsAsArray() throws IOException { /** * Convenience method for converting contents of the buffer * into a Double value. + *

+ * NOTE! Caller MUST validate contents before calling this method, + * to ensure textual version is valid JSON floating-point token -- this + * method is not guaranteed to do any validation and behavior with invalid + * content is not defined (either throws an exception or returns arbitrary + * number). * * @param useFastParser whether to use {@code FastDoubleParser} * @return Buffered text value parsed as a {@link Double}, if possible * - * @throws NumberFormatException if contents are not a valid Java number + * @throws NumberFormatException may (but is not guaranteed!) be thrown + * if contents are not a valid JSON floating-point number representation * * @since 2.14 */ - public double contentsAsDouble(final boolean useFastParser) throws NumberFormatException { + public double contentsAsDouble(final boolean useFastParser) throws NumberFormatException + { + // Order in which check is somewhat arbitrary... try likeliest ones + // that do not require allocation first, starting with likeliest + + if (_inputStart >= 0) { // shared? + return NumberInput.parseDouble(_inputBuffer, _inputStart, _inputLen, useFastParser); + } + if (_resultArray != null) { + return NumberInput.parseDouble(_resultArray, useFastParser); + } + // note: must check "_resultString" before segmented + if (_resultString != null) { + return NumberInput.parseDouble(_resultString, useFastParser); + } + if (_currentSize == 0) { // all content in current segment! + return NumberInput.parseDouble(_currentSegment, 0, _currentSize, useFastParser); + } + + // Otherwise, segmented so need to use slow path try { - if (_resultString == null) { - // Has array been requested? Can make a shortcut, if so: - if (_resultArray != null) { - return NumberInput.parseDouble(_resultArray, useFastParser); - } else { - // Do we use shared array? - if (_inputStart >= 0) { - if (_inputLen < 1) { - _resultString = ""; - throw new NumberFormatException("empty content"); - } - return NumberInput.parseDouble(_inputBuffer, _inputStart, _inputLen, useFastParser); - } else { // nope... need to copy - // But first, let's see if we have just one buffer - int segLen = _segmentSize; - int currLen = _currentSize; - - if (segLen == 0) { // yup - if (currLen == 0) { - _resultString = ""; - throw new NumberFormatException("empty content"); - } else { - return NumberInput.parseDouble(_currentSegment, 0, currLen, useFastParser); - } - } - } - } - } return NumberInput.parseDouble(contentsAsString(), useFastParser); } catch (IOException e) { // JsonParseException is used to denote a string that is too long @@ -603,43 +600,40 @@ public float contentsAsFloat() throws NumberFormatException { /** * Convenience method for converting contents of the buffer * into a Float value. + *

+ * NOTE! Caller MUST validate contents before calling this method, + * to ensure textual version is valid JSON floating-point token -- this + * method is not guaranteed to do any validation and behavior with invalid + * content is not defined (either throws an exception or returns arbitrary + * number). * * @param useFastParser whether to use {@code FastDoubleParser} * @return Buffered text value parsed as a {@link Float}, if possible * - * @throws NumberFormatException if contents are not a valid Java number + * @throws NumberFormatException may (but is not guaranteed!) be thrown + * if contents are not a valid JSON floating-point number representation + * * @since 2.14 */ - public float contentsAsFloat(final boolean useFastParser) throws NumberFormatException { + public float contentsAsFloat(final boolean useFastParser) throws NumberFormatException + { + // Order in which check is somewhat arbitrary... try likeliest ones + // that do not require allocation first, starting with likeliest + if (_inputStart >= 0) { // shared? + return NumberInput.parseFloat(_inputBuffer, _inputStart, _inputLen, useFastParser); + } + if (_resultArray != null) { + return NumberInput.parseFloat(_resultArray, useFastParser); + } + // note: must check "_resultString" before segmented + if (_resultString != null) { + return NumberInput.parseFloat(_resultString, useFastParser); + } + if (_currentSize == 0) { // all content in current segment! + return NumberInput.parseFloat(_currentSegment, 0, _currentSize, useFastParser); + } + // Otherwise, segmented so need to use slow path try { - if (_resultString == null) { - // Has array been requested? Can make a shortcut, if so: - if (_resultArray != null) { - return NumberInput.parseFloat(_resultArray, useFastParser); - } else { - // Do we use shared array? - if (_inputStart >= 0) { - if (_inputLen < 1) { - _resultString = ""; - throw new NumberFormatException("empty content"); - } - return NumberInput.parseFloat(_inputBuffer, _inputStart, _inputLen, useFastParser); - } else { // nope... need to copy - // But first, let's see if we have just one buffer - int segLen = _segmentSize; - int currLen = _currentSize; - - if (segLen == 0) { // yup - if (currLen == 0) { - _resultString = ""; - throw new NumberFormatException("empty content"); - } else { - return NumberInput.parseFloat(_currentSegment, 0, currLen, useFastParser); - } - } - } - } - } return NumberInput.parseFloat(contentsAsString(), useFastParser); } catch (IOException e) { // JsonParseException is used to denote a string that is too long diff --git a/src/test/java/com/fasterxml/jackson/core/util/TestTextBuffer.java b/src/test/java/com/fasterxml/jackson/core/util/TestTextBuffer.java index 5cf145b333..41a83a778b 100644 --- a/src/test/java/com/fasterxml/jackson/core/util/TestTextBuffer.java +++ b/src/test/java/com/fasterxml/jackson/core/util/TestTextBuffer.java @@ -7,7 +7,7 @@ import static org.junit.jupiter.api.Assertions.*; class TestTextBuffer - extends com.fasterxml.jackson.core.JUnit5TestBase + extends com.fasterxml.jackson.core.JUnit5TestBase { /** * Trivially simple basic test to ensure all basic append @@ -213,20 +213,6 @@ void getSizeFinishCurrentSegmentAndResetWith() throws Exception { assertEquals(2, textBuffer.size()); } - public void testEmptyContentsAsFloat() { - TextBuffer textBuffer = new TextBuffer(null); - assertThrows(NumberFormatException.class, () -> { - textBuffer.contentsAsFloat(false); - }); - } - - public void testEmptyContentsAsFloatFastParser() { - TextBuffer textBuffer = new TextBuffer(null); - assertThrows(NumberFormatException.class, () -> { - textBuffer.contentsAsFloat(true); - }); - } - public void testContentsAsFloat() throws IOException { TextBuffer textBuffer = new TextBuffer(null); textBuffer.resetWithString("1.2345678"); @@ -239,20 +225,6 @@ public void testContentsAsFloatFastParser() throws IOException { assertEquals(1.2345678f, textBuffer.contentsAsFloat(true)); } - public void testEmptyContentsAsDouble() { - TextBuffer textBuffer = new TextBuffer(null); - assertThrows(NumberFormatException.class, () -> { - textBuffer.contentsAsDouble(false); - }); - } - - public void testEmptyContentsAsDoubleFastParser() { - TextBuffer textBuffer = new TextBuffer(null); - assertThrows(NumberFormatException.class, () -> { - textBuffer.contentsAsDouble(true); - }); - } - public void testContentsAsDouble() throws IOException { TextBuffer textBuffer = new TextBuffer(null); textBuffer.resetWithString("1.234567890123456789"); @@ -264,11 +236,4 @@ public void testContentsAsDoubleFastParser() throws IOException { textBuffer.resetWithString("1.234567890123456789"); assertEquals(1.234567890123456789d, textBuffer.contentsAsDouble(true)); } - - public void testEmptyContentsAsDecimal() { - TextBuffer textBuffer = new TextBuffer(null); - assertThrows(NumberFormatException.class, () -> { - textBuffer.contentsAsDecimal(); - }); - } } From 4100902cedd93c55b1c4d0b9d92a962a35058dc2 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 19 Apr 2024 20:56:38 -0700 Subject: [PATCH 6/7] More tweaking --- .../jackson/core/util/TextBuffer.java | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) 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 56d21a9504..6d36c8e32a 100644 --- a/src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java +++ b/src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java @@ -543,21 +543,21 @@ public char[] contentsAsArray() throws IOException { public double contentsAsDouble(final boolean useFastParser) throws NumberFormatException { // Order in which check is somewhat arbitrary... try likeliest ones - // that do not require allocation first, starting with likeliest - - if (_inputStart >= 0) { // shared? - return NumberInput.parseDouble(_inputBuffer, _inputStart, _inputLen, useFastParser); - } - if (_resultArray != null) { - return NumberInput.parseDouble(_resultArray, useFastParser); - } - // note: must check "_resultString" before segmented + // that do not require allocation first + + // except _resultString first since it works best with JDK (non-fast parser) if (_resultString != null) { return NumberInput.parseDouble(_resultString, useFastParser); } + if (_inputStart >= 0) { // shared? + return NumberInput.parseDouble(_inputBuffer, _inputStart, _inputLen, useFastParser); + } if (_currentSize == 0) { // all content in current segment! return NumberInput.parseDouble(_currentSegment, 0, _currentSize, useFastParser); } + if (_resultArray != null) { + return NumberInput.parseDouble(_resultArray, useFastParser); + } // Otherwise, segmented so need to use slow path try { @@ -618,20 +618,22 @@ public float contentsAsFloat() throws NumberFormatException { public float contentsAsFloat(final boolean useFastParser) throws NumberFormatException { // Order in which check is somewhat arbitrary... try likeliest ones - // that do not require allocation first, starting with likeliest - if (_inputStart >= 0) { // shared? - return NumberInput.parseFloat(_inputBuffer, _inputStart, _inputLen, useFastParser); - } - if (_resultArray != null) { - return NumberInput.parseFloat(_resultArray, useFastParser); - } - // note: must check "_resultString" before segmented + // that do not require allocation first + + // except _resultString first since it works best with JDK (non-fast parser) if (_resultString != null) { return NumberInput.parseFloat(_resultString, useFastParser); } + if (_inputStart >= 0) { // shared? + return NumberInput.parseFloat(_inputBuffer, _inputStart, _inputLen, useFastParser); + } if (_currentSize == 0) { // all content in current segment! return NumberInput.parseFloat(_currentSegment, 0, _currentSize, useFastParser); } + if (_resultArray != null) { + return NumberInput.parseFloat(_resultArray, useFastParser); + } + // Otherwise, segmented so need to use slow path try { return NumberInput.parseFloat(contentsAsString(), useFastParser); From 04114a073c79ae5585c61767821cf0a8a585becc Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 19 Apr 2024 21:03:36 -0700 Subject: [PATCH 7/7] Update release notes --- release-notes/VERSION-2.x | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index cdaac7efb6..a98de131b0 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -16,14 +16,16 @@ a pure JSON library. 2.18.0 (not yet released) +#1230: Improve performance of `float` and `double` parsing from `TextBuffer` + (implemented by @pjfanning) #1251: `InternCache` replace synchronized with `ReentrantLock` - the cache size limit is no longer strictly enforced for performance reasons but we should never go far about the limit - (contributed by @pjfanning) + (implemented by @pjfanning) #1252: `ThreadLocalBufferManager` replace synchronized with `ReentrantLock` - (contributed by @pjfanning) + (implemented by @pjfanning) #1257: Increase InternCache default max size from 100 to 200 -#1262: Add diagnostic method pooledCount() in RecyclerPool +#1262: Add diagnostic method `pooledCount()` in `RecyclerPool` #1266: Change default recycler pool to `bewConcurrentDequePool()` in 2.18 2.17.1 (not yet released)