Skip to content

Commit

Permalink
Implement BigIntegerMath.roundToDouble, which rounds to the nearest r…
Browse files Browse the repository at this point in the history
…epresentable double value.

Partially implements #3895

RELNOTES=`math`: Added `BigIntegerMath.roundToDouble`.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=314837286
  • Loading branch information
lowasser authored and netdpb committed Jun 5, 2020
1 parent f87596b commit 2b5c096
Show file tree
Hide file tree
Showing 6 changed files with 850 additions and 6 deletions.
Expand Up @@ -22,6 +22,8 @@
import static com.google.common.math.MathTesting.NEGATIVE_BIGINTEGER_CANDIDATES;
import static com.google.common.math.MathTesting.NONZERO_BIGINTEGER_CANDIDATES;
import static com.google.common.math.MathTesting.POSITIVE_BIGINTEGER_CANDIDATES;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static java.math.BigInteger.ONE;
import static java.math.BigInteger.TEN;
import static java.math.BigInteger.ZERO;
Expand All @@ -33,6 +35,7 @@
import static java.math.RoundingMode.HALF_UP;
import static java.math.RoundingMode.UNNECESSARY;
import static java.math.RoundingMode.UP;
import static java.math.RoundingMode.values;
import static java.util.Arrays.asList;

import com.google.common.annotations.GwtCompatible;
Expand All @@ -41,6 +44,9 @@
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.Map;
import junit.framework.TestCase;

