Do not use InstanceIdentifier.getPathArguments()
[yangtools.git] / common / yang-common / src / main / java / org / opendaylight / yangtools / yang / common / Decimal64.java
index c50682c36026987db3931a67c5433e9e994d00a9..a8eba1b9c58427cacbbc5545a67ad8844117a0bf 100644 (file)
@@ -9,10 +9,10 @@ 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;
-import com.google.common.base.Strings;
+import java.io.Serial;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.util.Optional;
@@ -23,10 +23,7 @@ import org.opendaylight.yangtools.concepts.Either;
 /**
  * Dedicated type for YANG's 'type decimal64' type. This class is similar to {@link BigDecimal}, but provides more
  * efficient storage, as it has fixed precision.
- *
- * @author Robert Varga
  */
-@Beta
 @NonNullByDefault
 public class Decimal64 extends Number implements CanonicalValue<Decimal64> {
     public static final class Support extends AbstractCanonicalValueSupport<Decimal64> {
@@ -36,7 +33,7 @@ public class Decimal64 extends Number implements CanonicalValue<Decimal64> {
 
         @Override
         public Either<Decimal64, CanonicalValueViolation> fromString(final String str) {
-            // https://tools.ietf.org/html/rfc6020#section-9.3.1
+            // https://www.rfc-editor.org/rfc/rfc6020#section-9.3.1
             //
             // A decimal64 value is lexically represented as an optional sign ("+"
             // or "-"), followed by a sequence of decimal digits, optionally
@@ -135,7 +132,39 @@ public class Decimal64 extends Number implements CanonicalValue<Decimal64> {
         }
     }
 
+    /**
+     * 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();
+    @Serial
     private static final long serialVersionUID = 1L;
 
     private static final int MAX_SCALE = 18;
@@ -172,8 +201,8 @@ public class Decimal64 extends Number implements CanonicalValue<Decimal64> {
         MIN_VALUE = new Decimal64[MAX_SCALE];
         MAX_VALUE = new Decimal64[MAX_SCALE];
         for (byte i = 0; i < MAX_SCALE; ++i) {
-            MIN_VALUE[i] = new Decimal64(i, -9223372036854775808L);
-            MAX_VALUE[i] = new Decimal64(i, 9223372036854775807L);
+            MIN_VALUE[i] = new Decimal64(i, Long.MIN_VALUE);
+            MAX_VALUE[i] = new Decimal64(i, Long.MAX_VALUE);
         }
     }
 
@@ -242,8 +271,7 @@ public class Decimal64 extends Number implements CanonicalValue<Decimal64> {
         final byte offset = offsetOf(scale);
         final var conv = CONVERSION[offset];
         if (byteVal < conv.minByte || byteVal > conv.maxByte) {
-            throw new IllegalArgumentException("Value " + byteVal + " is not in range ["
-                + conv.minByte + ".." + conv.maxByte + "] to fit scale " + scale);
+            throw iae(scale, byteVal, conv);
         }
         return byteVal < 0 ? new Decimal64(offset, -byteVal, true) : new Decimal64(offset, byteVal, false);
     }
@@ -252,8 +280,7 @@ public class Decimal64 extends Number implements CanonicalValue<Decimal64> {
         final byte offset = offsetOf(scale);
         final var conv = CONVERSION[offset];
         if (shortVal < conv.minShort || shortVal > conv.maxShort) {
-            throw new IllegalArgumentException("Value " + shortVal + " is not in range ["
-                + conv.minShort + ".." + conv.maxShort + "] to fit scale " + scale);
+            throw iae(scale, shortVal, conv);
         }
         return shortVal < 0 ? new Decimal64(offset, -shortVal, true) : new Decimal64(offset, shortVal, false);
     }
@@ -262,8 +289,7 @@ public class Decimal64 extends Number implements CanonicalValue<Decimal64> {
         final byte offset = offsetOf(scale);
         final var conv = CONVERSION[offset];
         if (intVal < conv.minInt || intVal > conv.maxInt) {
-            throw new IllegalArgumentException("Value " + intVal + " is not in range ["
-                + conv.minInt + ".." + conv.maxInt + "] to fit scale " + scale);
+            throw iae(scale, intVal, conv);
         }
         return intVal < 0 ? new Decimal64(offset, - (long)intVal, true) : new Decimal64(offset, intVal, false);
     }
@@ -272,11 +298,11 @@ public class Decimal64 extends Number implements CanonicalValue<Decimal64> {
         final byte offset = offsetOf(scale);
         final var conv = CONVERSION[offset];
         if (longVal < conv.minLong || longVal > conv.maxLong) {
-            throw new IllegalArgumentException("Value " + longVal + " is not in range ["
-                + conv.minLong + ".." + conv.maxLong + "] to fit scale " + scale);
+            throw iae(scale, longVal, conv);
         }
         return longVal < 0 ? new Decimal64(offset, -longVal, true) : new Decimal64(offset, longVal, false);
     }
+
     // <<< FIXME
 
     // FIXME: this should take a RoundingMode and perform rounding
@@ -311,10 +337,10 @@ public class Decimal64 extends Number implements CanonicalValue<Decimal64> {
         final Either<Decimal64, CanonicalValueViolation> variant = SUPPORT.fromString(str);
         final Optional<Decimal64> value = variant.tryFirst();
         if (value.isPresent()) {
-            return value.get();
+            return value.orElseThrow();
         }
         final Optional<String> message = variant.getSecond().getMessage();
-        throw message.isPresent() ? new NumberFormatException(message.get()) : new NumberFormatException();
+        throw message.isPresent() ? new NumberFormatException(message.orElseThrow()) : new NumberFormatException();
     }
 
     /**
@@ -335,6 +361,87 @@ public class Decimal64 extends Number implements CanonicalValue<Decimal64> {
         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 -> Long.signum(trunc);
+            case DOWN -> 0;
+            case CEILING -> Long.signum(trunc) > 0 ? 1 : 0;
+            case FLOOR -> Long.signum(trunc) < 0 ? -1 : 0;
+            case HALF_UP -> switch (RemainderSignificance.of(remainder, factor)) {
+                case LT_HALF -> 0;
+                case HALF, GT_HALF -> Long.signum(trunc);
+            };
+            case HALF_DOWN -> switch (RemainderSignificance.of(remainder, factor)) {
+                case LT_HALF, HALF -> 0;
+                case GT_HALF -> Long.signum(trunc);
+            };
+            case HALF_EVEN -> switch (RemainderSignificance.of(remainder, factor)) {
+                case LT_HALF -> 0;
+                case HALF -> (trunc & 0x1) != 0 ? Long.signum(trunc) : 0;
+                case GT_HALF -> Long.signum(trunc);
+            };
+            case UNNECESSARY ->
+                throw new ArithmeticException("Decreasing scale of " + this + " to " + scale + " requires rounding");
+        };
+
+        return new Decimal64(scaleOffset, trunc + increment);
+    }
+
     public final BigDecimal decimalValue() {
         return BigDecimal.valueOf(value, scale());
     }
@@ -371,7 +478,7 @@ public class Decimal64 extends Number implements CanonicalValue<Decimal64> {
         final long val = longValueExact();
         final byte ret = (byte) val;
         if (val != ret) {
-            throw new ArithmeticException("Value " + val + " is outside of byte range");
+            throw ae("byte", val);
         }
         return ret;
     }
@@ -388,7 +495,7 @@ public class Decimal64 extends Number implements CanonicalValue<Decimal64> {
         final long val = longValueExact();
         final short ret = (short) val;
         if (val != ret) {
-            throw new ArithmeticException("Value " + val + " is outside of short range");
+            throw ae("short", val);
         }
         return ret;
     }
@@ -405,7 +512,7 @@ public class Decimal64 extends Number implements CanonicalValue<Decimal64> {
         final long val = longValueExact();
         final int ret = (int) val;
         if (val != ret) {
-            throw new ArithmeticException("Value " + val + " is outside of integer range");
+            throw ae("integer", val);
         }
         return ret;
     }
@@ -440,7 +547,7 @@ public class Decimal64 extends Number implements CanonicalValue<Decimal64> {
 
     @Override
     public final String toCanonicalString() {
-        // https://tools.ietf.org/html/rfc6020#section-9.3.2
+        // https://www.rfc-editor.org/rfc/rfc6020#section-9.3.2
         //
         // The canonical form of a positive decimal64 does not include the sign
         // "+".  The decimal point is required.  Leading and trailing zeros are
@@ -448,22 +555,31 @@ public class Decimal64 extends Number implements CanonicalValue<Decimal64> {
         // before and after the decimal point.  The value zero is represented as
         // "0.0".
 
-        final long intPart = intPart();
-        final long fracPart = fracPart();
-        final StringBuilder sb = new StringBuilder(21);
-        if (intPart == 0 && fracPart < 0) {
-            sb.append('-');
+        // Pad unscaled value to scale + 1 size string starting after optional '-' sign
+        final var builder = new StringBuilder(21).append(value);
+        final int start = value < 0 ? 1 : 0;
+        final int scale = scale();
+        final int padding = scale + 1 + start - builder.length();
+        if (padding > 0) {
+            builder.insert(start, "0".repeat(padding));
         }
-        sb.append(intPart).append('.');
 
-        if (fracPart != 0) {
-            // We may need to zero-pad the fraction part
-            sb.append(Strings.padStart(Long.toString(Math.abs(fracPart)), scale(), '0'));
-        } else {
-            sb.append('0');
+        // The first digit of the fraction part is now 'scale' from the end. We will insert the decimal point there,
+        // but also we it is the digit we never trim.
+        final int length = builder.length();
+        final int firstDecimal = length - scale;
+
+        // Remove trailing '0's from decimal part. We walk backwards from the last character stop at firstDecimal
+        int significantLength = length;
+        for (int i = length - 1; i > firstDecimal && builder.charAt(i) == '0'; --i) {
+            significantLength = i;
+        }
+        if (significantLength != length) {
+            builder.setLength(significantLength);
         }
 
-        return sb.toString();
+        // Insert '.' before the first decimal and we're done
+        return builder.insert(firstDecimal, '.').toString();
     }
 
     @Override
@@ -515,4 +631,13 @@ public class Decimal64 extends Number implements CanonicalValue<Decimal64> {
         checkArgument(scale >= 1 && scale <= MAX_SCALE, "Scale %s is not in range [1..%s]", scale, MAX_SCALE);
         return (byte) (scale - 1);
     }
+
+    private static ArithmeticException ae(final String type, final long val) {
+        return new ArithmeticException("Value " + val + " is outside of " + type + " range");
+    }
+
+    private static IllegalArgumentException iae(final int scale, final long longVal, final Decimal64Conversion conv) {
+        return new IllegalArgumentException("Value " + longVal + " is not in range ["
+            + conv.minLong + ".." + conv.maxLong + "] to fit scale " + scale);
+    }
 }