Skip to content

Commit

Permalink
New algo to support parsing large numbers and returning the smallest …
Browse files Browse the repository at this point in the history
…data type between Long, BigInteger, Double, and BigDecimal. Very useful for JSON processing.
  • Loading branch information
jdereg committed Apr 16, 2024
1 parent 1ea59f8 commit 64a9e4c
Show file tree
Hide file tree
Showing 2 changed files with 156 additions and 11 deletions.
63 changes: 63 additions & 0 deletions src/main/java/com/cedarsoftware/util/MathUtilities.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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;
}
}
}
}
104 changes: 93 additions & 11 deletions src/test/java/com/cedarsoftware/util/TestMathUtilities.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);

Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -154,7 +156,7 @@ public void testMinimumBigDecimal()
}

@Test
public void testMaximumLong()
void testMaximumLong()
{
long max = MathUtilities.maximum(0, 1, 2);
assertEquals(2, max);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -238,7 +240,7 @@ public void testMaximumBigInteger()
}

@Test
public void testNullInMaximumBigInteger()
void testNullInMaximumBigInteger()
{
try
{
Expand All @@ -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);
Expand All @@ -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));
}
}

0 comments on commit 64a9e4c

Please sign in to comment.