/**
Expand Down Expand Up @@ -542,6 +548,219 @@ public void testBinomialOutside() {
}
}

@GwtIncompatible
private static final class RoundToDoubleTester {
private final BigInteger input;
private final Map<RoundingMode, Double> expectedValues = new EnumMap<>(RoundingMode.class);
private boolean unnecessaryShouldThrow = false;

RoundToDoubleTester(BigInteger input) {
this.input = input;
}

RoundToDoubleTester setExpectation(double expectedValue, RoundingMode... modes) {
for (RoundingMode mode : modes) {
Double previous = expectedValues.put(mode, expectedValue);
if (previous != null) {
throw new AssertionError();
}
}
return this;
}

public RoundToDoubleTester roundUnnecessaryShouldThrow() {
unnecessaryShouldThrow = true;
return this;
}

public void test() {
assertThat(expectedValues.keySet())
.containsAtLeastElementsIn(EnumSet.complementOf(EnumSet.of(UNNECESSARY)));
for (Map.Entry<RoundingMode, Double> entry : expectedValues.entrySet()) {
RoundingMode mode = entry.getKey();
Double expectation = entry.getValue();
assertWithMessage("roundToDouble(" + input + ", " + mode + ")")
.that(BigIntegerMath.roundToDouble(input, mode))
.isEqualTo(expectation);
}

if (!expectedValues.containsKey(UNNECESSARY)) {
assertWithMessage("Expected roundUnnecessaryShouldThrow call")
.that(unnecessaryShouldThrow)
.isTrue();
try {
BigIntegerMath.roundToDouble(input, UNNECESSARY);
fail("Expected ArithmeticException for roundToDouble(" + input + ", UNNECESSARY)");
} catch (ArithmeticException expected) {
// expected
}
}
}
}

@GwtIncompatible
public void testRoundToDouble_Zero() {
new RoundToDoubleTester(BigInteger.ZERO).setExpectation(0.0, values()).test();
}

@GwtIncompatible
public void testRoundToDouble_smallPositive() {
new RoundToDoubleTester(BigInteger.valueOf(16)).setExpectation(16.0, values()).test();
}

@GwtIncompatible
public void testRoundToDouble_maxPreciselyRepresentable() {
new RoundToDoubleTester(BigInteger.valueOf(1L << 53))
.setExpectation(Math.pow(2, 53), values())
.test();
}

@GwtIncompatible
public void testRoundToDouble_maxPreciselyRepresentablePlusOne() {
double twoToThe53 = Math.pow(2, 53);
// the representable doubles are 2^53 and 2^53 + 2.
// 2^53+1 is halfway between, so HALF_UP will go up and HALF_DOWN will go down.
new RoundToDoubleTester(BigInteger.valueOf((1L << 53) + 1))
.setExpectation(twoToThe53, DOWN, FLOOR, HALF_DOWN, HALF_EVEN)
.setExpectation(Math.nextUp(twoToThe53), CEILING, UP, HALF_UP)
.roundUnnecessaryShouldThrow()
.test();
}

@GwtIncompatible
public void testRoundToDouble_twoToThe54PlusOne() {
double twoToThe54 = Math.pow(2, 54);
// the representable doubles are 2^54 and 2^54 + 4
// 2^54+1 is less than halfway between, so HALF_DOWN and HALF_UP will both go down.
new RoundToDoubleTester(BigInteger.valueOf((1L << 54) + 1))
.setExpectation(twoToThe54, DOWN, FLOOR, HALF_DOWN, HALF_UP, HALF_EVEN)
.setExpectation(Math.nextUp(twoToThe54), CEILING, UP)
.roundUnnecessaryShouldThrow()
.test();
}

@GwtIncompatible
public void testRoundToDouble_twoToThe54PlusThree() {
double twoToThe54 = Math.pow(2, 54);
// the representable doubles are 2^54 and 2^54 + 4
// 2^54+3 is more than halfway between, so HALF_DOWN and HALF_UP will both go up.
new RoundToDoubleTester(BigInteger.valueOf((1L << 54) + 3))
.setExpectation(twoToThe54, DOWN, FLOOR)
.setExpectation(Math.nextUp(twoToThe54), CEILING, UP, HALF_DOWN, HALF_UP, HALF_EVEN)
.roundUnnecessaryShouldThrow()
.test();
}

@GwtIncompatible
public void testRoundToDouble_twoToThe54PlusFour() {
new RoundToDoubleTester(BigInteger.valueOf((1L << 54) + 4))
.setExpectation(Math.pow(2, 54) + 4, values())
.test();
}

@GwtIncompatible
public void testRoundToDouble_maxDouble() {
BigInteger maxDoubleAsBI = DoubleMath.roundToBigInteger(Double.MAX_VALUE, UNNECESSARY);
new RoundToDoubleTester(maxDoubleAsBI).setExpectation(Double.MAX_VALUE, values()).test();
}

@GwtIncompatible
public void testRoundToDouble_maxDoublePlusOne() {
BigInteger maxDoubleAsBI =
DoubleMath.roundToBigInteger(Double.MAX_VALUE, UNNECESSARY).add(BigInteger.ONE);
new RoundToDoubleTester(maxDoubleAsBI)
.setExpectation(Double.MAX_VALUE, DOWN, FLOOR, HALF_EVEN, HALF_UP, HALF_DOWN)
.setExpectation(Double.POSITIVE_INFINITY, UP, CEILING)
.roundUnnecessaryShouldThrow()
.test();
}

@GwtIncompatible
public void testRoundToDouble_wayTooBig() {
BigInteger bi = BigInteger.ONE.shiftLeft(2 * Double.MAX_EXPONENT);
new RoundToDoubleTester(bi)
.setExpectation(Double.MAX_VALUE, DOWN, FLOOR, HALF_EVEN, HALF_UP, HALF_DOWN)
.setExpectation(Double.POSITIVE_INFINITY, UP, CEILING)
.roundUnnecessaryShouldThrow()
.test();
}

@GwtIncompatible
public void testRoundToDouble_smallNegative() {
new RoundToDoubleTester(BigInteger.valueOf(-16)).setExpectation(-16.0, values()).test();
}

@GwtIncompatible
public void testRoundToDouble_minPreciselyRepresentable() {
new RoundToDoubleTester(BigInteger.valueOf(-1L << 53))
.setExpectation(-Math.pow(2, 53), values())
.test();
}

@GwtIncompatible
public void testRoundToDouble_minPreciselyRepresentableMinusOne() {
// the representable doubles are -2^53 and -2^53 - 2.
// -2^53-1 is halfway between, so HALF_UP will go up and HALF_DOWN will go down.
new RoundToDoubleTester(BigInteger.valueOf((-1L << 53) - 1))
.setExpectation(-Math.pow(2, 53), DOWN, CEILING, HALF_DOWN, HALF_EVEN)
.setExpectation(DoubleUtils.nextDown(-Math.pow(2, 53)), FLOOR, UP, HALF_UP)
.roundUnnecessaryShouldThrow()
.test();
}

@GwtIncompatible
public void testRoundToDouble_negativeTwoToThe54MinusOne() {
new RoundToDoubleTester(BigInteger.valueOf((-1L << 54) - 1))
.setExpectation(-Math.pow(2, 54), DOWN, CEILING, HALF_DOWN, HALF_UP, HALF_EVEN)
.setExpectation(DoubleUtils.nextDown(-Math.pow(2, 54)), FLOOR, UP)
.roundUnnecessaryShouldThrow()
.test();
}

@GwtIncompatible
public void testRoundToDouble_negativeTwoToThe54MinusThree() {
new RoundToDoubleTester(BigInteger.valueOf((-1L << 54) - 3))
.setExpectation(-Math.pow(2, 54), DOWN, CEILING)
.setExpectation(
DoubleUtils.nextDown(-Math.pow(2, 54)), FLOOR, UP, HALF_DOWN, HALF_UP, HALF_EVEN)
.roundUnnecessaryShouldThrow()
.test();
}

@GwtIncompatible
public void testRoundToDouble_negativeTwoToThe54MinusFour() {
new RoundToDoubleTester(BigInteger.valueOf((-1L << 54) - 4))
.setExpectation(-Math.pow(2, 54) - 4, values())
.test();
}

@GwtIncompatible
public void testRoundToDouble_minDouble() {
BigInteger minDoubleAsBI = DoubleMath.roundToBigInteger(-Double.MAX_VALUE, UNNECESSARY);
new RoundToDoubleTester(minDoubleAsBI).setExpectation(-Double.MAX_VALUE, values()).test();
}

@GwtIncompatible
public void testRoundToDouble_minDoubleMinusOne() {
BigInteger minDoubleAsBI =
DoubleMath.roundToBigInteger(-Double.MAX_VALUE, UNNECESSARY).subtract(BigInteger.ONE);
new RoundToDoubleTester(minDoubleAsBI)
.setExpectation(-Double.MAX_VALUE, DOWN, CEILING, HALF_EVEN, HALF_UP, HALF_DOWN)
.setExpectation(Double.NEGATIVE_INFINITY, UP, FLOOR)
.roundUnnecessaryShouldThrow()
.test();
}

@GwtIncompatible
public void testRoundToDouble_negativeWayTooBig() {
BigInteger bi = BigInteger.ONE.shiftLeft(2 * Double.MAX_EXPONENT).negate();
new RoundToDoubleTester(bi)
.setExpectation(-Double.MAX_VALUE, DOWN, CEILING, HALF_EVEN, HALF_UP, HALF_DOWN)
.setExpectation(Double.NEGATIVE_INFINITY, UP, FLOOR)
.roundUnnecessaryShouldThrow()
.test();
}

@GwtIncompatible // NullPointerTester
public void testNullPointers() {
NullPointerTester tester = new NullPointerTester();
Expand Down
59 changes: 56 additions & 3 deletions android/guava/src/com/google/common/math/BigIntegerMath.java
Expand Up @@ -21,7 +21,9 @@
import static com.google.common.math.MathPreconditions.checkRoundingUnnecessary;
import static java.math.RoundingMode.CEILING;
import static java.math.RoundingMode.FLOOR;
import static java.math.RoundingMode.HALF_DOWN;
import static java.math.RoundingMode.HALF_EVEN;
import static java.math.RoundingMode.UNNECESSARY;

import com.google.common.annotations.Beta;
import com.google.common.annotations.GwtCompatible;
Expand Down Expand Up @@ -56,7 +58,7 @@ public final class BigIntegerMath {
*/
@Beta
public static BigInteger ceilingPowerOfTwo(BigInteger x) {
return BigInteger.ZERO.setBit(log2(x, RoundingMode.CEILING));
return BigInteger.ZERO.setBit(log2(x, CEILING));
}

