import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Verify.verify;
+import static java.util.Objects.requireNonNull;
import com.google.common.annotations.Beta;
import com.google.common.annotations.VisibleForTesting;
}
}
+ /**
+ * Tri-state indicator of how a non-zero remainder is significant to rounding.
+ */
+ private enum RemainderSignificance {
+ /**
+ * The remainder is less than the half of the interval.
+ */
+ LT_HALF,
+ /**
+ * The remainder is exactly half of the interval.
+ */
+ HALF,
+ /**
+ * The remainder is greater than the half of the interval.
+ */
+ GT_HALF;
+
+ static RemainderSignificance of(final long remainder, final long interval) {
+ final long absRemainder = Math.abs(remainder);
+ final long half = interval / 2;
+
+ if (absRemainder > half) {
+ return GT_HALF;
+ } else if (absRemainder < half) {
+ return LT_HALF;
+ } else {
+ return HALF;
+ }
+ }
+ }
+
private static final CanonicalValueSupport<Decimal64> SUPPORT = new Support();
private static final long serialVersionUID = 1L;
return value;
}
+ /**
+ * Return this decimal in the specified scale.
+ *
+ * @param scale target scale
+ * @return Scaled number
+ * @throws ArithmeticException if the conversion would overflow or require rounding
+ */
+ public Decimal64 scaleTo(final int scale) {
+ return scaleTo(scale, RoundingMode.UNNECESSARY);
+ }
+
+ /**
+ * Return this decimal in the specified scale.
+ *
+ * @param scale scale
+ * @param roundingMode rounding mode
+ * @return Scaled number
+ * @throws ArithmeticException if the conversion would overflow or require rounding and {@code roundingMode} is
+ * {@link RoundingMode#UNNECESSARY}.
+ * @throws IllegalArgumentException if {@code scale} is not valid
+ * @throws NullPointerException if {@code roundingMode} is {@code null}
+ */
+ public Decimal64 scaleTo(final int scale, final RoundingMode roundingMode) {
+ final var mode = requireNonNull(roundingMode);
+ final byte scaleOffset = offsetOf(scale);
+ final int diff = scaleOffset - offset;
+ if (diff == 0) {
+ // Same scale, no-op
+ return this;
+ } else if (value == 0) {
+ // Zero is special, as it has the same unscaled value in all scales
+ return new Decimal64(scaleOffset, 0);
+ }
+
+ if (diff > 0) {
+ // Increasing scale is simple, as we have pre-calculated min/max boundaries and then it's just
+ // factor multiplication
+ final int diffOffset = diff - 1;
+ final var conv = CONVERSION[diffOffset];
+ if (value < conv.minLong || value > conv.maxLong) {
+ throw new ArithmeticException("Increasing scale of " + this + " to " + scale + " would overflow");
+ }
+ return new Decimal64(scaleOffset, value * FACTOR[diffOffset]);
+ }
+
+ // Decreasing scale is hard, as we need to deal with rounding
+ final int diffOffset = -diff - 1;
+ final long factor = FACTOR[diffOffset];
+ final long trunc = value / factor;
+ final long remainder = value - trunc * factor;
+
+ // No remainder, we do not need to involve rounding
+ if (remainder == 0) {
+ return new Decimal64(scaleOffset, trunc);
+ }
+
+ final long increment;
+ switch (mode) {
+ case UP:
+ increment = Long.signum(trunc);
+ break;
+ case DOWN:
+ increment = 0;
+ break;
+ case CEILING:
+ increment = Long.signum(trunc) > 0 ? 1 : 0;
+ break;
+ case FLOOR:
+ increment = Long.signum(trunc) < 0 ? -1 : 0;
+ break;
+ case HALF_UP:
+ switch (RemainderSignificance.of(remainder, factor)) {
+ case LT_HALF:
+ increment = 0;
+ break;
+ case HALF:
+ case GT_HALF:
+ increment = Long.signum(trunc);
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+ break;
+ case HALF_DOWN:
+ switch (RemainderSignificance.of(remainder, factor)) {
+ case LT_HALF:
+ case HALF:
+ increment = 0;
+ break;
+ case GT_HALF:
+ increment = Long.signum(trunc);
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+ break;
+ case HALF_EVEN:
+ switch (RemainderSignificance.of(remainder, factor)) {
+ case LT_HALF:
+ increment = 0;
+ break;
+ case HALF:
+ increment = (trunc & 0x1) != 0 ? Long.signum(trunc) : 0;
+ break;
+ case GT_HALF:
+ increment = Long.signum(trunc);
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+ break;
+ case UNNECESSARY:
+ throw new ArithmeticException("Decreasing scale of " + this + " to " + scale + " requires rounding");
+ default:
+ throw new IllegalStateException();
+ }
+
+ return new Decimal64(scaleOffset, trunc + increment);
+ }
+
public final BigDecimal decimalValue() {
return BigDecimal.valueOf(value, scale());
}
--- /dev/null
+/*
+ * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.common;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.math.RoundingMode;
+import org.junit.jupiter.api.Test;
+
+public class YT1440Test {
+ @Test
+ public void testScaleSame() {
+ final var twenty = Decimal64.valueOf(2, 20);
+ assertSame(twenty, twenty.scaleTo(2));
+
+ // Do not tolerate null rounding even for no-op
+ assertThrows(NullPointerException.class, () -> twenty.scaleTo(2, null));
+ }
+
+ @Test
+ public void testScaleZero() {
+ final var two = Decimal64.valueOf(2, 0);
+ final var one = two.scaleTo(1);
+ assertEquals(1, one.scale());
+ assertEquals(0, one.unscaledValue());
+ final var three = two.scaleTo(3);
+ assertEquals(3, three.scale());
+ assertEquals(0, three.unscaledValue());
+ }
+
+ @Test
+ public void testScaleUpNoRemain() {
+ // Template, scale=5
+ final var two = Decimal64.valueOf(2, 20);
+
+ // scale = 5
+ final var five = two.scaleTo(5);
+ assertEquals(5, five.scale());
+ assertEquals(two, five);
+ assertEquals("20.0", five.toString());
+
+ // scale = 18 fails
+ final var ex = assertThrows(ArithmeticException.class, () -> two.scaleTo(18));
+ assertEquals("Increasing scale of 20.0 to 18 would overflow", ex.getMessage());
+ }
+
+ @Test
+ public void testScaleDownNoRemain() {
+ // Template, scale=5
+ final var five = Decimal64.valueOf(5, 20);
+
+ // scale = 2
+ final var two = five.scaleTo(2);
+ assertEquals(2, two.scale());
+ assertEquals(five, two);
+ assertEquals("20.0", two.toString());
+ }
+
+ @Test
+ public void testScaleDownPositive() {
+ final var two = Decimal64.valueOf("0.63");
+ assertEquals(2, two.scale());
+ assertEquals(63, two.unscaledValue());
+
+ // Trim '3'
+ assertScaleDown(7, two, 1, RoundingMode.UP);
+ assertScaleDown(6, two, 1, RoundingMode.DOWN);
+ assertScaleDown(7, two, 1, RoundingMode.CEILING);
+ assertScaleDown(6, two, 1, RoundingMode.FLOOR);
+ assertScaleDown(6, two, 1, RoundingMode.HALF_UP);
+ assertScaleDown(6, two, 1, RoundingMode.HALF_DOWN);
+ assertScaleDown(6, two, 1, RoundingMode.HALF_EVEN);
+
+ final var three = Decimal64.valueOf("0.635");
+ assertEquals(3, three.scale());
+ assertEquals(635, three.unscaledValue());
+
+ // Trim '5'
+ assertScaleDown(64, three, 2, RoundingMode.UP);
+ assertScaleDown(63, three, 2, RoundingMode.DOWN);
+ assertScaleDown(64, three, 2, RoundingMode.CEILING);
+ assertScaleDown(63, three, 2, RoundingMode.FLOOR);
+ assertScaleDown(64, three, 2, RoundingMode.HALF_UP);
+ assertScaleDown(63, three, 2, RoundingMode.HALF_DOWN);
+ assertScaleDown(64, three, 2, RoundingMode.HALF_EVEN);
+
+ // Trim '35'
+ assertScaleDown(7, three, 1, RoundingMode.UP);
+ assertScaleDown(6, three, 1, RoundingMode.DOWN);
+ assertScaleDown(7, three, 1, RoundingMode.CEILING);
+ assertScaleDown(6, three, 1, RoundingMode.FLOOR);
+ assertScaleDown(6, three, 1, RoundingMode.HALF_UP);
+ assertScaleDown(6, three, 1, RoundingMode.HALF_DOWN);
+ assertScaleDown(6, three, 1, RoundingMode.HALF_EVEN);
+
+ final var four = Decimal64.valueOf("0.6355");
+ assertEquals(4, four.scale());
+ assertEquals(6355, four.unscaledValue());
+
+ // Trim 55
+ assertScaleDown(64, four, 2, RoundingMode.UP);
+ assertScaleDown(63, four, 2, RoundingMode.DOWN);
+ assertScaleDown(64, four, 2, RoundingMode.CEILING);
+ assertScaleDown(63, four, 2, RoundingMode.FLOOR);
+ assertScaleDown(64, four, 2, RoundingMode.HALF_UP);
+ assertScaleDown(64, four, 2, RoundingMode.HALF_DOWN);
+ assertScaleDown(64, four, 2, RoundingMode.HALF_EVEN);
+
+ final var five = Decimal64.valueOf("0.635").scaleTo(5);
+ assertEquals(5, five.scale());
+ assertEquals(63500, five.unscaledValue());
+
+ // Trim 500
+ assertScaleDown(64, five, 2, RoundingMode.UP);
+ assertScaleDown(63, five, 2, RoundingMode.DOWN);
+ assertScaleDown(64, five, 2, RoundingMode.CEILING);
+ assertScaleDown(63, five, 2, RoundingMode.FLOOR);
+ assertScaleDown(64, five, 2, RoundingMode.HALF_UP);
+ assertScaleDown(63, five, 2, RoundingMode.HALF_DOWN);
+ assertScaleDown(64, five, 2, RoundingMode.HALF_EVEN);
+ }
+
+ @Test
+ public void testScaleDownNegative() {
+ final var two = Decimal64.valueOf("-0.63");
+ assertEquals(2, two.scale());
+ assertEquals(-63, two.unscaledValue());
+
+ // Trim '3'
+ assertScaleDown(-7, two, 1, RoundingMode.UP);
+ assertScaleDown(-6, two, 1, RoundingMode.DOWN);
+ assertScaleDown(-6, two, 1, RoundingMode.CEILING);
+ assertScaleDown(-7, two, 1, RoundingMode.FLOOR);
+ assertScaleDown(-6, two, 1, RoundingMode.HALF_UP);
+ assertScaleDown(-6, two, 1, RoundingMode.HALF_DOWN);
+ assertScaleDown(-6, two, 1, RoundingMode.HALF_EVEN);
+
+ final var three = Decimal64.valueOf("-0.635");
+ assertEquals(3, three.scale());
+ assertEquals(-635, three.unscaledValue());
+
+ // Trim '5'
+ assertScaleDown(-64, three, 2, RoundingMode.UP);
+ assertScaleDown(-63, three, 2, RoundingMode.DOWN);
+ assertScaleDown(-63, three, 2, RoundingMode.CEILING);
+ assertScaleDown(-64, three, 2, RoundingMode.FLOOR);
+ assertScaleDown(-64, three, 2, RoundingMode.HALF_UP);
+ assertScaleDown(-63, three, 2, RoundingMode.HALF_DOWN);
+ assertScaleDown(-64, three, 2, RoundingMode.HALF_EVEN);
+
+ // Trim '35'
+ assertScaleDown(-7, three, 1, RoundingMode.UP);
+ assertScaleDown(-6, three, 1, RoundingMode.DOWN);
+ assertScaleDown(-6, three, 1, RoundingMode.CEILING);
+ assertScaleDown(-7, three, 1, RoundingMode.FLOOR);
+ assertScaleDown(-6, three, 1, RoundingMode.HALF_UP);
+ assertScaleDown(-6, three, 1, RoundingMode.HALF_DOWN);
+ assertScaleDown(-6, three, 1, RoundingMode.HALF_EVEN);
+
+ final var four = Decimal64.valueOf("-0.6355");
+ assertEquals(4, four.scale());
+ assertEquals(-6355, four.unscaledValue());
+
+ // Trim 55
+ assertScaleDown(-64, four, 2, RoundingMode.UP);
+ assertScaleDown(-63, four, 2, RoundingMode.DOWN);
+ assertScaleDown(-63, four, 2, RoundingMode.CEILING);
+ assertScaleDown(-64, four, 2, RoundingMode.FLOOR);
+ assertScaleDown(-64, four, 2, RoundingMode.HALF_UP);
+ assertScaleDown(-64, four, 2, RoundingMode.HALF_DOWN);
+ assertScaleDown(-64, four, 2, RoundingMode.HALF_EVEN);
+
+ final var five = Decimal64.valueOf("-0.635").scaleTo(5);
+ assertEquals(5, five.scale());
+ assertEquals(-63500, five.unscaledValue());
+
+ // Trim 500
+ assertScaleDown(-64, five, 2, RoundingMode.UP);
+ assertScaleDown(-63, five, 2, RoundingMode.DOWN);
+ assertScaleDown(-63, five, 2, RoundingMode.CEILING);
+ assertScaleDown(-64, five, 2, RoundingMode.FLOOR);
+ assertScaleDown(-64, five, 2, RoundingMode.HALF_UP);
+ assertScaleDown(-63, five, 2, RoundingMode.HALF_DOWN);
+ assertScaleDown(-64, five, 2, RoundingMode.HALF_EVEN);
+ }
+
+ @Test
+ public void testScaleDownTrim() {
+ final var two = Decimal64.valueOf("0.63");
+ final var ex = assertThrows(ArithmeticException.class, () -> two.scaleTo(1));
+ assertEquals("Decreasing scale of 0.63 to 1 requires rounding", ex.getMessage());
+ }
+
+ private static void assertScaleDown(final long expectedUnscaled, final Decimal64 value, final int scale,
+ final RoundingMode mode) {
+ final var scaled = value.scaleTo(scale, mode);
+ assertEquals(scale, scaled.scale());
+ assertEquals(expectedUnscaled, scaled.unscaledValue());
+ }
+}