2 * Copyright (c) 2015 Pantheon Technologies s.r.o. and others. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 * and is available at http://www.eclipse.org/legal/epl-v10.html
8 package org.opendaylight.yangtools.yang.common;
10 import static com.google.common.base.Preconditions.checkArgument;
11 import static com.google.common.base.Verify.verify;
12 import static java.util.Objects.requireNonNull;
14 import com.google.common.annotations.VisibleForTesting;
15 import java.io.Serial;
16 import java.math.BigDecimal;
17 import java.math.RoundingMode;
18 import java.util.Optional;
19 import org.eclipse.jdt.annotation.NonNullByDefault;
20 import org.eclipse.jdt.annotation.Nullable;
21 import org.opendaylight.yangtools.concepts.Either;
24 * Dedicated type for YANG's 'type decimal64' type. This class is similar to {@link BigDecimal}, but provides more
25 * efficient storage, as it has fixed precision.
28 public class Decimal64 extends Number implements CanonicalValue<Decimal64> {
29 public static final class Support extends AbstractCanonicalValueSupport<Decimal64> {
31 super(Decimal64.class);
35 public Either<Decimal64, CanonicalValueViolation> fromString(final String str) {
36 // https://www.rfc-editor.org/rfc/rfc6020#section-9.3.1
38 // A decimal64 value is lexically represented as an optional sign ("+"
39 // or "-"), followed by a sequence of decimal digits, optionally
40 // followed by a period ('.') as a decimal indicator and a sequence of
41 // decimal digits. If no sign is specified, "+" is assumed.
43 return CanonicalValueViolation.variantOf("Empty string is not a valid decimal64 representation");
46 // Deal with optional sign
47 final boolean negative;
48 int idx = switch (str.charAt(0)) {
62 // Sanity check length
63 if (idx == str.length()) {
64 return CanonicalValueViolation.variantOf("Missing digits after sign");
67 // Character limit, used for caching and cutting trailing zeroes
68 int limit = str.length() - 1;
70 // Skip any leading zeroes, but leave at least one
71 for (; idx < limit && str.charAt(idx) == '0'; idx++) {
72 final char ch = str.charAt(idx + 1);
73 if (ch < '0' || ch > '9') {
78 // Integer part and its length
82 for (; idx <= limit; idx++, intLen++) {
83 final char ch = str.charAt(idx);
88 if (intLen == MAX_SCALE) {
89 return CanonicalValueViolation.variantOf(
90 "Integer part is longer than " + MAX_SCALE + " digits");
93 intPart = 10 * intPart + toInt(ch, idx);
97 // No fraction digits, we are done
98 return Either.ofFirst(new Decimal64((byte)1, intPart, 0, negative));
101 // Bump index to skip over period and check the remainder
104 return CanonicalValueViolation.variantOf("Value '" + str + "' is missing fraction digits");
107 // Trim trailing zeroes, if any
108 while (idx < limit && str.charAt(limit) == '0') {
112 final int fracLimit = MAX_SCALE - intLen + 1;
115 for (; idx <= limit; idx++, fracLen++) {
116 final char ch = str.charAt(idx);
117 if (fracLen == fracLimit) {
118 return CanonicalValueViolation.variantOf("Fraction part longer than " + fracLimit + " digits");
121 fracPart = 10 * fracPart + toInt(ch, idx);
124 return Either.ofFirst(new Decimal64(fracLen, intPart, fracPart, negative));
127 private static int toInt(final char ch, final int index) {
128 if (ch < '0' || ch > '9') {
129 throw new NumberFormatException("Illegal character at offset " + index);
136 * Tri-state indicator of how a non-zero remainder is significant to rounding.
138 private enum RemainderSignificance {
140 * The remainder is less than the half of the interval.
144 * The remainder is exactly half of the interval.
148 * The remainder is greater than the half of the interval.
152 static RemainderSignificance of(final long remainder, final long interval) {
153 final long absRemainder = Math.abs(remainder);
154 final long half = interval / 2;
156 if (absRemainder > half) {
158 } else if (absRemainder < half) {
166 private static final CanonicalValueSupport<Decimal64> SUPPORT = new Support();
168 private static final long serialVersionUID = 1L;
170 private static final int MAX_SCALE = 18;
172 private static final long[] FACTOR = {
193 private static final Decimal64Conversion[] CONVERSION = Decimal64Conversion.values();
194 private static final Decimal64[] MIN_VALUE;
195 private static final Decimal64[] MAX_VALUE;
198 verify(CONVERSION.length == MAX_SCALE);
199 verify(FACTOR.length == MAX_SCALE);
201 MIN_VALUE = new Decimal64[MAX_SCALE];
202 MAX_VALUE = new Decimal64[MAX_SCALE];
203 for (byte i = 0; i < MAX_SCALE; ++i) {
204 MIN_VALUE[i] = new Decimal64(i, Long.MIN_VALUE);
205 MAX_VALUE[i] = new Decimal64(i, Long.MAX_VALUE);
209 private final byte offset;
210 private final long value;
213 Decimal64(final int scale, final long intPart, final long fracPart, final boolean negative) {
214 offset = offsetOf(scale);
216 final long bits = intPart * FACTOR[offset] + fracPart;
217 value = negative ? -bits : bits;
220 private Decimal64(final byte offset, final long intPart, final boolean negative) {
221 this.offset = offset;
222 final long bits = intPart * FACTOR[offset];
223 value = negative ? -bits : bits;
226 private Decimal64(final byte offset, final long value) {
227 this.offset = offset;
231 protected Decimal64(final Decimal64 other) {
232 this(other.offset, other.value);
236 * Return a {@link Decimal64} with specified scale and unscaled value.
238 * @param scale scale to use
239 * @param unscaledValue unscaled value to use
240 * @return A Decimal64 instance
241 * @throws IllegalArgumentException if {@code scale} is not in range {@code [1..18]}
243 public static Decimal64 of(final int scale, final long unscaledValue) {
244 return new Decimal64(offsetOf(scale), unscaledValue);
248 * Return the minimum value supported in specified scale.
250 * @param scale scale to use
251 * @return Minimum value in that scale
252 * @throws IllegalArgumentException if {@code scale} is not in range {@code [1..18]}
254 public static Decimal64 minValueIn(final int scale) {
255 return MIN_VALUE[offsetOf(scale)];
259 * Return the maximum value supported in specified scale.
261 * @param scale scale to use
262 * @return Maximum value in that scale
263 * @throws IllegalArgumentException if {@code scale} is not in range {@code [1..18]}
265 public static Decimal64 maxValueIn(final int scale) {
266 return MAX_VALUE[offsetOf(scale)];
269 // >>> FIXME: these need truncating counterparts
270 public static Decimal64 valueOf(final int scale, final byte byteVal) {
271 final byte offset = offsetOf(scale);
272 final var conv = CONVERSION[offset];
273 if (byteVal < conv.minByte || byteVal > conv.maxByte) {
274 throw iae(scale, byteVal, conv);
276 return byteVal < 0 ? new Decimal64(offset, -byteVal, true) : new Decimal64(offset, byteVal, false);
279 public static Decimal64 valueOf(final int scale, final short shortVal) {
280 final byte offset = offsetOf(scale);
281 final var conv = CONVERSION[offset];
282 if (shortVal < conv.minShort || shortVal > conv.maxShort) {
283 throw iae(scale, shortVal, conv);
285 return shortVal < 0 ? new Decimal64(offset, -shortVal, true) : new Decimal64(offset, shortVal, false);
288 public static Decimal64 valueOf(final int scale, final int intVal) {
289 final byte offset = offsetOf(scale);
290 final var conv = CONVERSION[offset];
291 if (intVal < conv.minInt || intVal > conv.maxInt) {
292 throw iae(scale, intVal, conv);
294 return intVal < 0 ? new Decimal64(offset, - (long)intVal, true) : new Decimal64(offset, intVal, false);
297 public static Decimal64 valueOf(final int scale, final long longVal) {
298 final byte offset = offsetOf(scale);
299 final var conv = CONVERSION[offset];
300 if (longVal < conv.minLong || longVal > conv.maxLong) {
301 throw iae(scale, longVal, conv);
303 return longVal < 0 ? new Decimal64(offset, -longVal, true) : new Decimal64(offset, longVal, false);
308 // FIXME: this should take a RoundingMode and perform rounding
309 // FIXME: this should have a truncating counterpart
310 public static Decimal64 valueOf(final float floatVal, final RoundingMode rounding) {
311 // XXX: we should be able to do something smarter here
312 return valueOf(Float.toString(floatVal));
315 // FIXME: this should take a RoundingMode and perform rounding
316 // FIXME: this should have a truncating counterpart
317 public static Decimal64 valueOf(final double doubleVal, final RoundingMode rounding) {
318 // XXX: we should be able to do something smarter here
319 return valueOf(Double.toString(doubleVal));
322 public static Decimal64 valueOf(final BigDecimal decimalVal) {
323 // FIXME: we should be able to do something smarter here using BigDecimal.unscaledValue() and BigDecimal.scale()
324 return valueOf(decimalVal.toPlainString());
328 * Attempt to parse a String into a Decimal64. This method uses minimum fraction digits required to hold
331 * @param str String to parser
332 * @return A Decimal64 instance
333 * @throws NullPointerException if value is null.
334 * @throws NumberFormatException if the string does not contain a parsable decimal64.
336 public static Decimal64 valueOf(final String str) {
337 final Either<Decimal64, CanonicalValueViolation> variant = SUPPORT.fromString(str);
338 final Optional<Decimal64> value = variant.tryFirst();
339 if (value.isPresent()) {
340 return value.orElseThrow();
342 final Optional<String> message = variant.getSecond().getMessage();
343 throw message.isPresent() ? new NumberFormatException(message.orElseThrow()) : new NumberFormatException();
347 * Return the scale of this decimal. This is the number of fraction digits, in range {@code [1..18]}.
349 * @return This decimal's scale
351 public final int scale() {
356 * Return the unscaled value of this decimal.
358 * @return This decimal's unscaled value
360 public final long unscaledValue() {
365 * Return this decimal in the specified scale.
367 * @param scale target scale
368 * @return Scaled number
369 * @throws ArithmeticException if the conversion would overflow or require rounding
371 public Decimal64 scaleTo(final int scale) {
372 return scaleTo(scale, RoundingMode.UNNECESSARY);
376 * Return this decimal in the specified scale.
379 * @param roundingMode rounding mode
380 * @return Scaled number
381 * @throws ArithmeticException if the conversion would overflow or require rounding and {@code roundingMode} is
382 * {@link RoundingMode#UNNECESSARY}.
383 * @throws IllegalArgumentException if {@code scale} is not valid
384 * @throws NullPointerException if {@code roundingMode} is {@code null}
386 public Decimal64 scaleTo(final int scale, final RoundingMode roundingMode) {
387 final var mode = requireNonNull(roundingMode);
388 final byte scaleOffset = offsetOf(scale);
389 final int diff = scaleOffset - offset;
393 } else if (value == 0) {
394 // Zero is special, as it has the same unscaled value in all scales
395 return new Decimal64(scaleOffset, 0);
399 // Increasing scale is simple, as we have pre-calculated min/max boundaries and then it's just
400 // factor multiplication
401 final int diffOffset = diff - 1;
402 final var conv = CONVERSION[diffOffset];
403 if (value < conv.minLong || value > conv.maxLong) {
404 throw new ArithmeticException("Increasing scale of " + this + " to " + scale + " would overflow");
406 return new Decimal64(scaleOffset, value * FACTOR[diffOffset]);
409 // Decreasing scale is hard, as we need to deal with rounding
410 final int diffOffset = -diff - 1;
411 final long factor = FACTOR[diffOffset];
412 final long trunc = value / factor;
413 final long remainder = value - trunc * factor;
415 // No remainder, we do not need to involve rounding
416 if (remainder == 0) {
417 return new Decimal64(scaleOffset, trunc);
420 final long increment = switch (mode) {
421 case UP -> Long.signum(trunc);
423 case CEILING -> Long.signum(trunc) > 0 ? 1 : 0;
424 case FLOOR -> Long.signum(trunc) < 0 ? -1 : 0;
425 case HALF_UP -> switch (RemainderSignificance.of(remainder, factor)) {
427 case HALF, GT_HALF -> Long.signum(trunc);
429 case HALF_DOWN -> switch (RemainderSignificance.of(remainder, factor)) {
430 case LT_HALF, HALF -> 0;
431 case GT_HALF -> Long.signum(trunc);
433 case HALF_EVEN -> switch (RemainderSignificance.of(remainder, factor)) {
435 case HALF -> (trunc & 0x1) != 0 ? Long.signum(trunc) : 0;
436 case GT_HALF -> Long.signum(trunc);
439 throw new ArithmeticException("Decreasing scale of " + this + " to " + scale + " requires rounding");
442 return new Decimal64(scaleOffset, trunc + increment);
445 public final BigDecimal decimalValue() {
446 return BigDecimal.valueOf(value, scale());
450 public final int intValue() {
451 return (int) intPart();
455 public final long longValue() {
460 public final float floatValue() {
461 return (float) doubleValue();
465 public final double doubleValue() {
466 return 1.0 * value / FACTOR[offset];
470 * Converts this {@code BigDecimal} to a {@code byte}, checking for lost information. If this {@code Decimal64} has
471 * a nonzero fractional part or is out of the possible range for a {@code byte} result then
472 * an {@code ArithmeticException} is thrown.
474 * @return this {@code Decimal64} converted to a {@code byte}.
475 * @throws ArithmeticException if {@code this} has a nonzero fractional part, or will not fit in a {@code byte}.
477 public final byte byteValueExact() {
478 final long val = longValueExact();
479 final byte ret = (byte) val;
481 throw ae("byte", val);
487 * Converts this {@code BigDecimal} to a {@code short}, checking for lost information. If this {@code Decimal64} has
488 * a nonzero fractional part or is out of the possible range for a {@code short} result then
489 * an {@code ArithmeticException} is thrown.
491 * @return this {@code Decimal64} converted to a {@code short}.
492 * @throws ArithmeticException if {@code this} has a nonzero fractional part, or will not fit in a {@code short}.
494 public final short shortValueExact() {
495 final long val = longValueExact();
496 final short ret = (short) val;
498 throw ae("short", val);
504 * Converts this {@code BigDecimal} to an {@code int}, checking for lost information. If this {@code Decimal64} has
505 * a nonzero fractional part or is out of the possible range for an {@code int} result then
506 * an {@code ArithmeticException} is thrown.
508 * @return this {@code Decimal64} converted to an {@code int}.
509 * @throws ArithmeticException if {@code this} has a nonzero fractional part, or will not fit in an {@code int}.
511 public final int intValueExact() {
512 final long val = longValueExact();
513 final int ret = (int) val;
515 throw ae("integer", val);
521 * Converts this {@code BigDecimal} to a {@code long}, checking for lost information. If this {@code Decimal64} has
522 * a nonzero fractional part then an {@code ArithmeticException} is thrown.
524 * @return this {@code Decimal64} converted to a {@code long}.
525 * @throws ArithmeticException if {@code this} has a nonzero fractional part.
527 public final long longValueExact() {
528 if (fracPart() != 0) {
529 throw new ArithmeticException("Conversion of " + this + " would lose fraction");
535 @SuppressWarnings("checkstyle:parameterName")
536 public final int compareTo(final Decimal64 o) {
540 if (offset == o.offset) {
541 return Long.compare(value, o.value);
544 // XXX: we could do something smarter here
545 return Double.compare(doubleValue(), o.doubleValue());
549 public final String toCanonicalString() {
550 // https://www.rfc-editor.org/rfc/rfc6020#section-9.3.2
552 // The canonical form of a positive decimal64 does not include the sign
553 // "+". The decimal point is required. Leading and trailing zeros are
554 // prohibited, subject to the rule that there MUST be at least one digit
555 // before and after the decimal point. The value zero is represented as
558 // Pad unscaled value to scale + 1 size string starting after optional '-' sign
559 final var builder = new StringBuilder(21).append(value);
560 final int start = value < 0 ? 1 : 0;
561 final int scale = scale();
562 final int padding = scale + 1 + start - builder.length();
564 builder.insert(start, "0".repeat(padding));
567 // The first digit of the fraction part is now 'scale' from the end. We will insert the decimal point there,
568 // but also we it is the digit we never trim.
569 final int length = builder.length();
570 final int firstDecimal = length - scale;
572 // Remove trailing '0's from decimal part. We walk backwards from the last character stop at firstDecimal
573 int significantLength = length;
574 for (int i = length - 1; i > firstDecimal && builder.charAt(i) == '0'; --i) {
575 significantLength = i;
577 if (significantLength != length) {
578 builder.setLength(significantLength);
581 // Insert '.' before the first decimal and we're done
582 return builder.insert(firstDecimal, '.').toString();
586 public final CanonicalValueSupport<Decimal64> support() {
591 public final int hashCode() {
592 // We need to normalize the results in order to be consistent with equals()
593 return Long.hashCode(intPart()) * 31 + Long.hashCode(fracPart());
597 public final boolean equals(final @Nullable Object obj) {
598 return this == obj || obj instanceof Decimal64 other && equalsImpl(other);
602 * A slightly faster version of {@link #equals(Object)}.
604 * @param obj Decimal64 object
605 * @return {@code true} if this object is the same as the obj argument; {@code false} otherwise.
607 public final boolean equals(final @Nullable Decimal64 obj) {
608 return this == obj || obj != null && equalsImpl(obj);
612 public final String toString() {
613 return toCanonicalString();
616 private boolean equalsImpl(final Decimal64 other) {
617 return offset == other.offset ? value == other.value
618 // We need to normalize both
619 : intPart() == other.intPart() && fracPart() == other.fracPart();
622 private long intPart() {
623 return value / FACTOR[offset];
626 private long fracPart() {
627 return value % FACTOR[offset];
630 private static byte offsetOf(final int scale) {
631 checkArgument(scale >= 1 && scale <= MAX_SCALE, "Scale %s is not in range [1..%s]", scale, MAX_SCALE);
632 return (byte) (scale - 1);
635 private static ArithmeticException ae(final String type, final long val) {
636 return new ArithmeticException("Value " + val + " is outside of " + type + " range");
639 private static IllegalArgumentException iae(final int scale, final long longVal, final Decimal64Conversion conv) {
640 return new IllegalArgumentException("Value " + longVal + " is not in range ["
641 + conv.minLong + ".." + conv.maxLong + "] to fit scale " + scale);