/
githubmirror
/
guava
Обзор
Документация
Войти
/
githubmirror
/
guava
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
android/guava-tests/test/com/google/common/math/BigDecimalMathTest.java
380 строк
15 KB
Kurt Alfred Kluever
Add safe bounded `toBigInteger()` and `toBigIntegerExact()` conversion helpers in `BigDecimalMath`.
17 июл 2026, 03:40
17 июл 2026, 03:40
a243588
Код
Авторство
О чём код?
/* * 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.lang.Math.nextDown; import static java.lang.Math.nextUp; 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 org.junit.Assert.assertThrows; import com.google.common.annotations.GwtIncompatible; import com.google.common.annotations.J2ktIncompatible; import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.math.BigDecimal; import java.math.BigInteger; import java.math.MathContext; import java.math.RoundingMode; import java.util.EnumMap; import java.util.EnumSet; import java.util.Map; import junit.framework.TestCase; import org.jspecify.annotations.NullUnmarked; @GwtIncompatible @J2ktIncompatible @NullUnmarked public class BigDecimalMathTest extends TestCase { 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; } @CanIgnoreReturnValue RoundToDoubleTester setExpectation(double expectedValue, RoundingMode... modes) { for (RoundingMode mode : modes) { Double previous = expectedValues.put(mode, expectedValue); if (previous != null) { throw new AssertionError(); } } return this; } @CanIgnoreReturnValue RoundToDoubleTester roundUnnecessaryShouldThrow() { unnecessaryShouldThrow = true; return this; } 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(%s, %s)", input, mode) .that(BigDecimalMath.roundToDouble(input, mode)) .isEqualTo(expectation); } if (!expectedValues.containsKey(UNNECESSARY)) { assertWithMessage("Expected roundUnnecessaryShouldThrow call") .that(unnecessaryShouldThrow) .isTrue(); assertThrows( "Expected ArithmeticException for roundToDouble(" + input + ", UNNECESSARY)", ArithmeticException.class, () -> BigDecimalMath.roundToDouble(input, UNNECESSARY)); } } } public void testRoundToDouble_zero() { new RoundToDoubleTester(BigDecimal.ZERO).setExpectation(0.0, RoundingMode.values()).test(); } 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(); } 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(); } 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(); } public void testRoundToDouble_smallPositive() { new RoundToDoubleTester(BigDecimal.valueOf(16)) .setExpectation(16.0, RoundingMode.values()) .test(); } public void testRoundToDouble_maxPreciselyRepresentable() { new RoundToDoubleTester(BigDecimal.valueOf(1L << 53)) .setExpectation(Math.pow(2, 53), RoundingMode.values()) .test(); } 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(nextUp(twoToThe53), CEILING, UP, HALF_UP) .roundUnnecessaryShouldThrow() .test(); } 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(nextUp(twoToThe54), CEILING, UP) .roundUnnecessaryShouldThrow() .test(); } 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(nextUp(twoToThe54), CEILING, UP) .roundUnnecessaryShouldThrow() .test(); } 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(nextUp(twoToThe54), CEILING, UP, HALF_DOWN, HALF_UP, HALF_EVEN) .roundUnnecessaryShouldThrow() .test(); } public void testRoundToDouble_twoToThe54PlusFour() { new RoundToDoubleTester(BigDecimal.valueOf((1L << 54) + 4)) .setExpectation(Math.pow(2, 54) + 4, RoundingMode.values()) .test(); } public void testRoundToDouble_maxDouble() { BigDecimal maxDoubleAsBigDecimal = new BigDecimal(Double.MAX_VALUE); new RoundToDoubleTester(maxDoubleAsBigDecimal) .setExpectation(Double.MAX_VALUE, RoundingMode.values()) .test(); } public void testRoundToDouble_maxDoublePlusOne() { BigDecimal maxDoubleAsBigDecimal = new BigDecimal(Double.MAX_VALUE).add(BigDecimal.ONE); new RoundToDoubleTester(maxDoubleAsBigDecimal) .setExpectation(Double.MAX_VALUE, DOWN, FLOOR, HALF_EVEN, HALF_UP, HALF_DOWN) .setExpectation(Double.POSITIVE_INFINITY, UP, CEILING) .roundUnnecessaryShouldThrow() .test(); } 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(); } public void testRoundToDouble_smallNegative() { new RoundToDoubleTester(BigDecimal.valueOf(-16)) .setExpectation(-16.0, RoundingMode.values()) .test(); } public void testRoundToDouble_minPreciselyRepresentable() { new RoundToDoubleTester(BigDecimal.valueOf(-1L << 53)) .setExpectation(-Math.pow(2, 53), RoundingMode.values()) .test(); } 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(nextDown(-Math.pow(2, 53)), FLOOR, UP, HALF_UP) .roundUnnecessaryShouldThrow() .test(); } 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(nextDown(-Math.pow(2, 54)), FLOOR, UP) .roundUnnecessaryShouldThrow() .test(); } public void testRoundToDouble_negativeTwoToThe54MinusThree() { new RoundToDoubleTester(BigDecimal.valueOf((-1L << 54) - 3)) .setExpectation(-Math.pow(2, 54), DOWN, CEILING) .setExpectation(nextDown(-Math.pow(2, 54)), FLOOR, UP, HALF_DOWN, HALF_UP, HALF_EVEN) .roundUnnecessaryShouldThrow() .test(); } public void testRoundToDouble_negativeTwoToThe54MinusFour() { new RoundToDoubleTester(BigDecimal.valueOf((-1L << 54) - 4)) .setExpectation(-Math.pow(2, 54) - 4, RoundingMode.values()) .test(); } public void testRoundToDouble_minDouble() { BigDecimal minDoubleAsBigDecimal = new BigDecimal(-Double.MAX_VALUE); new RoundToDoubleTester(minDoubleAsBigDecimal) .setExpectation(-Double.MAX_VALUE, RoundingMode.values()) .test(); } public void testRoundToDouble_minDoubleMinusOne() { BigDecimal minDoubleAsBigDecimal = new BigDecimal(-Double.MAX_VALUE).subtract(BigDecimal.ONE); new RoundToDoubleTester(minDoubleAsBigDecimal) .setExpectation(-Double.MAX_VALUE, DOWN, CEILING, HALF_EVEN, HALF_UP, HALF_DOWN) .setExpectation(Double.NEGATIVE_INFINITY, UP, FLOOR) .roundUnnecessaryShouldThrow() .test(); } 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(); } // TODO(kak): consider using TestParameterInjector public void testToBigInteger_zero() { assertThat(BigDecimalMath.toBigInteger(BigDecimal.ZERO, 0)).isEqualTo(BigInteger.ZERO); assertThat(BigDecimalMath.toBigInteger(new BigDecimal("0.00"), 0)).isEqualTo(BigInteger.ZERO); assertThat(BigDecimalMath.toBigInteger(new BigDecimal("0E+500"), 10)) .isEqualTo(BigInteger.ZERO); assertThat(BigDecimalMath.toBigInteger(new BigDecimal("0E+123456789"), 0)) .isEqualTo(BigInteger.ZERO); } public void testToBigIntegerExact_zero() { assertThat(BigDecimalMath.toBigIntegerExact(BigDecimal.ZERO, 0)).isEqualTo(BigInteger.ZERO); assertThat(BigDecimalMath.toBigIntegerExact(new BigDecimal("0.00"), 0)) .isEqualTo(BigInteger.ZERO); assertThat(BigDecimalMath.toBigIntegerExact(new BigDecimal("0E+123456789"), 0)) .isEqualTo(BigInteger.ZERO); } public void testToBigInteger_valid() { assertThat(BigDecimalMath.toBigInteger(new BigDecimal("12345.67"), 5)) .isEqualTo(BigInteger.valueOf(12345)); assertThat(BigDecimalMath.toBigInteger(new BigDecimal("0.5"), 0)).isEqualTo(BigInteger.ZERO); assertThat(BigDecimalMath.toBigInteger(new BigDecimal("-123.45"), 3)) .isEqualTo(BigInteger.valueOf(-123)); } public void testToBigIntegerExact_valid() { assertThat(BigDecimalMath.toBigIntegerExact(new BigDecimal("12345"), 5)) .isEqualTo(BigInteger.valueOf(12345)); assertThat(BigDecimalMath.toBigIntegerExact(new BigDecimal("123.00"), 3)) .isEqualTo(BigInteger.valueOf(123)); assertThat(BigDecimalMath.toBigIntegerExact(new BigDecimal("1E+4"), 5)) .isEqualTo(BigInteger.valueOf(10000)); assertThat(BigDecimalMath.toBigIntegerExact(new BigDecimal("-123"), 3)) .isEqualTo(BigInteger.valueOf(-123)); } public void testToBigInteger_largePositiveScale_doesNotExhaustMemory() { BigDecimal input = new BigDecimal("1E-123456789"); assertThat(BigDecimalMath.toBigInteger(input, 10)).isEqualTo(BigInteger.ZERO); ArithmeticException expected = assertThrows(ArithmeticException.class, () -> BigDecimalMath.toBigIntegerExact(input, 10)); assertThat(expected) .hasMessageThat() .contains( "has a nonzero fractional part and cannot be represented exactly as a BigInteger"); } public void testToBigInteger_exceedsMaxIntegerDigits_throwsException() { BigDecimal input = new BigDecimal("123456"); assertToBigIntegerThrows(input, 5); } public void testToBigInteger_largeNegativeScale_throwsWithoutExhaustingMemory() { BigDecimal input = new BigDecimal("1E+123456789"); assertToBigIntegerThrows(input, 20); } public void testToBigInteger_precisionMinusScaleOverflowsInt_throwsWithoutExhaustingMemory() { BigDecimal input = BigDecimal.valueOf(1, -Integer.MAX_VALUE); assertToBigIntegerThrows(input, 20); } /** * Helper method to assert that both {@code toBigInteger()} and {@code toBigIntegerExact()} throw * {@code ArithmeticException} with the same message. */ private static void assertToBigIntegerThrows(BigDecimal input, int maxIntegerDigits) { ArithmeticException expectedToBigInteger = assertThrows( ArithmeticException.class, () -> BigDecimalMath.toBigInteger(input, maxIntegerDigits)); assertThat(expectedToBigInteger).hasMessageThat().contains("> maxIntegerDigits"); ArithmeticException expectedToBigIntegerExact = assertThrows( ArithmeticException.class, () -> BigDecimalMath.toBigIntegerExact(input, maxIntegerDigits)); assertThat(expectedToBigIntegerExact).hasMessageThat().contains("> maxIntegerDigits"); } public void testToBigIntegerExact_nonZeroFractionalPart_throwsException() { BigDecimal input = new BigDecimal("123.45"); ArithmeticException expected = assertThrows(ArithmeticException.class, () -> BigDecimalMath.toBigIntegerExact(input, 10)); assertThat(expected).hasMessageThat().isEqualTo("Rounding necessary"); } public void testToBigInteger_invalidMaxIntegerDigits_throwsException() { assertThrows( IllegalArgumentException.class, () -> BigDecimalMath.toBigInteger(BigDecimal.ONE, -1)); assertThrows( IllegalArgumentException.class, () -> BigDecimalMath.toBigIntegerExact(BigDecimal.ONE, -1)); } }