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.Beta;
15 import com.google.common.annotations.VisibleForTesting;
16 import com.google.common.base.Strings;
17 import java.math.BigDecimal;
18 import java.math.RoundingMode;
19 import java.util.Optional;
20 import org.eclipse.jdt.annotation.NonNullByDefault;
21 import org.eclipse.jdt.annotation.Nullable;
22 import org.opendaylight.yangtools.concepts.Either;
25 * Dedicated type for YANG's 'type decimal64' type. This class is similar to {@link BigDecimal}, but provides more
26 * efficient storage, as it has fixed precision.
28 * @author Robert Varga
32 public class Decimal64 extends Number implements CanonicalValue<Decimal64> {
33 public static final class Support extends AbstractCanonicalValueSupport<Decimal64> {
35 super(Decimal64.class);
39 public Either<Decimal64, CanonicalValueViolation> fromString(final String str) {
40 // https://tools.ietf.org/html/rfc6020#section-9.3.1
42 // A decimal64 value is lexically represented as an optional sign ("+"
43 // or "-"), followed by a sequence of decimal digits, optionally
44 // followed by a period ('.') as a decimal indicator and a sequence of
45 // decimal digits. If no sign is specified, "+" is assumed.
47 return CanonicalValueViolation.variantOf("Empty string is not a valid decimal64 representation");
50 // Deal with optional sign
51 final boolean negative;
53 switch (str.charAt(0)) {
67 // Sanity check length
68 if (idx == str.length()) {
69 return CanonicalValueViolation.variantOf("Missing digits after sign");
72 // Character limit, used for caching and cutting trailing zeroes
73 int limit = str.length() - 1;
75 // Skip any leading zeroes, but leave at least one
76 for (; idx < limit && str.charAt(idx) == '0'; idx++) {
77 final char ch = str.charAt(idx + 1);
78 if (ch < '0' || ch > '9') {
83 // Integer part and its length
87 for (; idx <= limit; idx++, intLen++) {
88 final char ch = str.charAt(idx);
93 if (intLen == MAX_SCALE) {
94 return CanonicalValueViolation.variantOf(
95 "Integer part is longer than " + MAX_SCALE + " digits");
98 intPart = 10 * intPart + toInt(ch, idx);
102 // No fraction digits, we are done
103 return Either.ofFirst(new Decimal64((byte)1, intPart, 0, negative));
106 // Bump index to skip over period and check the remainder
109 return CanonicalValueViolation.variantOf("Value '" + str + "' is missing fraction digits");
112 // Trim trailing zeroes, if any
113 while (idx < limit && str.charAt(limit) == '0') {
117 final int fracLimit = MAX_SCALE - intLen + 1;
120 for (; idx <= limit; idx++, fracLen++) {
121 final char ch = str.charAt(idx);
122 if (fracLen == fracLimit) {
123 return CanonicalValueViolation.variantOf("Fraction part longer than " + fracLimit + " digits");
126 fracPart = 10 * fracPart + toInt(ch, idx);
129 return Either.ofFirst(new Decimal64(fracLen, intPart, fracPart, negative));
132 private static int toInt(final char ch, final int index) {
133 if (ch < '0' || ch > '9') {
134 throw new NumberFormatException("Illegal character at offset " + index);
141 * Tri-state indicator of how a non-zero remainder is significant to rounding.
143 private enum RemainderSignificance {
145 * The remainder is less than the half of the interval.
149 * The remainder is exactly half of the interval.
153 * The remainder is greater than the half of the interval.
157 static RemainderSignificance of(final long remainder, final long interval) {
158 final long absRemainder = Math.abs(remainder);
159 final long half = interval / 2;
161 if (absRemainder > half) {
163 } else if (absRemainder < half) {
171 private static final CanonicalValueSupport<Decimal64> SUPPORT = new Support();
172 private static final long serialVersionUID = 1L;
174 private static final int MAX_SCALE = 18;
176 private static final long[] FACTOR = {
197 private static final Decimal64Conversion[] CONVERSION = Decimal64Conversion.values();
198 private static final Decimal64[] MIN_VALUE;
199 private static final Decimal64[] MAX_VALUE;
202 verify(CONVERSION.length == MAX_SCALE);
203 verify(FACTOR.length == MAX_SCALE);
205 MIN_VALUE = new Decimal64[MAX_SCALE];
206 MAX_VALUE = new Decimal64[MAX_SCALE];
207 for (byte i = 0; i < MAX_SCALE; ++i) {
208 MIN_VALUE[i] = new Decimal64(i, Long.MIN_VALUE);
209 MAX_VALUE[i] = new Decimal64(i, Long.MAX_VALUE);
213 private final byte offset;
214 private final long value;
217 Decimal64(final int scale, final long intPart, final long fracPart, final boolean negative) {
218 offset = offsetOf(scale);
220 final long bits = intPart * FACTOR[offset] + fracPart;
221 value = negative ? -bits : bits;
224 private Decimal64(final byte offset, final long intPart, final boolean negative) {
225 this.offset = offset;
226 final long bits = intPart * FACTOR[offset];
227 value = negative ? -bits : bits;
230 private Decimal64(final byte offset, final long value) {
231 this.offset = offset;
235 protected Decimal64(final Decimal64 other) {
236 this(other.offset, other.value);
240 * Return a {@link Decimal64} with specified scale and unscaled value.
242 * @param scale scale to use
243 * @param unscaledValue unscaled value to use
244 * @return A Decimal64 instance
245 * @throws IllegalArgumentException if {@code scale} is not in range {@code [1..18]}
247 public static Decimal64 of(final int scale, final long unscaledValue) {
248 return new Decimal64(offsetOf(scale), unscaledValue);
252 * Return the minimum value supported in specified scale.
254 * @param scale scale to use
255 * @return Minimum value in that scale
256 * @throws IllegalArgumentException if {@code scale} is not in range {@code [1..18]}
258 public static Decimal64 minValueIn(final int scale) {
259 return MIN_VALUE[offsetOf(scale)];
263 * Return the maximum value supported in specified scale.
265 * @param scale scale to use
266 * @return Maximum value in that scale
267 * @throws IllegalArgumentException if {@code scale} is not in range {@code [1..18]}
269 public static Decimal64 maxValueIn(final int scale) {
270 return MAX_VALUE[offsetOf(scale)];
273 // >>> FIXME: these need truncating counterparts
274 public static Decimal64 valueOf(final int scale, final byte byteVal) {
275 final byte offset = offsetOf(scale);
276 final var conv = CONVERSION[offset];
277 if (byteVal < conv.minByte || byteVal > conv.maxByte) {
278 throw new IllegalArgumentException("Value " + byteVal + " is not in range ["
279 + conv.minByte + ".." + conv.maxByte + "] to fit scale " + scale);
281 return byteVal < 0 ? new Decimal64(offset, -byteVal, true) : new Decimal64(offset, byteVal, false);
284 public static Decimal64 valueOf(final int scale, final short shortVal) {
285 final byte offset = offsetOf(scale);
286 final var conv = CONVERSION[offset];
287 if (shortVal < conv.minShort || shortVal > conv.maxShort) {
288 throw new IllegalArgumentException("Value " + shortVal + " is not in range ["
289 + conv.minShort + ".." + conv.maxShort + "] to fit scale " + scale);
291 return shortVal < 0 ? new Decimal64(offset, -shortVal, true) : new Decimal64(offset, shortVal, false);
294 public static Decimal64 valueOf(final int scale, final int intVal) {
295 final byte offset = offsetOf(scale);
296 final var conv = CONVERSION[offset];
297 if (intVal < conv.minInt || intVal > conv.maxInt) {
298 throw new IllegalArgumentException("Value " + intVal + " is not in range ["
299 + conv.minInt + ".." + conv.maxInt + "] to fit scale " + scale);
301 return intVal < 0 ? new Decimal64(offset, - (long)intVal, true) : new Decimal64(offset, intVal, false);
304 public static Decimal64 valueOf(final int scale, final long longVal) {
305 final byte offset = offsetOf(scale);
306 final var conv = CONVERSION[offset];
307 if (longVal < conv.minLong || longVal > conv.maxLong) {
308 throw new IllegalArgumentException("Value " + longVal + " is not in range ["
309 + conv.minLong + ".." + conv.maxLong + "] to fit scale " + scale);
311 return longVal < 0 ? new Decimal64(offset, -longVal, true) : new Decimal64(offset, longVal, false);
315 // FIXME: this should take a RoundingMode and perform rounding
316 // FIXME: this should have a truncating counterpart
317 public static Decimal64 valueOf(final float floatVal, final RoundingMode rounding) {
318 // XXX: we should be able to do something smarter here
319 return valueOf(Float.toString(floatVal));
322 // FIXME: this should take a RoundingMode and perform rounding
323 // FIXME: this should have a truncating counterpart
324 public static Decimal64 valueOf(final double doubleVal, final RoundingMode rounding) {
325 // XXX: we should be able to do something smarter here
326 return valueOf(Double.toString(doubleVal));
329 public static Decimal64 valueOf(final BigDecimal decimalVal) {
330 // FIXME: we should be able to do something smarter here using BigDecimal.unscaledValue() and BigDecimal.scale()
331 return valueOf(decimalVal.toPlainString());
335 * Attempt to parse a String into a Decimal64. This method uses minimum fraction digits required to hold
338 * @param str String to parser
339 * @return A Decimal64 instance
340 * @throws NullPointerException if value is null.
341 * @throws NumberFormatException if the string does not contain a parsable decimal64.
343 public static Decimal64 valueOf(final String str) {
344 final Either<Decimal64, CanonicalValueViolation> variant = SUPPORT.fromString(str);
345 final Optional<Decimal64> value = variant.tryFirst();
346 if (value.isPresent()) {
349 final Optional<String> message = variant.getSecond().getMessage();
350 throw message.isPresent() ? new NumberFormatException(message.get()) : new NumberFormatException();
354 * Return the scale of this decimal. This is the number of fraction digits, in range {@code [1..18]}.
356 * @return This decimal's scale
358 public final int scale() {
363 * Return the unscaled value of this decimal.
365 * @return This decimal's unscaled value
367 public final long unscaledValue() {
372 * Return this decimal in the specified scale.
374 * @param scale target scale
375 * @return Scaled number
376 * @throws ArithmeticException if the conversion would overflow or require rounding
378 public Decimal64 scaleTo(final int scale) {
379 return scaleTo(scale, RoundingMode.UNNECESSARY);
383 * Return this decimal in the specified scale.
386 * @param roundingMode rounding mode
387 * @return Scaled number
388 * @throws ArithmeticException if the conversion would overflow or require rounding and {@code roundingMode} is
389 * {@link RoundingMode#UNNECESSARY}.
390 * @throws IllegalArgumentException if {@code scale} is not valid
391 * @throws NullPointerException if {@code roundingMode} is {@code null}
393 public Decimal64 scaleTo(final int scale, final RoundingMode roundingMode) {
394 final var mode = requireNonNull(roundingMode);
395 final byte scaleOffset = offsetOf(scale);
396 final int diff = scaleOffset - offset;
400 } else if (value == 0) {
401 // Zero is special, as it has the same unscaled value in all scales
402 return new Decimal64(scaleOffset, 0);
406 // Increasing scale is simple, as we have pre-calculated min/max boundaries and then it's just
407 // factor multiplication
408 final int diffOffset = diff - 1;
409 final var conv = CONVERSION[diffOffset];
410 if (value < conv.minLong || value > conv.maxLong) {
411 throw new ArithmeticException("Increasing scale of " + this + " to " + scale + " would overflow");
413 return new Decimal64(scaleOffset, value * FACTOR[diffOffset]);
416 // Decreasing scale is hard, as we need to deal with rounding
417 final int diffOffset = -diff - 1;
418 final long factor = FACTOR[diffOffset];
419 final long trunc = value / factor;
420 final long remainder = value - trunc * factor;
422 // No remainder, we do not need to involve rounding
423 if (remainder == 0) {
424 return new Decimal64(scaleOffset, trunc);
427 final long increment;
430 increment = Long.signum(trunc);
436 increment = Long.signum(trunc) > 0 ? 1 : 0;
439 increment = Long.signum(trunc) < 0 ? -1 : 0;
442 switch (RemainderSignificance.of(remainder, factor)) {
448 increment = Long.signum(trunc);
451 throw new IllegalStateException();
455 switch (RemainderSignificance.of(remainder, factor)) {
461 increment = Long.signum(trunc);
464 throw new IllegalStateException();
468 switch (RemainderSignificance.of(remainder, factor)) {
473 increment = (trunc & 0x1) != 0 ? Long.signum(trunc) : 0;
476 increment = Long.signum(trunc);
479 throw new IllegalStateException();
483 throw new ArithmeticException("Decreasing scale of " + this + " to " + scale + " requires rounding");
485 throw new IllegalStateException();
488 return new Decimal64(scaleOffset, trunc + increment);
491 public final BigDecimal decimalValue() {
492 return BigDecimal.valueOf(value, scale());
496 public final int intValue() {
497 return (int) intPart();
501 public final long longValue() {
506 public final float floatValue() {
507 return (float) doubleValue();
511 public final double doubleValue() {
512 return 1.0 * value / FACTOR[offset];
516 * Converts this {@code BigDecimal} to a {@code byte}, checking for lost information. If this {@code Decimal64} has
517 * a nonzero fractional part or is out of the possible range for a {@code byte} result then
518 * an {@code ArithmeticException} is thrown.
520 * @return this {@code Decimal64} converted to a {@code byte}.
521 * @throws ArithmeticException if {@code this} has a nonzero fractional part, or will not fit in a {@code byte}.
523 public final byte byteValueExact() {
524 final long val = longValueExact();
525 final byte ret = (byte) val;
527 throw new ArithmeticException("Value " + val + " is outside of byte range");
533 * Converts this {@code BigDecimal} to a {@code short}, checking for lost information. If this {@code Decimal64} has
534 * a nonzero fractional part or is out of the possible range for a {@code short} result then
535 * an {@code ArithmeticException} is thrown.
537 * @return this {@code Decimal64} converted to a {@code short}.
538 * @throws ArithmeticException if {@code this} has a nonzero fractional part, or will not fit in a {@code short}.
540 public final short shortValueExact() {
541 final long val = longValueExact();
542 final short ret = (short) val;
544 throw new ArithmeticException("Value " + val + " is outside of short range");
550 * Converts this {@code BigDecimal} to an {@code int}, checking for lost information. If this {@code Decimal64} has
551 * a nonzero fractional part or is out of the possible range for an {@code int} result then
552 * an {@code ArithmeticException} is thrown.
554 * @return this {@code Decimal64} converted to an {@code int}.
555 * @throws ArithmeticException if {@code this} has a nonzero fractional part, or will not fit in an {@code int}.
557 public final int intValueExact() {
558 final long val = longValueExact();
559 final int ret = (int) val;
561 throw new ArithmeticException("Value " + val + " is outside of integer range");
567 * Converts this {@code BigDecimal} to a {@code long}, checking for lost information. If this {@code Decimal64} has
568 * a nonzero fractional part then an {@code ArithmeticException} is thrown.
570 * @return this {@code Decimal64} converted to a {@code long}.
571 * @throws ArithmeticException if {@code this} has a nonzero fractional part.
573 public final long longValueExact() {
574 if (fracPart() != 0) {
575 throw new ArithmeticException("Conversion of " + this + " would lose fraction");
581 @SuppressWarnings("checkstyle:parameterName")
582 public final int compareTo(final Decimal64 o) {
586 if (offset == o.offset) {
587 return Long.compare(value, o.value);
590 // XXX: we could do something smarter here
591 return Double.compare(doubleValue(), o.doubleValue());
595 public final String toCanonicalString() {
596 // https://tools.ietf.org/html/rfc6020#section-9.3.2
598 // The canonical form of a positive decimal64 does not include the sign
599 // "+". The decimal point is required. Leading and trailing zeros are
600 // prohibited, subject to the rule that there MUST be at least one digit
601 // before and after the decimal point. The value zero is represented as
604 final long intPart = intPart();
605 final long fracPart = fracPart();
606 final StringBuilder sb = new StringBuilder(21);
607 if (intPart == 0 && fracPart < 0) {
610 sb.append(intPart).append('.');
613 // We may need to zero-pad the fraction part
614 sb.append(Strings.padStart(Long.toString(Math.abs(fracPart)), scale(), '0'));
619 return sb.toString();
623 public final CanonicalValueSupport<Decimal64> support() {
628 public final int hashCode() {
629 // We need to normalize the results in order to be consistent with equals()
630 return Long.hashCode(intPart()) * 31 + Long.hashCode(fracPart());
634 public final boolean equals(final @Nullable Object obj) {
635 return this == obj || obj instanceof Decimal64 && equalsImpl((Decimal64) obj);
639 * A slightly faster version of {@link #equals(Object)}.
641 * @param obj Decimal64 object
642 * @return {@code true} if this object is the same as the obj argument; {@code false} otherwise.
644 public final boolean equals(final @Nullable Decimal64 obj) {
645 return this == obj || obj != null && equalsImpl(obj);
649 public final String toString() {
650 return toCanonicalString();
653 private boolean equalsImpl(final Decimal64 other) {
654 return offset == other.offset ? value == other.value
655 // We need to normalize both
656 : intPart() == other.intPart() && fracPart() == other.fracPart();
659 private long intPart() {
660 return value / FACTOR[offset];
663 private long fracPart() {
664 return value % FACTOR[offset];
667 private static byte offsetOf(final int scale) {
668 checkArgument(scale >= 1 && scale <= MAX_SCALE, "Scale %s is not in range [1..%s]", scale, MAX_SCALE);
669 return (byte) (scale - 1);