diff --git a/fastdoubleparser-dev/src/main/java/ch.randelshofer.fastdoubleparser/ch/randelshofer/fastdoubleparser/FastIntegerMath.java b/fastdoubleparser-dev/src/main/java/ch.randelshofer.fastdoubleparser/FastIntegerMath.java similarity index 89% rename from fastdoubleparser-dev/src/main/java/ch.randelshofer.fastdoubleparser/ch/randelshofer/fastdoubleparser/FastIntegerMath.java rename to fastdoubleparser-dev/src/main/java/ch.randelshofer.fastdoubleparser/FastIntegerMath.java index fb5022c4..ff61477d 100644 --- a/fastdoubleparser-dev/src/main/java/ch.randelshofer.fastdoubleparser/ch/randelshofer/fastdoubleparser/FastIntegerMath.java +++ b/fastdoubleparser-dev/src/main/java/ch.randelshofer.fastdoubleparser/FastIntegerMath.java @@ -91,12 +91,17 @@ static NavigableMap createPowersOfTenFloor16Map() { return powersOfTen; } - public static long estimateNumBits(long numDecimalDigits) { - // For the decimal number 10 we need log_2(10) = 3.3219 bits. - // The following formula uses 3.322 * 1024 = 3401.8 rounded up - // and adds 1, so that we overestimate but never underestimate - // the number of bits. - return (((numDecimalDigits * 3402L) >>> 10) + 1); + /** Gives the number of bits necessary to store given number of decimal digits. + * According to tests, overestimation is 1 bit at most for numbers like "999...", + * as the smallest one is "100..." additional 4 bits overestimation can occur. + * @param numDecimalDigits number of decimal digits, expected to be positive + * @return estimated number of bits + */ + public static long estimateNumBits(int numDecimalDigits) { + // For the decimal digit we need log_2(10) = 3.32192809488736234787 bits. + // The following formula uses 3.32192809488736234787 * 1073741824 (=2^30) = 3566893131.8 rounded up + // and adds 1, so that we overestimate but never underestimate the number of bits. + return (((numDecimalDigits * 3_566_893_132L) >>> 30) + 1); } /** diff --git a/fastdoubleparser-dev/src/test/java/ch/randelshofer/fastdoubleparser/FastIntegerMathTest.java b/fastdoubleparser-dev/src/test/java/ch/randelshofer/fastdoubleparser/FastIntegerMathTest.java index 40e09800..5402f63c 100644 --- a/fastdoubleparser-dev/src/test/java/ch/randelshofer/fastdoubleparser/FastIntegerMathTest.java +++ b/fastdoubleparser-dev/src/test/java/ch/randelshofer/fastdoubleparser/FastIntegerMathTest.java @@ -5,10 +5,67 @@ package ch.randelshofer.fastdoubleparser; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.provider.ValueSource; + +import java.math.BigInteger; +import java.util.Random; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; public class FastIntegerMathTest { + + @Test + void estimateNumBitsSmallValues() { + for (int i = 0; i < 500; i++) { // works for i < 119185 + long expected = digitsToBitsSlow(i); + long actual = FastIntegerMath.estimateNumBits(i); + assertEquals(actual, expected); + } + } + + @Test + void estimateNumBitsRandomValues() { + Random r = new Random(0); + for (int i = 0; i < 1000; i++) { + int x = r.nextInt(Integer.MAX_VALUE); // random positive value + long expected = digitsToBits(x); + long actual = FastIntegerMath.estimateNumBits(x); + assertEqualsOrLessByOne(actual, expected); + } + } + + @ParameterizedTest + @ValueSource(ints = {119185, 293637, 336249, 378861, 421473}) + void estimateNumBitsInaccurateValues(int inaccurate) { + assertEquals(digitsToBits(inaccurate) + 1, FastIntegerMath.estimateNumBits(inaccurate)); + } + + @Test + @DisabledIfSystemProperty(named = "enableLongRunningTests", matches = "^false$") + void estimateNumBitsAll() { + for (int i = 0; i < Integer.MAX_VALUE; i++) { + long expected = digitsToBits(i); + long actual = FastIntegerMath.estimateNumBits(i); + assertEqualsOrLessByOne(actual, expected); + } + } + + private static void assertEqualsOrLessByOne(long actual, long expected) { // actual >= expected + assertTrue(actual - expected >= 0,actual + " - " + expected + " < 0"); + assertTrue(actual - expected <= 1, actual + " - " + expected + " > 1"); + } + + // supposed to be precisely accurate + private static long digitsToBits(int numDecimalDigits) { + return (long)(3.32192809488736234787 * numDecimalDigits) + 1; + } + + // precisely accurate, but slow + private static long digitsToBitsSlow(int numDecimalDigits) { + return numDecimalDigits + BigInteger.valueOf(5).pow(numDecimalDigits).bitLength(); + } + @Test public void testFullMultiplication() { FastIntegerMath.UInt128 actual = FastIntegerMath.fullMultiplication(0x123456789ABCDEF0L, 0x10L);