From 2b5c096ddf412241c320edccd207d089d5b79b21 Mon Sep 17 00:00:00 2001 From: lowasser Date: Thu, 4 Jun 2020 17:36:59 -0700 Subject: [PATCH] Implement BigIntegerMath.roundToDouble, which rounds to the nearest representable double value. Partially implements https://github.com/google/guava/issues/3895 RELNOTES=`math`: Added `BigIntegerMath.roundToDouble`. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=314837286 --- .../common/math/BigIntegerMathTest.java | 219 ++++++++++++++++++ .../google/common/math/BigIntegerMath.java | 59 ++++- .../google/common/math/ToDoubleRounder.java | 150 ++++++++++++ .../common/math/BigIntegerMathTest.java | 219 ++++++++++++++++++ .../google/common/math/BigIntegerMath.java | 59 ++++- .../google/common/math/ToDoubleRounder.java | 150 ++++++++++++ 6 files changed, 850 insertions(+), 6 deletions(-) create mode 100644 android/guava/src/com/google/common/math/ToDoubleRounder.java create mode 100644 guava/src/com/google/common/math/ToDoubleRounder.java diff --git a/android/guava-tests/test/com/google/common/math/BigIntegerMathTest.java b/android/guava-tests/test/com/google/common/math/BigIntegerMathTest.java index 33f3bcf4c788..8a9b4fcd39e7 100644 --- a/android/guava-tests/test/com/google/common/math/BigIntegerMathTest.java +++ b/android/guava-tests/test/com/google/common/math/BigIntegerMathTest.java @@ -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; @@ -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; @@ -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; /** @@ -542,6 +548,219 @@ public void testBinomialOutside() { } } + @GwtIncompatible + private static final class RoundToDoubleTester { + private final BigInteger input; + private final Map 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 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(); diff --git a/android/guava/src/com/google/common/math/BigIntegerMath.java b/android/guava/src/com/google/common/math/BigIntegerMath.java index b0c076652b5c..e3a07d6eaace 100644 --- a/android/guava/src/com/google/common/math/BigIntegerMath.java +++ b/android/guava/src/com/google/common/math/BigIntegerMath.java @@ -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; @@ -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)); } /** @@ -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. */ @@ -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}. + * + *

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}. + * + *

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 { + 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}. @@ -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; diff --git a/android/guava/src/com/google/common/math/ToDoubleRounder.java b/android/guava/src/com/google/common/math/ToDoubleRounder.java new file mode 100644 index 000000000000..be0faf3d2547 --- /dev/null +++ b/android/guava/src/com/google/common/math/ToDoubleRounder.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2020 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.common.math; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.math.MathPreconditions.checkRoundingUnnecessary; + +import com.google.common.annotations.GwtIncompatible; +import java.math.RoundingMode; + +/** + * Helper type to implement rounding {@code X} to a representable {@code double} value according to + * a {@link RoundingMode}. + */ +@GwtIncompatible +abstract class ToDoubleRounder> { + /** + * Returns x rounded to either the greatest double less than or equal to the precise value of x, + * or the least double greater than or equal to the precise value of x. + */ + abstract double roundToDoubleArbitrarily(X x); + + /** Returns the sign of x: either -1, 0, or 1. */ + abstract int sign(X x); + + /** Returns d's value as an X, rounded with the specified mode. */ + abstract X toX(double d, RoundingMode mode); + + /** Returns a - b, guaranteed that both arguments are nonnegative. */ + abstract X minus(X a, X b); + + /** Rounds {@code x} to a {@code double}. */ + final double roundToDouble(X x, RoundingMode mode) { + checkNotNull(x, "x"); + checkNotNull(mode, "mode"); + double roundArbitrarily = roundToDoubleArbitrarily(x); + if (Double.isInfinite(roundArbitrarily)) { + switch (mode) { + case DOWN: + case HALF_EVEN: + case HALF_DOWN: + case HALF_UP: + return Double.MAX_VALUE * sign(x); + case FLOOR: + return (roundArbitrarily == Double.POSITIVE_INFINITY) + ? Double.MAX_VALUE + : Double.NEGATIVE_INFINITY; + case CEILING: + return (roundArbitrarily == Double.POSITIVE_INFINITY) + ? Double.POSITIVE_INFINITY + : -Double.MAX_VALUE; + case UP: + return roundArbitrarily; + case UNNECESSARY: + throw new ArithmeticException(x + " cannot be represented precisely as a double"); + } + } + X roundArbitrarilyAsX = toX(roundArbitrarily, RoundingMode.UNNECESSARY); + int cmpXToRoundArbitrarily = x.compareTo(roundArbitrarilyAsX); + switch (mode) { + case UNNECESSARY: + checkRoundingUnnecessary(cmpXToRoundArbitrarily == 0); + return roundArbitrarily; + case FLOOR: + return (cmpXToRoundArbitrarily >= 0) + ? roundArbitrarily + : DoubleUtils.nextDown(roundArbitrarily); + case CEILING: + return (cmpXToRoundArbitrarily <= 0) ? roundArbitrarily : Math.nextUp(roundArbitrarily); + case DOWN: + if (sign(x) >= 0) { + return (cmpXToRoundArbitrarily >= 0) + ? roundArbitrarily + : DoubleUtils.nextDown(roundArbitrarily); + } else { + return (cmpXToRoundArbitrarily <= 0) ? roundArbitrarily : Math.nextUp(roundArbitrarily); + } + case UP: + if (sign(x) >= 0) { + return (cmpXToRoundArbitrarily <= 0) ? roundArbitrarily : Math.nextUp(roundArbitrarily); + } else { + return (cmpXToRoundArbitrarily >= 0) + ? roundArbitrarily + : DoubleUtils.nextDown(roundArbitrarily); + } + case HALF_DOWN: + case HALF_UP: + case HALF_EVEN: + { + X roundFloor; + double roundFloorAsDouble; + X roundCeiling; + double roundCeilingAsDouble; + + if (cmpXToRoundArbitrarily >= 0) { + roundFloorAsDouble = roundArbitrarily; + roundFloor = roundArbitrarilyAsX; + roundCeilingAsDouble = Math.nextUp(roundArbitrarily); + if (roundCeilingAsDouble == Double.POSITIVE_INFINITY) { + return roundFloorAsDouble; + } + roundCeiling = toX(roundCeilingAsDouble, RoundingMode.CEILING); + } else { + roundCeilingAsDouble = roundArbitrarily; + roundCeiling = roundArbitrarilyAsX; + roundFloorAsDouble = DoubleUtils.nextDown(roundArbitrarily); + if (roundFloorAsDouble == Double.NEGATIVE_INFINITY) { + return roundCeilingAsDouble; + } + roundFloor = toX(roundFloorAsDouble, RoundingMode.FLOOR); + } + + X deltaToFloor = minus(x, roundFloor); + X deltaToCeiling = minus(roundCeiling, x); + int diff = deltaToFloor.compareTo(deltaToCeiling); + if (diff < 0) { // closer to floor + return roundFloorAsDouble; + } else if (diff > 0) { // closer to ceiling + return roundCeilingAsDouble; + } + // halfway between the representable values; do the half-whatever logic + switch (mode) { + case HALF_EVEN: + return ((DoubleUtils.getSignificand(roundFloorAsDouble) & 1L) == 0) + ? roundFloorAsDouble + : roundCeilingAsDouble; + case HALF_DOWN: + return (sign(x) >= 0) ? roundFloorAsDouble : roundCeilingAsDouble; + case HALF_UP: + return (sign(x) >= 0) ? roundCeilingAsDouble : roundFloorAsDouble; + default: + throw new AssertionError("impossible"); + } + } + } + throw new AssertionError("impossible"); + } +} diff --git a/guava-tests/test/com/google/common/math/BigIntegerMathTest.java b/guava-tests/test/com/google/common/math/BigIntegerMathTest.java index 33f3bcf4c788..8a9b4fcd39e7 100644 --- a/guava-tests/test/com/google/common/math/BigIntegerMathTest.java +++ b/guava-tests/test/com/google/common/math/BigIntegerMathTest.java @@ -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; @@ -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; @@ -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; /** @@ -542,6 +548,219 @@ public void testBinomialOutside() { } } + @GwtIncompatible + private static final class RoundToDoubleTester { + private final BigInteger input; + private final Map 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 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(); diff --git a/guava/src/com/google/common/math/BigIntegerMath.java b/guava/src/com/google/common/math/BigIntegerMath.java index b0c076652b5c..e3a07d6eaace 100644 --- a/guava/src/com/google/common/math/BigIntegerMath.java +++ b/guava/src/com/google/common/math/BigIntegerMath.java @@ -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; @@ -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)); } /** @@ -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. */ @@ -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}. + * + *

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}. + * + *

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 { + 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}. @@ -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; diff --git a/guava/src/com/google/common/math/ToDoubleRounder.java b/guava/src/com/google/common/math/ToDoubleRounder.java new file mode 100644 index 000000000000..be0faf3d2547 --- /dev/null +++ b/guava/src/com/google/common/math/ToDoubleRounder.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2020 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.common.math; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.math.MathPreconditions.checkRoundingUnnecessary; + +import com.google.common.annotations.GwtIncompatible; +import java.math.RoundingMode; + +/** + * Helper type to implement rounding {@code X} to a representable {@code double} value according to + * a {@link RoundingMode}. + */ +@GwtIncompatible +abstract class ToDoubleRounder> { + /** + * Returns x rounded to either the greatest double less than or equal to the precise value of x, + * or the least double greater than or equal to the precise value of x. + */ + abstract double roundToDoubleArbitrarily(X x); + + /** Returns the sign of x: either -1, 0, or 1. */ + abstract int sign(X x); + + /** Returns d's value as an X, rounded with the specified mode. */ + abstract X toX(double d, RoundingMode mode); + + /** Returns a - b, guaranteed that both arguments are nonnegative. */ + abstract X minus(X a, X b); + + /** Rounds {@code x} to a {@code double}. */ + final double roundToDouble(X x, RoundingMode mode) { + checkNotNull(x, "x"); + checkNotNull(mode, "mode"); + double roundArbitrarily = roundToDoubleArbitrarily(x); + if (Double.isInfinite(roundArbitrarily)) { + switch (mode) { + case DOWN: + case HALF_EVEN: + case HALF_DOWN: + case HALF_UP: + return Double.MAX_VALUE * sign(x); + case FLOOR: + return (roundArbitrarily == Double.POSITIVE_INFINITY) + ? Double.MAX_VALUE + : Double.NEGATIVE_INFINITY; + case CEILING: + return (roundArbitrarily == Double.POSITIVE_INFINITY) + ? Double.POSITIVE_INFINITY + : -Double.MAX_VALUE; + case UP: + return roundArbitrarily; + case UNNECESSARY: + throw new ArithmeticException(x + " cannot be represented precisely as a double"); + } + } + X roundArbitrarilyAsX = toX(roundArbitrarily, RoundingMode.UNNECESSARY); + int cmpXToRoundArbitrarily = x.compareTo(roundArbitrarilyAsX); + switch (mode) { + case UNNECESSARY: + checkRoundingUnnecessary(cmpXToRoundArbitrarily == 0); + return roundArbitrarily; + case FLOOR: + return (cmpXToRoundArbitrarily >= 0) + ? roundArbitrarily + : DoubleUtils.nextDown(roundArbitrarily); + case CEILING: + return (cmpXToRoundArbitrarily <= 0) ? roundArbitrarily : Math.nextUp(roundArbitrarily); + case DOWN: + if (sign(x) >= 0) { + return (cmpXToRoundArbitrarily >= 0) + ? roundArbitrarily + : DoubleUtils.nextDown(roundArbitrarily); + } else { + return (cmpXToRoundArbitrarily <= 0) ? roundArbitrarily : Math.nextUp(roundArbitrarily); + } + case UP: + if (sign(x) >= 0) { + return (cmpXToRoundArbitrarily <= 0) ? roundArbitrarily : Math.nextUp(roundArbitrarily); + } else { + return (cmpXToRoundArbitrarily >= 0) + ? roundArbitrarily + : DoubleUtils.nextDown(roundArbitrarily); + } + case HALF_DOWN: + case HALF_UP: + case HALF_EVEN: + { + X roundFloor; + double roundFloorAsDouble; + X roundCeiling; + double roundCeilingAsDouble; + + if (cmpXToRoundArbitrarily >= 0) { + roundFloorAsDouble = roundArbitrarily; + roundFloor = roundArbitrarilyAsX; + roundCeilingAsDouble = Math.nextUp(roundArbitrarily); + if (roundCeilingAsDouble == Double.POSITIVE_INFINITY) { + return roundFloorAsDouble; + } + roundCeiling = toX(roundCeilingAsDouble, RoundingMode.CEILING); + } else { + roundCeilingAsDouble = roundArbitrarily; + roundCeiling = roundArbitrarilyAsX; + roundFloorAsDouble = DoubleUtils.nextDown(roundArbitrarily); + if (roundFloorAsDouble == Double.NEGATIVE_INFINITY) { + return roundCeilingAsDouble; + } + roundFloor = toX(roundFloorAsDouble, RoundingMode.FLOOR); + } + + X deltaToFloor = minus(x, roundFloor); + X deltaToCeiling = minus(roundCeiling, x); + int diff = deltaToFloor.compareTo(deltaToCeiling); + if (diff < 0) { // closer to floor + return roundFloorAsDouble; + } else if (diff > 0) { // closer to ceiling + return roundCeilingAsDouble; + } + // halfway between the representable values; do the half-whatever logic + switch (mode) { + case HALF_EVEN: + return ((DoubleUtils.getSignificand(roundFloorAsDouble) & 1L) == 0) + ? roundFloorAsDouble + : roundCeilingAsDouble; + case HALF_DOWN: + return (sign(x) >= 0) ? roundFloorAsDouble : roundCeilingAsDouble; + case HALF_UP: + return (sign(x) >= 0) ? roundCeilingAsDouble : roundFloorAsDouble; + default: + throw new AssertionError("impossible"); + } + } + } + throw new AssertionError("impossible"); + } +}