From 64a9e4c4d67d605eb6d0ef7cde1610021cf9ce3f Mon Sep 17 00:00:00 2001 From: John DeRegnaucourt Date: Tue, 16 Apr 2024 01:46:29 -0400 Subject: [PATCH] New algo to support parsing large numbers and returning the smallest data type between Long, BigInteger, Double, and BigDecimal. Very useful for JSON processing. --- .../com/cedarsoftware/util/MathUtilities.java | 63 +++++++++++ .../cedarsoftware/util/TestMathUtilities.java | 104 ++++++++++++++++-- 2 files changed, 156 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/cedarsoftware/util/MathUtilities.java b/src/main/java/com/cedarsoftware/util/MathUtilities.java index 5b72b094..3f8a95df 100644 --- a/src/main/java/com/cedarsoftware/util/MathUtilities.java +++ b/src/main/java/com/cedarsoftware/util/MathUtilities.java @@ -24,6 +24,11 @@ */ public final class MathUtilities { + public static final BigInteger BIG_INT_LONG_MIN = BigInteger.valueOf(Long.MIN_VALUE); + public static final BigInteger BIG_INT_LONG_MAX = BigInteger.valueOf(Long.MAX_VALUE); + public static final BigDecimal BIG_DEC_DOUBLE_MIN = BigDecimal.valueOf(-Double.MAX_VALUE); + public static final BigDecimal BIG_DEC_DOUBLE_MAX = BigDecimal.valueOf(Double.MAX_VALUE); + private MathUtilities() { super(); @@ -228,4 +233,62 @@ public static BigDecimal maximum(BigDecimal... values) return current; } + + /** + * Parse the passed in String as a numeric value and return the minimal data type between Long, Double, + * BigDecimal, or BigInteger. Useful for processing values from JSON files. + * @param numStr String to parse. + * @return Long, BigInteger, Double, or BigDecimal depending on the value. If the value is and integer and + * between the range of Long min/max, a Long is returned. If the value is an integer and outside this range, a + * BigInteger is returned. If the value is a decimal but within the confines of a Double, then a Double is + * returned, otherwise a BigDecimal is returned. + */ + public static Number parseToMinimalNumericType(String numStr) { + // Handle and preserve negative signs correctly while removing leading zeros + boolean isNegative = numStr.startsWith("-"); + if (isNegative || numStr.startsWith("+")) { + char sign = numStr.charAt(0); + numStr = sign + numStr.substring(1).replaceFirst("^0+", ""); + } else { + numStr = numStr.replaceFirst("^0+", ""); + } + + boolean hasDecimalPoint = false; + boolean hasExponent = false; + int mantissaSize = 0; + StringBuilder exponentValue = new StringBuilder(); + + for (int i = 0; i < numStr.length(); i++) { + char c = numStr.charAt(i); + if (c == '.') { + hasDecimalPoint = true; + } else if (c == 'e' || c == 'E') { + hasExponent = true; + } else if (c >= '0' && c <= '9') { + if (!hasExponent) { + mantissaSize++; // Count digits in the mantissa only + } else { + exponentValue.append(c); + } + } + } + + if (hasDecimalPoint || hasExponent) { + if (mantissaSize < 17 && (exponentValue.length() == 0 || Math.abs(Integer.parseInt(exponentValue.toString())) < 308)) { + return Double.parseDouble(numStr); + } else { + return new BigDecimal(numStr); + } + } else { + if (numStr.length() < 19) { + return Long.parseLong(numStr); + } + BigInteger bigInt = new BigInteger(numStr); + if (bigInt.compareTo(BIG_INT_LONG_MIN) >= 0 && bigInt.compareTo(BIG_INT_LONG_MAX) <= 0) { + return bigInt.longValue(); // Correctly convert BigInteger back to Long if within range + } else { + return bigInt; + } + } + } } diff --git a/src/test/java/com/cedarsoftware/util/TestMathUtilities.java b/src/test/java/com/cedarsoftware/util/TestMathUtilities.java index a256ae41..70b25b2f 100644 --- a/src/test/java/com/cedarsoftware/util/TestMathUtilities.java +++ b/src/test/java/com/cedarsoftware/util/TestMathUtilities.java @@ -5,8 +5,10 @@ import java.math.BigDecimal; import java.math.BigInteger; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import static com.cedarsoftware.util.MathUtilities.parseToMinimalNumericType; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.fail; @@ -28,10 +30,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -public class TestMathUtilities +class TestMathUtilities { @Test - public void testConstructorIsPrivate() throws Exception { + void testConstructorIsPrivate() throws Exception { Class c = MathUtilities.class; assertEquals(Modifier.FINAL, c.getModifiers() & Modifier.FINAL); @@ -43,7 +45,7 @@ public void testConstructorIsPrivate() throws Exception { } @Test - public void testMinimumLong() + void testMinimumLong() { long min = MathUtilities.minimum(0, 1, 2); assertEquals(0, min); @@ -71,7 +73,7 @@ public void testMinimumLong() } @Test - public void testMinimumDouble() + void testMinimumDouble() { double min = MathUtilities.minimum(0.1, 1.1, 2.1); assertEquals(0.1, min); @@ -99,7 +101,7 @@ public void testMinimumDouble() } @Test - public void testMinimumBigInteger() + void testMinimumBigInteger() { BigInteger minBi = MathUtilities.minimum(new BigInteger("-1"), new BigInteger("0"), new BigInteger("1")); assertEquals(new BigInteger("-1"), minBi); @@ -127,7 +129,7 @@ public void testMinimumBigInteger() } @Test - public void testMinimumBigDecimal() + void testMinimumBigDecimal() { BigDecimal minBd = MathUtilities.minimum(new BigDecimal("-1"), new BigDecimal("0"), new BigDecimal("1")); assertEquals(new BigDecimal("-1"), minBd); @@ -154,7 +156,7 @@ public void testMinimumBigDecimal() } @Test - public void testMaximumLong() + void testMaximumLong() { long max = MathUtilities.maximum(0, 1, 2); assertEquals(2, max); @@ -182,7 +184,7 @@ public void testMaximumLong() } @Test - public void testMaximumDouble() + void testMaximumDouble() { double max = MathUtilities.maximum(0.1, 1.1, 2.1); assertEquals(2.1, max); @@ -210,7 +212,7 @@ public void testMaximumDouble() } @Test - public void testMaximumBigInteger() + void testMaximumBigInteger() { BigInteger minBi = MathUtilities.minimum(new BigInteger("-1"), new BigInteger("0"), new BigInteger("1")); assertEquals(new BigInteger("-1"), minBi); @@ -238,7 +240,7 @@ public void testMaximumBigInteger() } @Test - public void testNullInMaximumBigInteger() + void testNullInMaximumBigInteger() { try { @@ -249,7 +251,7 @@ public void testNullInMaximumBigInteger() } @Test - public void testMaximumBigDecimal() + void testMaximumBigDecimal() { BigDecimal minBd = MathUtilities.maximum(new BigDecimal("-1"), new BigDecimal("0"), new BigDecimal("1")); assertEquals(new BigDecimal("1"), minBd); @@ -275,4 +277,84 @@ public void testMaximumBigDecimal() } catch (Exception ignored) { } } + + @Test + void testMaxLongBoundary() { + String maxLong = String.valueOf(Long.MAX_VALUE); + assertEquals(Long.MAX_VALUE, parseToMinimalNumericType(maxLong)); + } + + @Test + void testMinLongBoundary() { + String minLong = String.valueOf(Long.MIN_VALUE); + assertEquals(Long.MIN_VALUE, parseToMinimalNumericType(minLong)); + } + + @Test + void testBeyondMaxLongBoundary() { + String beyondMaxLong = "9223372036854775808"; // Long.MAX_VALUE + 1 + assertEquals(new BigInteger("9223372036854775808"), parseToMinimalNumericType(beyondMaxLong)); + } + + @Test + void testBeyondMinLongBoundary() { + String beyondMinLong = "-9223372036854775809"; // Long.MIN_VALUE - 1 + assertEquals(new BigInteger("-9223372036854775809"), parseToMinimalNumericType(beyondMinLong)); + } + + @Test + void testBeyondMaxDoubleBoundary() { + String beyondMaxDouble = "1e309"; // A value larger than Double.MAX_VALUE + assertEquals(new BigDecimal("1e309"), parseToMinimalNumericType(beyondMaxDouble)); + } + + @Test + void testShouldSwitchToBigDec() { + String maxDoubleSci = "8.7976931348623157e308"; // Double.MAX_VALUE in scientific notation + assertEquals(new BigDecimal(maxDoubleSci), parseToMinimalNumericType(maxDoubleSci)); + } + + @Test + void testInvalidScientificNotationExceedingDouble() { + String invalidSci = "1e1024"; // Exceeds maximum exponent for Double + assertEquals(new BigDecimal(invalidSci), parseToMinimalNumericType(invalidSci)); + } + + @Test + void testExponentWithLeadingZeros() + { + String s = "1.45e+0000000000000000000000307"; + Number d = parseToMinimalNumericType(s); + assert d instanceof Double; + } + + // The very edges are hard to hit, without expensive additional processing to detect there difference in + // Examples like this: "12345678901234567890.12345678901234567890" needs to be a BigDecimal, but Double + // will parse this correctly in it's short handed notation. My algorithm catches these. However, the values + // right near e+308 positive or negative will be returned as BigDecimals to ensure accuracy + @Disabled + @Test + void testMaxDoubleScientificNotation() { + String maxDoubleSci = "1.7976931348623157e308"; // Double.MAX_VALUE in scientific notation + assertEquals(Double.parseDouble(maxDoubleSci), parseToMinimalNumericType(maxDoubleSci)); + } + + @Disabled + @Test + void testMaxDoubleBoundary() { + assertEquals(Double.MAX_VALUE, parseToMinimalNumericType(Double.toString(Double.MAX_VALUE))); + } + + @Disabled + @Test + void testMinDoubleBoundary() { + assertEquals(-Double.MAX_VALUE, parseToMinimalNumericType(Double.toString(-Double.MAX_VALUE))); + } + + @Disabled + @Test + void testTinyDoubleScientificNotation() { + String tinyDoubleSci = "2.2250738585072014e-308"; // A very small double value + assertEquals(Double.parseDouble(tinyDoubleSci), parseToMinimalNumericType(tinyDoubleSci)); + } }