Add Decimal64.scaleTo()
[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 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;
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://tools.ietf.org/html/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;
53             switch (str.charAt(0)) {
54                 case '-':
55                     negative = true;
56                     idx = 1;
57                     break;
58                 case '+':
59                     negative = false;
60                     idx = 1;
61                     break;
62                 default:
63                     negative = false;
64                     idx = 0;
65             }
66
67             // Sanity check length
68             if (idx == str.length()) {
69                 return CanonicalValueViolation.variantOf("Missing digits after sign");
70             }
71
72             // Character limit, used for caching and cutting trailing zeroes
73             int limit = str.length() - 1;
74
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') {
79                     break;
80                 }
81             }
82
83             // Integer part and its length
84             int intLen = 0;
85             long intPart = 0;
86
87             for (; idx <= limit; idx++, intLen++) {
88                 final char ch = str.charAt(idx);
89                 if (ch == '.') {
90                     // Fractions are next
91                     break;
92                 }
93                 if (intLen == MAX_SCALE) {
94                     return CanonicalValueViolation.variantOf(
95                         "Integer part is longer than " + MAX_SCALE + " digits");
96                 }
97
98                 intPart = 10 * intPart + toInt(ch, idx);
99             }
100
101             if (idx > limit) {
102                 // No fraction digits, we are done
103                 return Either.ofFirst(new Decimal64((byte)1, intPart, 0, negative));
104             }
105
106             // Bump index to skip over period and check the remainder
107             idx++;
108             if (idx > limit) {
109                 return CanonicalValueViolation.variantOf("Value '" + str + "' is missing fraction digits");
110             }
111
112             // Trim trailing zeroes, if any
113             while (idx < limit && str.charAt(limit) == '0') {
114                 limit--;
115             }
116
117             final int fracLimit = MAX_SCALE - intLen + 1;
118             byte fracLen = 0;
119             long fracPart = 0;
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");
124                 }
125
126                 fracPart = 10 * fracPart + toInt(ch, idx);
127             }
128
129             return Either.ofFirst(new Decimal64(fracLen, intPart, fracPart, negative));
130         }
131
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);
135             }
136             return ch - '0';
137         }
138     }
139
140     /**
141      * Tri-state indicator of how a non-zero remainder is significant to rounding.
142      */
143     private enum RemainderSignificance {
144         /**
145          * The remainder is less than the half of the interval.
146          */
147         LT_HALF,
148         /**
149          * The remainder is exactly half of the interval.
150          */
151         HALF,
152         /**
153          * The remainder is greater than the half of the interval.
154          */
155         GT_HALF;
156
157         static RemainderSignificance of(final long remainder, final long interval) {
158             final long absRemainder = Math.abs(remainder);
159             final long half = interval / 2;
160
161             if (absRemainder > half) {
162                 return GT_HALF;
163             } else if (absRemainder < half) {
164                 return LT_HALF;
165             } else {
166                 return HALF;
167             }
168         }
169     }
170
171     private static final CanonicalValueSupport<Decimal64> SUPPORT = new Support();
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.get();
348         }
349         final Optional<String> message = variant.getSecond().getMessage();
350         throw message.isPresent() ? new NumberFormatException(message.get()) : 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;
428         switch (mode) {
429             case UP:
430                 increment = Long.signum(trunc);
431                 break;
432             case DOWN:
433                 increment = 0;
434                 break;
435             case CEILING:
436                 increment = Long.signum(trunc) > 0 ? 1 : 0;
437                 break;
438             case FLOOR:
439                 increment = Long.signum(trunc) < 0 ? -1 : 0;
440                 break;
441             case HALF_UP:
442                 switch (RemainderSignificance.of(remainder, factor)) {
443                     case LT_HALF:
444                         increment = 0;
445                         break;
446                     case HALF:
447                     case GT_HALF:
448                         increment = Long.signum(trunc);
449                         break;
450                     default:
451                         throw new IllegalStateException();
452                 }
453                 break;
454             case HALF_DOWN:
455                 switch (RemainderSignificance.of(remainder, factor)) {
456                     case LT_HALF:
457                     case HALF:
458                         increment = 0;
459                         break;
460                     case GT_HALF:
461                         increment = Long.signum(trunc);
462                         break;
463                     default:
464                         throw new IllegalStateException();
465                 }
466                 break;
467             case HALF_EVEN:
468                 switch (RemainderSignificance.of(remainder, factor)) {
469                     case LT_HALF:
470                         increment = 0;
471                         break;
472                     case HALF:
473                         increment = (trunc & 0x1) != 0 ? Long.signum(trunc) : 0;
474                         break;
475                     case GT_HALF:
476                         increment = Long.signum(trunc);
477                         break;
478                     default:
479                         throw new IllegalStateException();
480                 }
481                 break;
482             case UNNECESSARY:
483                 throw new ArithmeticException("Decreasing scale of " + this + " to " + scale + " requires rounding");
484             default:
485                 throw new IllegalStateException();
486         }
487
488         return new Decimal64(scaleOffset, trunc + increment);
489     }
490
491     public final BigDecimal decimalValue() {
492         return BigDecimal.valueOf(value, scale());
493     }
494
495     @Override
496     public final int intValue() {
497         return (int) intPart();
498     }
499
500     @Override
501     public final long longValue() {
502         return intPart();
503     }
504
505     @Override
506     public final float floatValue() {
507         return (float) doubleValue();
508     }
509
510     @Override
511     public final double doubleValue() {
512         return 1.0 * value / FACTOR[offset];
513     }
514
515     /**
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.
519      *
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}.
522      */
523     public final byte byteValueExact() {
524         final long val = longValueExact();
525         final byte ret = (byte) val;
526         if (val != ret) {
527             throw new ArithmeticException("Value " + val + " is outside of byte range");
528         }
529         return ret;
530     }
531
532     /**
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.
536      *
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}.
539      */
540     public final short shortValueExact() {
541         final long val = longValueExact();
542         final short ret = (short) val;
543         if (val != ret) {
544             throw new ArithmeticException("Value " + val + " is outside of short range");
545         }
546         return ret;
547     }
548
549     /**
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.
553      *
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}.
556      */
557     public final int intValueExact() {
558         final long val = longValueExact();
559         final int ret = (int) val;
560         if (val != ret) {
561             throw new ArithmeticException("Value " + val + " is outside of integer range");
562         }
563         return ret;
564     }
565
566     /**
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.
569      *
570      * @return this {@code Decimal64} converted to a {@code long}.
571      * @throws ArithmeticException if {@code this} has a nonzero fractional part.
572      */
573     public final long longValueExact() {
574         if (fracPart() != 0) {
575             throw new ArithmeticException("Conversion of " + this + " would lose fraction");
576         }
577         return intPart();
578     }
579
580     @Override
581     @SuppressWarnings("checkstyle:parameterName")
582     public final int compareTo(final Decimal64 o) {
583         if (this == o) {
584             return 0;
585         }
586         if (offset == o.offset) {
587             return Long.compare(value, o.value);
588         }
589
590         // XXX: we could do something smarter here
591         return Double.compare(doubleValue(), o.doubleValue());
592     }
593
594     @Override
595     public final String toCanonicalString() {
596         // https://tools.ietf.org/html/rfc6020#section-9.3.2
597         //
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
602         // "0.0".
603
604         final long intPart = intPart();
605         final long fracPart = fracPart();
606         final StringBuilder sb = new StringBuilder(21);
607         if (intPart == 0 && fracPart < 0) {
608             sb.append('-');
609         }
610         sb.append(intPart).append('.');
611
612         if (fracPart != 0) {
613             // We may need to zero-pad the fraction part
614             sb.append(Strings.padStart(Long.toString(Math.abs(fracPart)), scale(), '0'));
615         } else {
616             sb.append('0');
617         }
618
619         return sb.toString();
620     }
621
622     @Override
623     public final CanonicalValueSupport<Decimal64> support() {
624         return SUPPORT;
625     }
626
627     @Override
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());
631     }
632
633     @Override
634     public final boolean equals(final @Nullable Object obj) {
635         return this == obj || obj instanceof Decimal64 && equalsImpl((Decimal64) obj);
636     }
637
638     /**
639      * A slightly faster version of {@link #equals(Object)}.
640      *
641      * @param obj Decimal64 object
642      * @return {@code true} if this object is the same as the obj argument; {@code false} otherwise.
643      */
644     public final boolean equals(final @Nullable Decimal64 obj) {
645         return this == obj || obj != null && equalsImpl(obj);
646     }
647
648     @Override
649     public final String toString() {
650         return toCanonicalString();
651     }
652
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();
657     }
658
659     private long intPart() {
660         return value / FACTOR[offset];
661     }
662
663     private long fracPart() {
664         return value % FACTOR[offset];
665     }
666
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);
670     }
671 }