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;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* 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
// Deal with optional sign
final boolean negative;
- int idx;
- switch (str.charAt(0)) {
- case '-':
+ int idx = switch (str.charAt(0)) {
+ case '-' -> {
negative = true;
- idx = 1;
- break;
- case '+':
+ yield 1;
+ }
+ case '+' -> {
negative = false;
- idx = 1;
- break;
- default:
+ yield 1;
+ }
+ default -> {
negative = false;
- idx = 0;
- }
-
+ yield 0;
+ }
+ };
// Sanity check length
if (idx == str.length()) {
return CanonicalValueViolation.variantOf("Missing digits after sign");
// Fractions are next
break;
}
- if (intLen == MAX_FRACTION_DIGITS) {
+ if (intLen == MAX_SCALE) {
return CanonicalValueViolation.variantOf(
- "Integer part is longer than " + MAX_FRACTION_DIGITS + " digits");
+ "Integer part is longer than " + MAX_SCALE + " digits");
}
intPart = 10 * intPart + toInt(ch, idx);
limit--;
}
- final int fracLimit = MAX_FRACTION_DIGITS - intLen;
+ final int fracLimit = MAX_SCALE - intLen + 1;
byte fracLen = 0;
long fracPart = 0;
for (; idx <= limit; idx++, fracLen++) {
}
}
+ /**
+ * 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_FRACTION_DIGITS = 18;
+ private static final int MAX_SCALE = 18;
- private static final long[] SCALE = {
+ private static final long[] FACTOR = {
10,
100,
1000,
1000000000000000000L
};
+ private static final Decimal64Conversion[] CONVERSION = Decimal64Conversion.values();
+ private static final Decimal64[] MIN_VALUE;
+ private static final Decimal64[] MAX_VALUE;
+
static {
- verify(SCALE.length == MAX_FRACTION_DIGITS);
+ verify(CONVERSION.length == MAX_SCALE);
+ verify(FACTOR.length == MAX_SCALE);
+
+ 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, Long.MIN_VALUE);
+ MAX_VALUE[i] = new Decimal64(i, Long.MAX_VALUE);
+ }
}
- private final byte scaleOffset;
+ private final byte offset;
private final long value;
@VisibleForTesting
- Decimal64(final int fractionDigits, final long intPart, final long fracPart, final boolean negative) {
- checkArgument(fractionDigits >= 1 && fractionDigits <= MAX_FRACTION_DIGITS);
- this.scaleOffset = (byte) (fractionDigits - 1);
+ Decimal64(final int scale, final long intPart, final long fracPart, final boolean negative) {
+ offset = offsetOf(scale);
- final long bits = intPart * SCALE[this.scaleOffset] + fracPart;
- this.value = negative ? -bits : bits;
+ final long bits = intPart * FACTOR[offset] + fracPart;
+ value = negative ? -bits : bits;
+ }
+
+ private Decimal64(final byte offset, final long intPart, final boolean negative) {
+ this.offset = offset;
+ final long bits = intPart * FACTOR[offset];
+ value = negative ? -bits : bits;
+ }
+
+ private Decimal64(final byte offset, final long value) {
+ this.offset = offset;
+ this.value = value;
}
protected Decimal64(final Decimal64 other) {
- this.scaleOffset = other.scaleOffset;
- this.value = other.value;
+ this(other.offset, other.value);
+ }
+
+ /**
+ * Return a {@link Decimal64} with specified scale and unscaled value.
+ *
+ * @param scale scale to use
+ * @param unscaledValue unscaled value to use
+ * @return A Decimal64 instance
+ * @throws IllegalArgumentException if {@code scale} is not in range {@code [1..18]}
+ */
+ public static Decimal64 of(final int scale, final long unscaledValue) {
+ return new Decimal64(offsetOf(scale), unscaledValue);
}
- public static Decimal64 valueOf(final byte byteVal) {
- return byteVal < 0 ? new Decimal64(1, -byteVal, 0, true) : new Decimal64(1, byteVal, 0, false);
+ /**
+ * Return the minimum value supported in specified scale.
+ *
+ * @param scale scale to use
+ * @return Minimum value in that scale
+ * @throws IllegalArgumentException if {@code scale} is not in range {@code [1..18]}
+ */
+ public static Decimal64 minValueIn(final int scale) {
+ return MIN_VALUE[offsetOf(scale)];
}
- public static Decimal64 valueOf(final short shortVal) {
- return shortVal < 0 ? new Decimal64(1, -shortVal, 0, true) : new Decimal64(1, shortVal, 0, false);
+ /**
+ * Return the maximum value supported in specified scale.
+ *
+ * @param scale scale to use
+ * @return Maximum value in that scale
+ * @throws IllegalArgumentException if {@code scale} is not in range {@code [1..18]}
+ */
+ public static Decimal64 maxValueIn(final int scale) {
+ return MAX_VALUE[offsetOf(scale)];
}
- public static Decimal64 valueOf(final int intVal) {
- return intVal < 0 ? new Decimal64(1, - (long)intVal, 0, true) : new Decimal64(1, intVal, 0, false);
+ // >>> FIXME: these need truncating counterparts
+ public static Decimal64 valueOf(final int scale, final byte byteVal) {
+ final byte offset = offsetOf(scale);
+ final var conv = CONVERSION[offset];
+ if (byteVal < conv.minByte || byteVal > conv.maxByte) {
+ throw iae(scale, byteVal, conv);
+ }
+ return byteVal < 0 ? new Decimal64(offset, -byteVal, true) : new Decimal64(offset, byteVal, false);
+ }
+
+ public static Decimal64 valueOf(final int scale, final short shortVal) {
+ final byte offset = offsetOf(scale);
+ final var conv = CONVERSION[offset];
+ if (shortVal < conv.minShort || shortVal > conv.maxShort) {
+ throw iae(scale, shortVal, conv);
+ }
+ return shortVal < 0 ? new Decimal64(offset, -shortVal, true) : new Decimal64(offset, shortVal, false);
+ }
+
+ public static Decimal64 valueOf(final int scale, final int intVal) {
+ final byte offset = offsetOf(scale);
+ final var conv = CONVERSION[offset];
+ if (intVal < conv.minInt || intVal > conv.maxInt) {
+ throw iae(scale, intVal, conv);
+ }
+ return intVal < 0 ? new Decimal64(offset, - (long)intVal, true) : new Decimal64(offset, intVal, false);
+ }
+
+ public static Decimal64 valueOf(final int scale, final long longVal) {
+ final byte offset = offsetOf(scale);
+ final var conv = CONVERSION[offset];
+ if (longVal < conv.minLong || longVal > conv.maxLong) {
+ throw iae(scale, longVal, conv);
+ }
+ return longVal < 0 ? new Decimal64(offset, -longVal, true) : new Decimal64(offset, longVal, false);
}
- public static Decimal64 valueOf(final long longVal) {
+ // <<< FIXME
+
+ // FIXME: this should take a RoundingMode and perform rounding
+ // FIXME: this should have a truncating counterpart
+ public static Decimal64 valueOf(final float floatVal, final RoundingMode rounding) {
// XXX: we should be able to do something smarter here
- return valueOf(Long.toString(longVal));
+ return valueOf(Float.toString(floatVal));
}
- public static Decimal64 valueOf(final double doubleVal) {
+ // FIXME: this should take a RoundingMode and perform rounding
+ // FIXME: this should have a truncating counterpart
+ public static Decimal64 valueOf(final double doubleVal, final RoundingMode rounding) {
// XXX: we should be able to do something smarter here
return valueOf(Double.toString(doubleVal));
}
public static Decimal64 valueOf(final BigDecimal decimalVal) {
- // XXX: we should be able to do something smarter here
+ // FIXME: we should be able to do something smarter here using BigDecimal.unscaledValue() and BigDecimal.scale()
return valueOf(decimalVal.toPlainString());
}
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 the scale of this decimal. This is the number of fraction digits, in range {@code [1..18]}.
+ *
+ * @return This decimal's scale
+ */
+ public final int scale() {
+ return offset + 1;
+ }
+
+ /**
+ * Return the unscaled value of this decimal.
+ *
+ * @return This decimal's unscaled value
+ */
+ public final long unscaledValue() {
+ 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, scaleOffset + 1);
+ return BigDecimal.valueOf(value, scale());
}
@Override
@Override
public final double doubleValue() {
- return 1.0 * value / SCALE[scaleOffset];
+ return 1.0 * value / FACTOR[offset];
}
/**
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;
}
if (this == o) {
return 0;
}
- if (scaleOffset == o.scaleOffset) {
+ if (offset == o.offset) {
return Long.compare(value, o.value);
}
@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
// prohibited, subject to the rule that there MUST be at least one digit
// before and after the decimal point. The value zero is represented as
// "0.0".
- final StringBuilder sb = new StringBuilder(21).append(intPart()).append('.');
- final long fracPart = fracPart();
- if (fracPart != 0) {
- // We may need to zero-pad the fraction part
- sb.append(Strings.padStart(Long.toString(fracPart), scaleOffset + 1, '0'));
- } else {
- sb.append('0');
+
+ // 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));
}
- return sb.toString();
+ // 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);
+ }
+
+ // Insert '.' before the first decimal and we're done
+ return builder.insert(firstDecimal, '.').toString();
}
@Override
@Override
public final boolean equals(final @Nullable Object obj) {
- return this == obj || obj instanceof Decimal64 && equalsImpl((Decimal64) obj);
+ return this == obj || obj instanceof Decimal64 other && equalsImpl(other);
}
/**
}
private boolean equalsImpl(final Decimal64 other) {
- return scaleOffset == other.scaleOffset ? value == other.value
+ return offset == other.offset ? value == other.value
// We need to normalize both
: intPart() == other.intPart() && fracPart() == other.fracPart();
}
private long intPart() {
- return value / SCALE[scaleOffset];
+ return value / FACTOR[offset];
}
private long fracPart() {
- return Math.abs(value % SCALE[scaleOffset]);
+ return value % FACTOR[offset];
+ }
+
+ private static byte offsetOf(final int scale) {
+ 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);
}
}