Skip to content

Commit

Permalink
Implement BigDecimalMath.roundToDouble.
Browse files Browse the repository at this point in the history
RELNOTES=Add BigDecimalMath.roundToDouble.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=318342542
  • Loading branch information
lowasser authored and kluever committed Jun 30, 2020
1 parent 689d2b3 commit bee4f3c
Show file tree
Hide file tree
Showing 11 changed files with 805 additions and 4 deletions.
Expand Up @@ -88,4 +88,14 @@ int divide(int reps) {
}
return tmp;
}

@Benchmark
long roundToDouble(int reps) {
long tmp = 0;
for (int i = 0; i < reps; i++) {
int j = i & ARRAY_MASK;
tmp += Double.doubleToRawLongBits(BigIntegerMath.roundToDouble(nonzero1[j], mode));
}
return tmp;
}
}
@@ -0,0 +1,295 @@
/*
* 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.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
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 java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.Map;
import junit.framework.TestCase;

@GwtCompatible(emulated = true)
public class BigDecimalMathTest extends TestCase {
@GwtIncompatible
private static final class RoundToDoubleTester {
private final BigDecimal input;
private final Map<RoundingMode, Double> expectedValues = new EnumMap<>(RoundingMode.class);
private boolean unnecessaryShouldThrow = false;

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

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

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

@GwtIncompatible
public void testRoundToDouble_oneThird() {
new RoundToDoubleTester(
BigDecimal.ONE.divide(BigDecimal.valueOf(3), new MathContext(50, HALF_EVEN)))
.roundUnnecessaryShouldThrow()
.setExpectation(0.33333333333333337, UP, CEILING)
.setExpectation(0.3333333333333333, HALF_EVEN, FLOOR, DOWN, HALF_UP, HALF_DOWN)
.test();
}

@GwtIncompatible
public void testRoundToDouble_halfMinDouble() {
BigDecimal minDouble = new BigDecimal(Double.MIN_VALUE);
BigDecimal halfMinDouble = minDouble.divide(BigDecimal.valueOf(2));
new RoundToDoubleTester(halfMinDouble)
.roundUnnecessaryShouldThrow()
.setExpectation(Double.MIN_VALUE, UP, CEILING, HALF_UP)
.setExpectation(0.0, HALF_EVEN, FLOOR, DOWN, HALF_DOWN)
.test();
}

@GwtIncompatible
public void testRoundToDouble_halfNegativeMinDouble() {
BigDecimal minDouble = new BigDecimal(-Double.MIN_VALUE);
BigDecimal halfMinDouble = minDouble.divide(BigDecimal.valueOf(2));
new RoundToDoubleTester(halfMinDouble)
.roundUnnecessaryShouldThrow()
.setExpectation(-Double.MIN_VALUE, UP, FLOOR, HALF_UP)
.setExpectation(-0.0, HALF_EVEN, CEILING, DOWN, HALF_DOWN)
.test();
}

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

@GwtIncompatible
public void testRoundToDouble_maxPreciselyRepresentable() {
new RoundToDoubleTester(BigDecimal.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(BigDecimal.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(BigDecimal.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_twoToThe54PlusOneHalf() {
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(BigDecimal.valueOf(1L << 54).add(new BigDecimal(0.5)))
.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(BigDecimal.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(BigDecimal.valueOf((1L << 54) + 4))
.setExpectation(Math.pow(2, 54) + 4, values())
.test();
}

@GwtIncompatible
public void testRoundToDouble_maxDouble() {
BigDecimal maxDoubleAsBD = new BigDecimal(Double.MAX_VALUE);
new RoundToDoubleTester(maxDoubleAsBD).setExpectation(Double.MAX_VALUE, values()).test();
}

@GwtIncompatible
public void testRoundToDouble_maxDoublePlusOne() {
BigDecimal maxDoubleAsBD = new BigDecimal(Double.MAX_VALUE).add(BigDecimal.ONE);
new RoundToDoubleTester(maxDoubleAsBD)
.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() {
BigDecimal bi = BigDecimal.valueOf(2).pow(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(BigDecimal.valueOf(-16)).setExpectation(-16.0, values()).test();
}

@GwtIncompatible
public void testRoundToDouble_minPreciselyRepresentable() {
new RoundToDoubleTester(BigDecimal.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(BigDecimal.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(BigDecimal.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(BigDecimal.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(BigDecimal.valueOf((-1L << 54) - 4))
.setExpectation(-Math.pow(2, 54) - 4, values())
.test();
}

@GwtIncompatible
public void testRoundToDouble_minDouble() {
BigDecimal minDoubleAsBD = new BigDecimal(-Double.MAX_VALUE);
new RoundToDoubleTester(minDoubleAsBD).setExpectation(-Double.MAX_VALUE, values()).test();
}

@GwtIncompatible
public void testRoundToDouble_minDoubleMinusOne() {
BigDecimal minDoubleAsBD = new BigDecimal(-Double.MAX_VALUE).subtract(BigDecimal.ONE);
new RoundToDoubleTester(minDoubleAsBD)
.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() {
BigDecimal bi = BigDecimal.valueOf(2).pow(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();
}
}
81 changes: 81 additions & 0 deletions android/guava/src/com/google/common/math/BigDecimalMath.java
@@ -0,0 +1,81 @@
/*
* 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 com.google.common.annotations.GwtIncompatible;
import java.math.BigDecimal;
import java.math.RoundingMode;

/**
* A class for arithmetic on {@link BigDecimal} that is not covered by its built-in methods.
*
* @author Louis Wasserman
* @since NEXT
*/
@GwtIncompatible
public class BigDecimalMath {
private BigDecimalMath() {}

/**
* 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(BigDecimal.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
*/
public static double roundToDouble(BigDecimal x, RoundingMode mode) {
return BigDecimalToDoubleRounder.INSTANCE.roundToDouble(x, mode);
}

private static class BigDecimalToDoubleRounder extends ToDoubleRounder<BigDecimal> {
static final BigDecimalToDoubleRounder INSTANCE = new BigDecimalToDoubleRounder();

private BigDecimalToDoubleRounder() {}

@Override
double roundToDoubleArbitrarily(BigDecimal bigDecimal) {
return bigDecimal.doubleValue();
}

@Override
int sign(BigDecimal bigDecimal) {
return bigDecimal.signum();
}

@Override
BigDecimal toX(double d, RoundingMode mode) {
return new BigDecimal(d);
}

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

0 comments on commit bee4f3c

Please sign in to comment.