From: Robert Varga Date: Wed, 25 May 2022 08:27:47 +0000 (+0200) Subject: Add Decimal64.scaleTo() X-Git-Tag: v8.0.6~2 X-Git-Url: https://git.opendaylight.org/gerrit/gitweb?a=commitdiff_plain;ds=sidebyside;h=d7a4d9434e5e691f68d975955f78c887e0f1a9c1;p=yangtools.git Add Decimal64.scaleTo() Decimal64.valueOf(String) results in variable-scale results, based on the input string. This is sub-optimal when we want to compare values of particular type -- those should be governed by fraction-digits and hence, for example "2.00" and "2.0" should be normalized to the same scale. Introduce Decimal64.scaleTo(), which can be used to adjust the scale of an existing Decimal64. JIRA: YANGTOOLS-1440 Change-Id: Icbc215dff6d8146996c5be1040751fd1e14b6cfe Signed-off-by: Robert Varga (cherry picked from commit c6624c7716ea59e4df3cb7bf90d172e81a544be5) --- diff --git a/common/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/Decimal64.java b/common/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/Decimal64.java index 5a762a5bcd..a2254371c6 100644 --- a/common/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/Decimal64.java +++ b/common/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/Decimal64.java @@ -9,6 +9,7 @@ package org.opendaylight.yangtools.yang.common; 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; @@ -136,6 +137,37 @@ public class Decimal64 extends Number implements CanonicalValue { } } + /** + * 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 SUPPORT = new Support(); private static final long serialVersionUID = 1L; @@ -336,6 +368,126 @@ public class Decimal64 extends Number implements CanonicalValue { 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()); } diff --git a/common/yang-common/src/test/java/org/opendaylight/yangtools/yang/common/YT1440Test.java b/common/yang-common/src/test/java/org/opendaylight/yangtools/yang/common/YT1440Test.java new file mode 100644 index 0000000000..68faebe5a0 --- /dev/null +++ b/common/yang-common/src/test/java/org/opendaylight/yangtools/yang/common/YT1440Test.java @@ -0,0 +1,207 @@ +/* + * 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()); + } +}