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;
/**
* 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> {
@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
}
}
+ /**
+ * 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;
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);
}
}
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);
}
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);
}
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);
}
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
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();
}
/**
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());
}
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;
}
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;
}
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;
}
@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
// 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
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);
+ }
}