Skip to content

Commit

Permalink
Add LongMath.roundToDouble.
Browse files Browse the repository at this point in the history
RELNOTES=Add LongMath.roundToDouble (#3895)

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=316951853
  • Loading branch information
lowasser authored and nick-someone committed Jun 18, 2020
1 parent bbf3e1b commit 633abf2
Show file tree
Hide file tree
Showing 4 changed files with 522 additions and 0 deletions.
164 changes: 164 additions & 0 deletions android/guava-tests/test/com/google/common/math/LongMathTest.java
Expand Up @@ -25,17 +25,28 @@
import static com.google.common.math.MathTesting.NONZERO_LONG_CANDIDATES;
import static com.google.common.math.MathTesting.POSITIVE_INTEGER_CANDIDATES;
import static com.google.common.math.MathTesting.POSITIVE_LONG_CANDIDATES;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static java.math.BigInteger.valueOf;
import static java.math.RoundingMode.CEILING;
import static java.math.RoundingMode.DOWN;
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.HALF_UP;
import static java.math.RoundingMode.UNNECESSARY;
import static java.math.RoundingMode.UP;
import static java.math.RoundingMode.values;

import com.google.common.annotations.GwtCompatible;
import com.google.common.annotations.GwtIncompatible;
import com.google.common.testing.NullPointerTester;
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 java.util.Random;
import junit.framework.TestCase;

Expand Down Expand Up @@ -949,6 +960,159 @@ public void testIsPrimeThrowsOnNegative() {
}
}

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

RoundToDoubleTester(long 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(LongMath.roundToDouble(input, mode))
.isEqualTo(expectation);
}

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

@GwtIncompatible
public void testRoundToDouble_zero() {
new RoundToDoubleTester(0).setExpectation(0.0, values()).test();
}

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

@GwtIncompatible
public void testRoundToDouble_maxPreciselyRepresentable() {
new RoundToDoubleTester(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.
// 2^53 is "more even" -- it's a multiple of a larger power of two -- so HALF_EVEN goes to it.
new RoundToDoubleTester((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_* will all go down.
new RoundToDoubleTester((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_* will all go up.
new RoundToDoubleTester((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((1L << 54) + 4).setExpectation(Math.pow(2, 54) + 4, values()).test();
}

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

@GwtIncompatible
public void testRoundToDouble_minPreciselyRepresentable() {
new RoundToDoubleTester(-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.
// -2^53 is "more even" -- a multiple of a greater power of two -- so HALF_EVEN will go to it.
new RoundToDoubleTester((-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((-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((-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((-1L << 54) - 4).setExpectation(-Math.pow(2, 54) - 4, values()).test();
}

private static void failFormat(String template, Object... args) {
assertWithMessage(template, args).fail();
}
Expand Down
97 changes: 97 additions & 0 deletions android/guava/src/com/google/common/math/LongMath.java
Expand Up @@ -29,6 +29,7 @@
import com.google.common.annotations.GwtCompatible;
import com.google.common.annotations.GwtIncompatible;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.primitives.Longs;
import com.google.common.primitives.UnsignedLongs;
import java.math.BigInteger;
import java.math.RoundingMode;
Expand Down Expand Up @@ -1203,5 +1204,101 @@ private boolean testWitness(long base, long n) {
}
}

/**
* 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_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
*/
@SuppressWarnings("deprecation")
@GwtIncompatible
public static double roundToDouble(long x, RoundingMode mode) {
// Logic copied from ToDoubleRounder. The repeated logic isn't ideal, but this doesn't box.
double roundArbitrarily = (double) x;
long roundArbitrarilyAsLong = (long) roundArbitrarily;
int cmpXToRoundArbitrarily = Longs.compare(x, roundArbitrarilyAsLong);
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 (x >= 0) {
return (cmpXToRoundArbitrarily >= 0)
? roundArbitrarily
: DoubleUtils.nextDown(roundArbitrarily);
} else {
return (cmpXToRoundArbitrarily <= 0) ? roundArbitrarily : Math.nextUp(roundArbitrarily);
}
case UP:
if (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:
{
long roundFloor;
double roundFloorAsDouble;
long roundCeiling;
double roundCeilingAsDouble;

if (cmpXToRoundArbitrarily >= 0) {
roundFloorAsDouble = roundArbitrarily;
roundFloor = roundArbitrarilyAsLong;
roundCeilingAsDouble = Math.nextUp(roundArbitrarily);
roundCeiling = (long) Math.ceil(roundCeilingAsDouble);
} else {
roundCeilingAsDouble = roundArbitrarily;
roundCeiling = roundArbitrarilyAsLong;
roundFloorAsDouble = DoubleUtils.nextDown(roundArbitrarily);
roundFloor = (long) Math.floor(roundFloorAsDouble);
}

long deltaToFloor = x - roundFloor;
long deltaToCeiling = roundCeiling - x;
int diff = Longs.compare(deltaToFloor, 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 (x >= 0) ? roundFloorAsDouble : roundCeilingAsDouble;
case HALF_UP:
return (x >= 0) ? roundCeilingAsDouble : roundFloorAsDouble;
default:
throw new AssertionError("impossible");
}
}
}
throw new AssertionError("impossible");
}

private LongMath() {}
}

0 comments on commit 633abf2

Please sign in to comment.