/**
Expand All @@ -68,7 +70,7 @@ public static BigInteger ceilingPowerOfTwo(BigInteger x) {
*/
@Beta
public static BigInteger floorPowerOfTwo(BigInteger x) {
return BigInteger.ZERO.setBit(log2(x, RoundingMode.FLOOR));
return BigInteger.ZERO.setBit(log2(x, FLOOR));
}

/** Returns {@code true} if {@code x} represents a power of two. */
Expand Down Expand Up @@ -306,6 +308,57 @@ private static BigInteger sqrtApproxWithDoubles(BigInteger x) {
return DoubleMath.roundToBigInteger(Math.sqrt(DoubleUtils.bigToDouble(x)), HALF_EVEN);
}

/**
* Returns {@code x}, rounded to a {@code double} with the specified rounding mode. If {@code x}
* is precisely representable as a {@code double}, its {@code double} value will be returned;
* otherwise, the rounding will choose between the two nearest representable values with {@code
* mode}.
*
* <p>For the case of {@link RoundingMode#HALF_DOWN}, {@code HALF_UP}, and {@code HALF_EVEN},
* infinite {@code double} values are considered infinitely far away. For example, 2^2000 is not
* representable as a double, but {@code roundToDouble(BigInteger.valueOf(2).pow(2000), HALF_UP)}
* will return {@code Double.MAX_VALUE}, not {@code Double.POSITIVE_INFINITY}.
*
* <p>For the case of {@link RoundingMode#HALF_EVEN}, this implementation uses the IEEE 754
* default rounding mode: if the two nearest representable values are equally near, the one with
* the least significant bit zero is chosen. (In such cases, both of the nearest representable
* values are even integers; this method returns the one that is a multiple of a greater power of
* two.)
*
* @throws ArithmeticException if {@code mode} is {@link RoundingMode#UNNECESSARY} and {@code x}
* is not precisely representable as a {@code double}
* @since NEXT
*/
@GwtIncompatible
public static double roundToDouble(BigInteger x, RoundingMode mode) {
return BigIntegerToDoubleRounder.INSTANCE.roundToDouble(x, mode);
}

@GwtIncompatible
private static class BigIntegerToDoubleRounder extends ToDoubleRounder<BigInteger> {
private static final BigIntegerToDoubleRounder INSTANCE = new BigIntegerToDoubleRounder();

@Override
double roundToDoubleArbitrarily(BigInteger bigInteger) {
return DoubleUtils.bigToDouble(bigInteger);
}

@Override
int sign(BigInteger bigInteger) {
return bigInteger.signum();
}

@Override
BigInteger toX(double d, RoundingMode mode) {
return DoubleMath.roundToBigInteger(d, mode);
}

@Override
BigInteger minus(BigInteger a, BigInteger b) {
return a.subtract(b);
}
}

/**
* Returns the result of dividing {@code p} by {@code q}, rounding using the specified {@code
* RoundingMode}.
Expand Down Expand Up @@ -432,7 +485,7 @@ public static BigInteger binomial(int n, int k) {
long numeratorAccum = n;
long denominatorAccum = 1;

int bits = LongMath.log2(n, RoundingMode.CEILING);
int bits = LongMath.log2(n, CEILING);

int numeratorBits = bits;

Expand Down

0 comments on commit 2b5c096

Please sign in to comment.