a8eba1b9c58427cacbbc5545a67ad8844117a0bf
[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.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;
22
23 /**
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.
26  */
27 @NonNullByDefault
28 public class Decimal64 extends Number implements CanonicalValue<Decimal64> {
29     public static final class Support extends AbstractCanonicalValueSupport<Decimal64> {
30         public Support() {
31             super(Decimal64.class);
32         }
33
34         @Override
35         public Either<Decimal64, CanonicalValueViolation> fromString(final String str) {
36             // https://www.rfc-editor.org/rfc/rfc6020#section-9.3.1
37             //
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.
42             if (str.isEmpty()) {
43                 return CanonicalValueViolation.variantOf("Empty string is not a valid decimal64 representation");
44             }
45
46             // Deal with optional sign
47             final boolean negative;
48             int idx = switch (str.charAt(0)) {
49                 case '-' -> {
50                     negative = true;
51                     yield 1;
52                 }
53                 case '+' -> {
54                     negative = false;
55                     yield 1;
56                 }
57                 default -> {
58                     negative = false;
59                     yield 0;
60                 }
61             };
62             // Sanity check length
63             if (idx == str.length()) {
64                 return CanonicalValueViolation.variantOf("Missing digits after sign");
65             }
66
67             // Character limit, used for caching and cutting trailing zeroes
68             int limit = str.length() - 1;
69
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') {
74                     break;
75                 }
76             }
77
78             // Integer part and its length
79             int intLen = 0;
80             long intPart = 0;
81
82             for (; idx <= limit; idx++, intLen++) {
83                 final char ch = str.charAt(idx);
84                 if (ch == '.') {
85                     // Fractions are next
86                     break;
87                 }
88                 if (intLen == MAX_SCALE) {
89                     return CanonicalValueViolation.variantOf(
90                         "Integer part is longer than " + MAX_SCALE + " digits");
91                 }
92
93                 intPart = 10 * intPart + toInt(ch, idx);
94             }
95
96             if (idx > limit) {
97                 // No fraction digits, we are done
98                 return Either.ofFirst(new Decimal64((byte)1, intPart, 0, negative));
99             }
100
101             // Bump index to skip over period and check the remainder
102             idx++;
103             if (idx > limit) {
104                 return CanonicalValueViolation.variantOf("Value '" + str + "' is missing fraction digits");
105             }
106
107             // Trim trailing zeroes, if any
108             while (idx < limit && str.charAt(limit) == '0') {
109                 limit--;
110             }
111
112             final int fracLimit = MAX_SCALE - intLen + 1;
113             byte fracLen = 0;
114             long fracPart = 0;
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");
119                 }
120
121                 fracPart = 10 * fracPart + toInt(ch, idx);
122             }
123
124             return Either.ofFirst(new Decimal64(fracLen, intPart, fracPart, negative));
125         }
126
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);
130             }
131             return ch - '0';
132         }
133     }
134
135     /**
136      * Tri-state indicator of how a non-zero remainder is significant to rounding.
137      */
138     private enum RemainderSignificance {
139         /**
140          * The remainder is less than the half of the interval.
141          */
142         LT_HALF,
143         /**
144          * The remainder is exactly half of the interval.
145          */
146         HALF,
147         /**
148          * The remainder is greater than the half of the interval.
149          */
150         GT_HALF;
151
152         static RemainderSignificance of(final long remainder, final long interval) {
153             final long absRemainder = Math.abs(remainder);
154             final long half = interval / 2;
155
156             if (absRemainder > half) {
157                 return GT_HALF;
158             } else if (absRemainder < half) {
159                 return LT_HALF;
160             } else {
161                 return HALF;
162             }
163         }
164     }
165
166     private static final CanonicalValueSupport<Decimal64> SUPPORT = new Support();
167     @Serial
168     private static final long serialVersionUID = 1L;
169
170     private static final int MAX_SCALE = 18;
171
172     private static final long[] FACTOR = {
173         10,
174         100,
175         1000,
176         10000,
177         100000,
178         1000000,
179         10000000,
180         100000000,
181         1000000000,
182         10000000000L,
183         100000000000L,
184         1000000000000L,
185         10000000000000L,
186         100000000000000L,
187         1000000000000000L,
188         10000000000000000L,
189         100000000000000000L,
190         1000000000000000000L
191     };
192
193     private static final Decimal64Conversion[] CONVERSION = Decimal64Conversion.values();
194     private static final Decimal64[] MIN_VALUE;
195     private static final Decimal64[] MAX_VALUE;
196
197     static {
198         verify(CONVERSION.length == MAX_SCALE);
199         verify(FACTOR.length == MAX_SCALE);
200
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);
206         }
207     }
208
209     private final byte offset;
210     private final long value;
211
212     @VisibleForTesting
213     Decimal64(final int scale, final long intPart, final long fracPart, final boolean negative) {
214         offset = offsetOf(scale);
215
216         final long bits = intPart * FACTOR[offset] + fracPart;
217         value = negative ? -bits : bits;
218     }
219
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;
224     }
225
226     private Decimal64(final byte offset, final long value) {
227         this.offset = offset;
228         this.value = value;
229     }
230
231     protected Decimal64(final Decimal64 other) {
232         this(other.offset, other.value);
233     }
234
235     /**
236      * Return a {@link Decimal64} with specified scale and unscaled value.
237      *
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]}
242      */
243     public static Decimal64 of(final int scale, final long unscaledValue) {
244         return new Decimal64(offsetOf(scale), unscaledValue);
245     }
246
247     /**
248      * Return the minimum value supported in specified scale.
249      *
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]}
253      */
254     public static Decimal64 minValueIn(final int scale) {
255         return MIN_VALUE[offsetOf(scale)];
256     }
257
258     /**
259      * Return the maximum value supported in specified scale.
260      *
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]}
264      */
265     public static Decimal64 maxValueIn(final int scale) {
266         return MAX_VALUE[offsetOf(scale)];
267     }
268
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);
275         }
276         return byteVal < 0 ? new Decimal64(offset, -byteVal, true) : new Decimal64(offset, byteVal, false);
277     }
278
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);
284         }
285         return shortVal < 0 ? new Decimal64(offset, -shortVal, true) : new Decimal64(offset, shortVal, false);
286     }
287
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);
293         }
294         return intVal < 0 ? new Decimal64(offset, - (long)intVal, true) : new Decimal64(offset, intVal, false);
295     }
296
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);
302         }
303         return longVal < 0 ? new Decimal64(offset, -longVal, true) : new Decimal64(offset, longVal, false);
304     }
305
306     // <<< FIXME
307
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));
313     }
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 double doubleVal, final RoundingMode rounding) {
318         // XXX: we should be able to do something smarter here
319         return valueOf(Double.toString(doubleVal));
320     }
321
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());
325     }
326
327     /**
328      * Attempt to parse a String into a Decimal64. This method uses minimum fraction digits required to hold
329      * the entire value.
330      *
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.
335      */
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();
341         }
342         final Optional<String> message = variant.getSecond().getMessage();
343         throw message.isPresent() ? new NumberFormatException(message.orElseThrow()) : new NumberFormatException();
344     }
345
346     /**
347      * Return the scale of this decimal. This is the number of fraction digits, in range {@code [1..18]}.
348      *
349      * @return This decimal's scale
350      */
351     public final int scale() {
352         return offset + 1;
353     }
354
355     /**
356      * Return the unscaled value of this decimal.
357      *
358      * @return This decimal's unscaled value
359      */
360     public final long unscaledValue() {
361         return value;
362     }
363
364     /**
365      * Return this decimal in the specified scale.
366      *
367      * @param scale target scale
368      * @return Scaled number
369      * @throws ArithmeticException if the conversion would overflow or require rounding
370      */
371     public Decimal64 scaleTo(final int scale) {
372         return scaleTo(scale, RoundingMode.UNNECESSARY);
373     }
374
375     /**
376      * Return this decimal in the specified scale.
377      *
378      * @param scale 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}
385      */
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;
390         if (diff == 0) {
391             // Same scale, no-op
392             return this;
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);
396         }
397
398         if (diff > 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");
405             }
406             return new Decimal64(scaleOffset, value * FACTOR[diffOffset]);
407         }
408
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;
414
415         // No remainder, we do not need to involve rounding
416         if (remainder == 0) {
417             return new Decimal64(scaleOffset, trunc);
418         }
419
420         final long increment = switch (mode) {
421             case UP -> Long.signum(trunc);
422             case DOWN -> 0;
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)) {
426                 case LT_HALF -> 0;
427                 case HALF, GT_HALF -> Long.signum(trunc);
428             };
429             case HALF_DOWN -> switch (RemainderSignificance.of(remainder, factor)) {
430                 case LT_HALF, HALF -> 0;
431                 case GT_HALF -> Long.signum(trunc);
432             };
433             case HALF_EVEN -> switch (RemainderSignificance.of(remainder, factor)) {
434                 case LT_HALF -> 0;
435                 case HALF -> (trunc & 0x1) != 0 ? Long.signum(trunc) : 0;
436                 case GT_HALF -> Long.signum(trunc);
437             };
438             case UNNECESSARY ->
439                 throw new ArithmeticException("Decreasing scale of " + this + " to " + scale + " requires rounding");
440         };
441
442         return new Decimal64(scaleOffset, trunc + increment);
443     }
444
445     public final BigDecimal decimalValue() {
446         return BigDecimal.valueOf(value, scale());
447     }
448
449     @Override
450     public final int intValue() {
451         return (int) intPart();
452     }
453
454     @Override
455     public final long longValue() {
456         return intPart();
457     }
458
459     @Override
460     public final float floatValue() {
461         return (float) doubleValue();
462     }
463
464     @Override
465     public final double doubleValue() {
466         return 1.0 * value / FACTOR[offset];
467     }
468
469     /**
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.
473      *
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}.
476      */
477     public final byte byteValueExact() {
478         final long val = longValueExact();
479         final byte ret = (byte) val;
480         if (val != ret) {
481             throw ae("byte", val);
482         }
483         return ret;
484     }
485
486     /**
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.
490      *
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}.
493      */
494     public final short shortValueExact() {
495         final long val = longValueExact();
496         final short ret = (short) val;
497         if (val != ret) {
498             throw ae("short", val);
499         }
500         return ret;
501     }
502
503     /**
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.
507      *
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}.
510      */
511     public final int intValueExact() {
512         final long val = longValueExact();
513         final int ret = (int) val;
514         if (val != ret) {
515             throw ae("integer", val);
516         }
517         return ret;
518     }
519
520     /**
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.
523      *
524      * @return this {@code Decimal64} converted to a {@code long}.
525      * @throws ArithmeticException if {@code this} has a nonzero fractional part.
526      */
527     public final long longValueExact() {
528         if (fracPart() != 0) {
529             throw new ArithmeticException("Conversion of " + this + " would lose fraction");
530         }
531         return intPart();
532     }
533
534     @Override
535     @SuppressWarnings("checkstyle:parameterName")
536     public final int compareTo(final Decimal64 o) {
537         if (this == o) {
538             return 0;
539         }
540         if (offset == o.offset) {
541             return Long.compare(value, o.value);
542         }
543
544         // XXX: we could do something smarter here
545         return Double.compare(doubleValue(), o.doubleValue());
546     }
547
548     @Override
549     public final String toCanonicalString() {
550         // https://www.rfc-editor.org/rfc/rfc6020#section-9.3.2
551         //
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
556         // "0.0".
557
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();
563         if (padding > 0) {
564             builder.insert(start, "0".repeat(padding));
565         }
566
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;
571
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;
576         }
577         if (significantLength != length) {
578             builder.setLength(significantLength);
579         }
580
581         // Insert '.' before the first decimal and we're done
582         return builder.insert(firstDecimal, '.').toString();
583     }
584
585     @Override
586     public final CanonicalValueSupport<Decimal64> support() {
587         return SUPPORT;
588     }
589
590     @Override
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());
594     }
595
596     @Override
597     public final boolean equals(final @Nullable Object obj) {
598         return this == obj || obj instanceof Decimal64 other && equalsImpl(other);
599     }
600
601     /**
602      * A slightly faster version of {@link #equals(Object)}.
603      *
604      * @param obj Decimal64 object
605      * @return {@code true} if this object is the same as the obj argument; {@code false} otherwise.
606      */
607     public final boolean equals(final @Nullable Decimal64 obj) {
608         return this == obj || obj != null && equalsImpl(obj);
609     }
610
611     @Override
612     public final String toString() {
613         return toCanonicalString();
614     }
615
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();
620     }
621
622     private long intPart() {
623         return value / FACTOR[offset];
624     }
625
626     private long fracPart() {
627         return value % FACTOR[offset];
628     }
629
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);
633     }
634
635     private static ArithmeticException ae(final String type, final long val) {
636         return new ArithmeticException("Value " + val + " is outside of " + type + " range");
637     }
638
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);
642     }
643 }