Migrate RFC references to rfc-editor.org
[yangtools.git] / common / yang-common / src / main / java / org / opendaylight / yangtools / yang / common / Decimal64.java
1 /*
2  * Copyright (c) 2015 Pantheon Technologies s.r.o. and others.  All rights reserved.
3  *
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
7  */
8 package org.opendaylight.yangtools.yang.common;
9
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;
13
14 import com.google.common.annotations.Beta;
15 import com.google.common.annotations.VisibleForTesting;
16 import java.io.Serial;
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;
23
24 /**
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.
27  *
28  * @author Robert Varga
29  */
30 @Beta
31 @NonNullByDefault
32 public class Decimal64 extends Number implements CanonicalValue<Decimal64> {
33     public static final class Support extends AbstractCanonicalValueSupport<Decimal64> {
34         public Support() {
35             super(Decimal64.class);
36         }
37
38         @Override
39         public Either<Decimal64, CanonicalValueViolation> fromString(final String str) {
40             // https://www.rfc-editor.org/rfc/rfc6020#section-9.3.1
41             //
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.
46             if (str.isEmpty()) {
47                 return CanonicalValueViolation.variantOf("Empty string is not a valid decimal64 representation");
48             }
49
50             // Deal with optional sign
51             final boolean negative;
52             int idx = switch (str.charAt(0)) {
53                 case '-' -> {
54                     negative = true;
55                     yield 1;
56                 }
57                 case '+' -> {
58                     negative = false;
59                     yield 1;
60                 }
61                 default -> {
62                     negative = false;
63                     yield 0;
64                 }
65             };
66             // Sanity check length
67             if (idx == str.length()) {
68                 return CanonicalValueViolation.variantOf("Missing digits after sign");
69             }
70
71             // Character limit, used for caching and cutting trailing zeroes
72             int limit = str.length() - 1;
73
74             // Skip any leading zeroes, but leave at least one
75             for (; idx < limit && str.charAt(idx) == '0'; idx++) {
76                 final char ch = str.charAt(idx + 1);
77                 if (ch < '0' || ch > '9') {
78                     break;
79                 }
80             }
81
82             // Integer part and its length
83             int intLen = 0;
84             long intPart = 0;
85
86             for (; idx <= limit; idx++, intLen++) {
87                 final char ch = str.charAt(idx);
88                 if (ch == '.') {
89                     // Fractions are next
90                     break;
91                 }
92                 if (intLen == MAX_SCALE) {
93                     return CanonicalValueViolation.variantOf(
94                         "Integer part is longer than " + MAX_SCALE + " digits");
95                 }
96
97                 intPart = 10 * intPart + toInt(ch, idx);
98             }
99
100             if (idx > limit) {
101                 // No fraction digits, we are done
102                 return Either.ofFirst(new Decimal64((byte)1, intPart, 0, negative));
103             }
104
105             // Bump index to skip over period and check the remainder
106             idx++;
107             if (idx > limit) {
108                 return CanonicalValueViolation.variantOf("Value '" + str + "' is missing fraction digits");
109             }
110
111             // Trim trailing zeroes, if any
112             while (idx < limit && str.charAt(limit) == '0') {
113                 limit--;
114             }
115
116             final int fracLimit = MAX_SCALE - intLen + 1;
117             byte fracLen = 0;
118             long fracPart = 0;
119             for (; idx <= limit; idx++, fracLen++) {
120                 final char ch = str.charAt(idx);
121                 if (fracLen == fracLimit) {
122                     return CanonicalValueViolation.variantOf("Fraction part longer than " + fracLimit + " digits");
123                 }
124
125                 fracPart = 10 * fracPart + toInt(ch, idx);
126             }
127
128             return Either.ofFirst(new Decimal64(fracLen, intPart, fracPart, negative));
129         }
130
131         private static int toInt(final char ch, final int index) {
132             if (ch < '0' || ch > '9') {
133                 throw new NumberFormatException("Illegal character at offset " + index);
134             }
135             return ch - '0';
136         }
137     }
138
139     /**
140      * Tri-state indicator of how a non-zero remainder is significant to rounding.
141      */
142     private enum RemainderSignificance {
143         /**
144          * The remainder is less than the half of the interval.
145          */
146         LT_HALF,
147         /**
148          * The remainder is exactly half of the interval.
149          */
150         HALF,
151         /**
152          * The remainder is greater than the half of the interval.
153          */
154         GT_HALF;
155
156         static RemainderSignificance of(final long remainder, final long interval) {
157             final long absRemainder = Math.abs(remainder);
158             final long half = interval / 2;
159
160             if (absRemainder > half) {
161                 return GT_HALF;
162             } else if (absRemainder < half) {
163                 return LT_HALF;
164             } else {
165                 return HALF;
166             }
167         }
168     }
169
170     private static final CanonicalValueSupport<Decimal64> SUPPORT = new Support();
171     @Serial
172     private static final long serialVersionUID = 1L;
173
174     private static final int MAX_SCALE = 18;
175
176     private static final long[] FACTOR = {
177         10,
178         100,
179         1000,
180         10000,
181         100000,
182         1000000,
183         10000000,
184         100000000,
185         1000000000,
186         10000000000L,
187         100000000000L,
188         1000000000000L,
189         10000000000000L,
190         100000000000000L,
191         1000000000000000L,
192         10000000000000000L,
193         100000000000000000L,
194         1000000000000000000L
195     };
196
197     private static final Decimal64Conversion[] CONVERSION = Decimal64Conversion.values();
198     private static final Decimal64[] MIN_VALUE;
199     private static final Decimal64[] MAX_VALUE;
200
201     static {
202         verify(CONVERSION.length == MAX_SCALE);
203         verify(FACTOR.length == MAX_SCALE);
204
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);
210         }
211     }
212
213     private final byte offset;
214     private final long value;
215
216     @VisibleForTesting
217     Decimal64(final int scale, final long intPart, final long fracPart, final boolean negative) {
218         offset = offsetOf(scale);
219
220         final long bits = intPart * FACTOR[offset] + fracPart;
221         value = negative ? -bits : bits;
222     }
223
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;
228     }
229
230     private Decimal64(final byte offset, final long value) {
231         this.offset = offset;
232         this.value = value;
233     }
234
235     protected Decimal64(final Decimal64 other) {
236         this(other.offset, other.value);
237     }
238
239     /**
240      * Return a {@link Decimal64} with specified scale and unscaled value.
241      *
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]}
246      */
247     public static Decimal64 of(final int scale, final long unscaledValue) {
248         return new Decimal64(offsetOf(scale), unscaledValue);
249     }
250
251     /**
252      * Return the minimum value supported in specified scale.
253      *
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]}
257      */
258     public static Decimal64 minValueIn(final int scale) {
259         return MIN_VALUE[offsetOf(scale)];
260     }
261
262     /**
263      * Return the maximum value supported in specified scale.
264      *
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]}
268      */
269     public static Decimal64 maxValueIn(final int scale) {
270         return MAX_VALUE[offsetOf(scale)];
271     }
272
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);
280         }
281         return byteVal < 0 ? new Decimal64(offset, -byteVal, true) : new Decimal64(offset, byteVal, false);
282     }
283
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);
290         }
291         return shortVal < 0 ? new Decimal64(offset, -shortVal, true) : new Decimal64(offset, shortVal, false);
292     }
293
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);
300         }
301         return intVal < 0 ? new Decimal64(offset, - (long)intVal, true) : new Decimal64(offset, intVal, false);
302     }
303
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);
310         }
311         return longVal < 0 ? new Decimal64(offset, -longVal, true) : new Decimal64(offset, longVal, false);
312     }
313     // <<< FIXME
314
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));
320     }
321
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));
327     }
328
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());
332     }
333
334     /**
335      * Attempt to parse a String into a Decimal64. This method uses minimum fraction digits required to hold
336      * the entire value.
337      *
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.
342      */
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()) {
347             return value.orElseThrow();
348         }
349         final Optional<String> message = variant.getSecond().getMessage();
350         throw message.isPresent() ? new NumberFormatException(message.orElseThrow()) : new NumberFormatException();
351     }
352
353     /**
354      * Return the scale of this decimal. This is the number of fraction digits, in range {@code [1..18]}.
355      *
356      * @return This decimal's scale
357      */
358     public final int scale() {
359         return offset + 1;
360     }
361
362     /**
363      * Return the unscaled value of this decimal.
364      *
365      * @return This decimal's unscaled value
366      */
367     public final long unscaledValue() {
368         return value;
369     }
370
371     /**
372      * Return this decimal in the specified scale.
373      *
374      * @param scale target scale
375      * @return Scaled number
376      * @throws ArithmeticException if the conversion would overflow or require rounding
377      */
378     public Decimal64 scaleTo(final int scale) {
379         return scaleTo(scale, RoundingMode.UNNECESSARY);
380     }
381
382     /**
383      * Return this decimal in the specified scale.
384      *
385      * @param scale 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}
392      */
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;
397         if (diff == 0) {
398             // Same scale, no-op
399             return this;
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);
403         }
404
405         if (diff > 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");
412             }
413             return new Decimal64(scaleOffset, value * FACTOR[diffOffset]);
414         }
415
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;
421
422         // No remainder, we do not need to involve rounding
423         if (remainder == 0) {
424             return new Decimal64(scaleOffset, trunc);
425         }
426
427         final long increment = switch (mode) {
428             case UP -> Long.signum(trunc);
429             case DOWN -> 0;
430             case CEILING -> Long.signum(trunc) > 0 ? 1 : 0;
431             case FLOOR -> Long.signum(trunc) < 0 ? -1 : 0;
432             case HALF_UP -> switch (RemainderSignificance.of(remainder, factor)) {
433                 case LT_HALF -> 0;
434                 case HALF, GT_HALF -> Long.signum(trunc);
435             };
436             case HALF_DOWN -> switch (RemainderSignificance.of(remainder, factor)) {
437                 case LT_HALF, HALF -> 0;
438                 case GT_HALF -> Long.signum(trunc);
439             };
440             case HALF_EVEN -> switch (RemainderSignificance.of(remainder, factor)) {
441                 case LT_HALF -> 0;
442                 case HALF -> (trunc & 0x1) != 0 ? Long.signum(trunc) : 0;
443                 case GT_HALF -> Long.signum(trunc);
444             };
445             case UNNECESSARY ->
446                 throw new ArithmeticException("Decreasing scale of " + this + " to " + scale + " requires rounding");
447         };
448
449         return new Decimal64(scaleOffset, trunc + increment);
450     }
451
452     public final BigDecimal decimalValue() {
453         return BigDecimal.valueOf(value, scale());
454     }
455
456     @Override
457     public final int intValue() {
458         return (int) intPart();
459     }
460
461     @Override
462     public final long longValue() {
463         return intPart();
464     }
465
466     @Override
467     public final float floatValue() {
468         return (float) doubleValue();
469     }
470
471     @Override
472     public final double doubleValue() {
473         return 1.0 * value / FACTOR[offset];
474     }
475
476     /**
477      * Converts this {@code BigDecimal} to a {@code byte}, checking for lost information. If this {@code Decimal64} has
478      * a nonzero fractional part or is out of the possible range for a {@code byte} result then
479      * an {@code ArithmeticException} is thrown.
480      *
481      * @return this {@code Decimal64} converted to a {@code byte}.
482      * @throws ArithmeticException if {@code this} has a nonzero fractional part, or will not fit in a {@code byte}.
483      */
484     public final byte byteValueExact() {
485         final long val = longValueExact();
486         final byte ret = (byte) val;
487         if (val != ret) {
488             throw new ArithmeticException("Value " + val + " is outside of byte range");
489         }
490         return ret;
491     }
492
493     /**
494      * Converts this {@code BigDecimal} to a {@code short}, checking for lost information. If this {@code Decimal64} has
495      * a nonzero fractional part or is out of the possible range for a {@code short} result then
496      * an {@code ArithmeticException} is thrown.
497      *
498      * @return this {@code Decimal64} converted to a {@code short}.
499      * @throws ArithmeticException if {@code this} has a nonzero fractional part, or will not fit in a {@code short}.
500      */
501     public final short shortValueExact() {
502         final long val = longValueExact();
503         final short ret = (short) val;
504         if (val != ret) {
505             throw new ArithmeticException("Value " + val + " is outside of short range");
506         }
507         return ret;
508     }
509
510     /**
511      * Converts this {@code BigDecimal} to an {@code int}, checking for lost information. If this {@code Decimal64} has
512      * a nonzero fractional part or is out of the possible range for an {@code int} result then
513      * an {@code ArithmeticException} is thrown.
514      *
515      * @return this {@code Decimal64} converted to an {@code int}.
516      * @throws ArithmeticException if {@code this} has a nonzero fractional part, or will not fit in an {@code int}.
517      */
518     public final int intValueExact() {
519         final long val = longValueExact();
520         final int ret = (int) val;
521         if (val != ret) {
522             throw new ArithmeticException("Value " + val + " is outside of integer range");
523         }
524         return ret;
525     }
526
527     /**
528      * Converts this {@code BigDecimal} to a {@code long}, checking for lost information.  If this {@code Decimal64} has
529      * a nonzero fractional part then an {@code ArithmeticException} is thrown.
530      *
531      * @return this {@code Decimal64} converted to a {@code long}.
532      * @throws ArithmeticException if {@code this} has a nonzero fractional part.
533      */
534     public final long longValueExact() {
535         if (fracPart() != 0) {
536             throw new ArithmeticException("Conversion of " + this + " would lose fraction");
537         }
538         return intPart();
539     }
540
541     @Override
542     @SuppressWarnings("checkstyle:parameterName")
543     public final int compareTo(final Decimal64 o) {
544         if (this == o) {
545             return 0;
546         }
547         if (offset == o.offset) {
548             return Long.compare(value, o.value);
549         }
550
551         // XXX: we could do something smarter here
552         return Double.compare(doubleValue(), o.doubleValue());
553     }
554
555     @Override
556     public final String toCanonicalString() {
557         // https://www.rfc-editor.org/rfc/rfc6020#section-9.3.2
558         //
559         // The canonical form of a positive decimal64 does not include the sign
560         // "+".  The decimal point is required.  Leading and trailing zeros are
561         // prohibited, subject to the rule that there MUST be at least one digit
562         // before and after the decimal point.  The value zero is represented as
563         // "0.0".
564
565         // Pad unscaled value to scale + 1 size string starting after optional '-' sign
566         final var builder = new StringBuilder(21).append(value);
567         final int start = value < 0 ? 1 : 0;
568         final int scale = scale();
569         final int padding = scale + 1 + start - builder.length();
570         if (padding > 0) {
571             builder.insert(start, "0".repeat(padding));
572         }
573
574         // The first digit of the fraction part is now 'scale' from the end. We will insert the decimal point there,
575         // but also we it is the digit we never trim.
576         final int length = builder.length();
577         final int firstDecimal = length - scale;
578
579         // Remove trailing '0's from decimal part. We walk backwards from the last character stop at firstDecimal
580         int significantLength = length;
581         for (int i = length - 1; i > firstDecimal && builder.charAt(i) == '0'; --i) {
582             significantLength = i;
583         }
584         if (significantLength != length) {
585             builder.setLength(significantLength);
586         }
587
588         // Insert '.' before the first decimal and we're done
589         return builder.insert(firstDecimal, '.').toString();
590     }
591
592     @Override
593     public final CanonicalValueSupport<Decimal64> support() {
594         return SUPPORT;
595     }
596
597     @Override
598     public final int hashCode() {
599         // We need to normalize the results in order to be consistent with equals()
600         return Long.hashCode(intPart()) * 31 + Long.hashCode(fracPart());
601     }
602
603     @Override
604     public final boolean equals(final @Nullable Object obj) {
605         return this == obj || obj instanceof Decimal64 other && equalsImpl(other);
606     }
607
608     /**
609      * A slightly faster version of {@link #equals(Object)}.
610      *
611      * @param obj Decimal64 object
612      * @return {@code true} if this object is the same as the obj argument; {@code false} otherwise.
613      */
614     public final boolean equals(final @Nullable Decimal64 obj) {
615         return this == obj || obj != null && equalsImpl(obj);
616     }
617
618     @Override
619     public final String toString() {
620         return toCanonicalString();
621     }
622
623     private boolean equalsImpl(final Decimal64 other) {
624         return offset == other.offset ? value == other.value
625                 // We need to normalize both
626                 : intPart() == other.intPart() && fracPart() == other.fracPart();
627     }
628
629     private long intPart() {
630         return value / FACTOR[offset];
631     }
632
633     private long fracPart() {
634         return value % FACTOR[offset];
635     }
636
637     private static byte offsetOf(final int scale) {
638         checkArgument(scale >= 1 && scale <= MAX_SCALE, "Scale %s is not in range [1..%s]", scale, MAX_SCALE);
639         return (byte) (scale - 1);
640     }
641 }