02bd64bb22d051a1e2c46069073f8723b6e23395
[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
13 import com.google.common.annotations.Beta;
14 import com.google.common.annotations.VisibleForTesting;
15 import com.google.common.base.Strings;
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  * @author Robert Varga
28  */
29 @Beta
30 @NonNullByDefault
31 public class Decimal64 extends Number implements CanonicalValue<Decimal64> {
32     public static final class Support extends AbstractCanonicalValueSupport<Decimal64> {
33         public Support() {
34             super(Decimal64.class);
35         }
36
37         @Override
38         public Either<Decimal64, CanonicalValueViolation> fromString(final String str) {
39             // https://tools.ietf.org/html/rfc6020#section-9.3.1
40             //
41             // A decimal64 value is lexically represented as an optional sign ("+"
42             // or "-"), followed by a sequence of decimal digits, optionally
43             // followed by a period ('.') as a decimal indicator and a sequence of
44             // decimal digits.  If no sign is specified, "+" is assumed.
45             if (str.isEmpty()) {
46                 return CanonicalValueViolation.variantOf("Empty string is not a valid decimal64 representation");
47             }
48
49             // Deal with optional sign
50             final boolean negative;
51             int idx = switch (str.charAt(0)) {
52                 case '-' -> {
53                     negative = true;
54                     yield 1;
55                 }
56                 case '+' -> {
57                     negative = false;
58                     yield 1;
59                 }
60                 default -> {
61                     negative = false;
62                     yield 0;
63                 }
64             };
65             // Sanity check length
66             if (idx == str.length()) {
67                 return CanonicalValueViolation.variantOf("Missing digits after sign");
68             }
69
70             // Character limit, used for caching and cutting trailing zeroes
71             int limit = str.length() - 1;
72
73             // Skip any leading zeroes, but leave at least one
74             for (; idx < limit && str.charAt(idx) == '0'; idx++) {
75                 final char ch = str.charAt(idx + 1);
76                 if (ch < '0' || ch > '9') {
77                     break;
78                 }
79             }
80
81             // Integer part and its length
82             int intLen = 0;
83             long intPart = 0;
84
85             for (; idx <= limit; idx++, intLen++) {
86                 final char ch = str.charAt(idx);
87                 if (ch == '.') {
88                     // Fractions are next
89                     break;
90                 }
91                 if (intLen == MAX_SCALE) {
92                     return CanonicalValueViolation.variantOf(
93                         "Integer part is longer than " + MAX_SCALE + " digits");
94                 }
95
96                 intPart = 10 * intPart + toInt(ch, idx);
97             }
98
99             if (idx > limit) {
100                 // No fraction digits, we are done
101                 return Either.ofFirst(new Decimal64((byte)1, intPart, 0, negative));
102             }
103
104             // Bump index to skip over period and check the remainder
105             idx++;
106             if (idx > limit) {
107                 return CanonicalValueViolation.variantOf("Value '" + str + "' is missing fraction digits");
108             }
109
110             // Trim trailing zeroes, if any
111             while (idx < limit && str.charAt(limit) == '0') {
112                 limit--;
113             }
114
115             final int fracLimit = MAX_SCALE - intLen + 1;
116             byte fracLen = 0;
117             long fracPart = 0;
118             for (; idx <= limit; idx++, fracLen++) {
119                 final char ch = str.charAt(idx);
120                 if (fracLen == fracLimit) {
121                     return CanonicalValueViolation.variantOf("Fraction part longer than " + fracLimit + " digits");
122                 }
123
124                 fracPart = 10 * fracPart + toInt(ch, idx);
125             }
126
127             return Either.ofFirst(new Decimal64(fracLen, intPart, fracPart, negative));
128         }
129
130         private static int toInt(final char ch, final int index) {
131             if (ch < '0' || ch > '9') {
132                 throw new NumberFormatException("Illegal character at offset " + index);
133             }
134             return ch - '0';
135         }
136     }
137
138     private static final CanonicalValueSupport<Decimal64> SUPPORT = new Support();
139     private static final long serialVersionUID = 1L;
140
141     private static final int MAX_SCALE = 18;
142
143     private static final long[] FACTOR = {
144         10,
145         100,
146         1000,
147         10000,
148         100000,
149         1000000,
150         10000000,
151         100000000,
152         1000000000,
153         10000000000L,
154         100000000000L,
155         1000000000000L,
156         10000000000000L,
157         100000000000000L,
158         1000000000000000L,
159         10000000000000000L,
160         100000000000000000L,
161         1000000000000000000L
162     };
163
164     private static final Decimal64Conversion[] CONVERSION = Decimal64Conversion.values();
165     private static final Decimal64[] MIN_VALUE;
166     private static final Decimal64[] MAX_VALUE;
167
168     static {
169         verify(CONVERSION.length == MAX_SCALE);
170         verify(FACTOR.length == MAX_SCALE);
171
172         MIN_VALUE = new Decimal64[MAX_SCALE];
173         MAX_VALUE = new Decimal64[MAX_SCALE];
174         for (byte i = 0; i < MAX_SCALE; ++i) {
175             MIN_VALUE[i] = new Decimal64(i, Long.MIN_VALUE);
176             MAX_VALUE[i] = new Decimal64(i, Long.MAX_VALUE);
177         }
178     }
179
180     private final byte offset;
181     private final long value;
182
183     @VisibleForTesting
184     Decimal64(final int scale, final long intPart, final long fracPart, final boolean negative) {
185         offset = offsetOf(scale);
186
187         final long bits = intPart * FACTOR[offset] + fracPart;
188         value = negative ? -bits : bits;
189     }
190
191     private Decimal64(final byte offset, final long intPart, final boolean negative) {
192         this.offset = offset;
193         final long bits = intPart * FACTOR[offset];
194         value = negative ? -bits : bits;
195     }
196
197     private Decimal64(final byte offset, final long value) {
198         this.offset = offset;
199         this.value = value;
200     }
201
202     protected Decimal64(final Decimal64 other) {
203         this(other.offset, other.value);
204     }
205
206     /**
207      * Return a {@link Decimal64} with specified scale and unscaled value.
208      *
209      * @param scale scale to use
210      * @param unscaledValue unscaled value to use
211      * @return A Decimal64 instance
212      * @throws IllegalArgumentException if {@code scale} is not in range {@code [1..18]}
213      */
214     public static Decimal64 of(final int scale, final long unscaledValue) {
215         return new Decimal64(offsetOf(scale), unscaledValue);
216     }
217
218     /**
219      * Return the minimum value supported in specified scale.
220      *
221      * @param scale scale to use
222      * @return Minimum value in that scale
223      * @throws IllegalArgumentException if {@code scale} is not in range {@code [1..18]}
224      */
225     public static Decimal64 minValueIn(final int scale) {
226         return MIN_VALUE[offsetOf(scale)];
227     }
228
229     /**
230      * Return the maximum value supported in specified scale.
231      *
232      * @param scale scale to use
233      * @return Maximum value in that scale
234      * @throws IllegalArgumentException if {@code scale} is not in range {@code [1..18]}
235      */
236     public static Decimal64 maxValueIn(final int scale) {
237         return MAX_VALUE[offsetOf(scale)];
238     }
239
240     // >>> FIXME: these need truncating counterparts
241     public static Decimal64 valueOf(final int scale, final byte byteVal) {
242         final byte offset = offsetOf(scale);
243         final var conv = CONVERSION[offset];
244         if (byteVal < conv.minByte || byteVal > conv.maxByte) {
245             throw new IllegalArgumentException("Value " + byteVal + " is not in range ["
246                 + conv.minByte + ".." + conv.maxByte + "] to fit scale " + scale);
247         }
248         return byteVal < 0 ? new Decimal64(offset, -byteVal, true) : new Decimal64(offset, byteVal, false);
249     }
250
251     public static Decimal64 valueOf(final int scale, final short shortVal) {
252         final byte offset = offsetOf(scale);
253         final var conv = CONVERSION[offset];
254         if (shortVal < conv.minShort || shortVal > conv.maxShort) {
255             throw new IllegalArgumentException("Value " + shortVal + " is not in range ["
256                 + conv.minShort + ".." + conv.maxShort + "] to fit scale " + scale);
257         }
258         return shortVal < 0 ? new Decimal64(offset, -shortVal, true) : new Decimal64(offset, shortVal, false);
259     }
260
261     public static Decimal64 valueOf(final int scale, final int intVal) {
262         final byte offset = offsetOf(scale);
263         final var conv = CONVERSION[offset];
264         if (intVal < conv.minInt || intVal > conv.maxInt) {
265             throw new IllegalArgumentException("Value " + intVal + " is not in range ["
266                 + conv.minInt + ".." + conv.maxInt + "] to fit scale " + scale);
267         }
268         return intVal < 0 ? new Decimal64(offset, - (long)intVal, true) : new Decimal64(offset, intVal, false);
269     }
270
271     public static Decimal64 valueOf(final int scale, final long longVal) {
272         final byte offset = offsetOf(scale);
273         final var conv = CONVERSION[offset];
274         if (longVal < conv.minLong || longVal > conv.maxLong) {
275             throw new IllegalArgumentException("Value " + longVal + " is not in range ["
276                 + conv.minLong + ".." + conv.maxLong + "] to fit scale " + scale);
277         }
278         return longVal < 0 ? new Decimal64(offset, -longVal, true) : new Decimal64(offset, longVal, false);
279     }
280     // <<< FIXME
281
282     // FIXME: this should take a RoundingMode and perform rounding
283     // FIXME: this should have a truncating counterpart
284     public static Decimal64 valueOf(final float floatVal, final RoundingMode rounding) {
285         // XXX: we should be able to do something smarter here
286         return valueOf(Float.toString(floatVal));
287     }
288
289     // FIXME: this should take a RoundingMode and perform rounding
290     // FIXME: this should have a truncating counterpart
291     public static Decimal64 valueOf(final double doubleVal, final RoundingMode rounding) {
292         // XXX: we should be able to do something smarter here
293         return valueOf(Double.toString(doubleVal));
294     }
295
296     public static Decimal64 valueOf(final BigDecimal decimalVal) {
297         // FIXME: we should be able to do something smarter here using BigDecimal.unscaledValue() and BigDecimal.scale()
298         return valueOf(decimalVal.toPlainString());
299     }
300
301     /**
302      * Attempt to parse a String into a Decimal64. This method uses minimum fraction digits required to hold
303      * the entire value.
304      *
305      * @param str String to parser
306      * @return A Decimal64 instance
307      * @throws NullPointerException if value is null.
308      * @throws NumberFormatException if the string does not contain a parsable decimal64.
309      */
310     public static Decimal64 valueOf(final String str) {
311         final Either<Decimal64, CanonicalValueViolation> variant = SUPPORT.fromString(str);
312         final Optional<Decimal64> value = variant.tryFirst();
313         if (value.isPresent()) {
314             return value.get();
315         }
316         final Optional<String> message = variant.getSecond().getMessage();
317         throw message.isPresent() ? new NumberFormatException(message.get()) : new NumberFormatException();
318     }
319
320     /**
321      * Return the scale of this decimal. This is the number of fraction digits, in range {@code [1..18]}.
322      *
323      * @return This decimal's scale
324      */
325     public final int scale() {
326         return offset + 1;
327     }
328
329     /**
330      * Return the unscaled value of this decimal.
331      *
332      * @return This decimal's unscaled value
333      */
334     public final long unscaledValue() {
335         return value;
336     }
337
338     public final BigDecimal decimalValue() {
339         return BigDecimal.valueOf(value, scale());
340     }
341
342     @Override
343     public final int intValue() {
344         return (int) intPart();
345     }
346
347     @Override
348     public final long longValue() {
349         return intPart();
350     }
351
352     @Override
353     public final float floatValue() {
354         return (float) doubleValue();
355     }
356
357     @Override
358     public final double doubleValue() {
359         return 1.0 * value / FACTOR[offset];
360     }
361
362     /**
363      * Converts this {@code BigDecimal} to a {@code byte}, checking for lost information. If this {@code Decimal64} has
364      * a nonzero fractional part or is out of the possible range for a {@code byte} result then
365      * an {@code ArithmeticException} is thrown.
366      *
367      * @return this {@code Decimal64} converted to a {@code byte}.
368      * @throws ArithmeticException if {@code this} has a nonzero fractional part, or will not fit in a {@code byte}.
369      */
370     public final byte byteValueExact() {
371         final long val = longValueExact();
372         final byte ret = (byte) val;
373         if (val != ret) {
374             throw new ArithmeticException("Value " + val + " is outside of byte range");
375         }
376         return ret;
377     }
378
379     /**
380      * Converts this {@code BigDecimal} to a {@code short}, checking for lost information. If this {@code Decimal64} has
381      * a nonzero fractional part or is out of the possible range for a {@code short} result then
382      * an {@code ArithmeticException} is thrown.
383      *
384      * @return this {@code Decimal64} converted to a {@code short}.
385      * @throws ArithmeticException if {@code this} has a nonzero fractional part, or will not fit in a {@code short}.
386      */
387     public final short shortValueExact() {
388         final long val = longValueExact();
389         final short ret = (short) val;
390         if (val != ret) {
391             throw new ArithmeticException("Value " + val + " is outside of short range");
392         }
393         return ret;
394     }
395
396     /**
397      * Converts this {@code BigDecimal} to an {@code int}, checking for lost information. If this {@code Decimal64} has
398      * a nonzero fractional part or is out of the possible range for an {@code int} result then
399      * an {@code ArithmeticException} is thrown.
400      *
401      * @return this {@code Decimal64} converted to an {@code int}.
402      * @throws ArithmeticException if {@code this} has a nonzero fractional part, or will not fit in an {@code int}.
403      */
404     public final int intValueExact() {
405         final long val = longValueExact();
406         final int ret = (int) val;
407         if (val != ret) {
408             throw new ArithmeticException("Value " + val + " is outside of integer range");
409         }
410         return ret;
411     }
412
413     /**
414      * Converts this {@code BigDecimal} to a {@code long}, checking for lost information.  If this {@code Decimal64} has
415      * a nonzero fractional part then an {@code ArithmeticException} is thrown.
416      *
417      * @return this {@code Decimal64} converted to a {@code long}.
418      * @throws ArithmeticException if {@code this} has a nonzero fractional part.
419      */
420     public final long longValueExact() {
421         if (fracPart() != 0) {
422             throw new ArithmeticException("Conversion of " + this + " would lose fraction");
423         }
424         return intPart();
425     }
426
427     @Override
428     @SuppressWarnings("checkstyle:parameterName")
429     public final int compareTo(final Decimal64 o) {
430         if (this == o) {
431             return 0;
432         }
433         if (offset == o.offset) {
434             return Long.compare(value, o.value);
435         }
436
437         // XXX: we could do something smarter here
438         return Double.compare(doubleValue(), o.doubleValue());
439     }
440
441     @Override
442     public final String toCanonicalString() {
443         // https://tools.ietf.org/html/rfc6020#section-9.3.2
444         //
445         // The canonical form of a positive decimal64 does not include the sign
446         // "+".  The decimal point is required.  Leading and trailing zeros are
447         // prohibited, subject to the rule that there MUST be at least one digit
448         // before and after the decimal point.  The value zero is represented as
449         // "0.0".
450
451         final long intPart = intPart();
452         final long fracPart = fracPart();
453         final StringBuilder sb = new StringBuilder(21);
454         if (intPart == 0 && fracPart < 0) {
455             sb.append('-');
456         }
457         sb.append(intPart).append('.');
458
459         if (fracPart != 0) {
460             // We may need to zero-pad the fraction part
461             sb.append(Strings.padStart(Long.toString(Math.abs(fracPart)), scale(), '0'));
462         } else {
463             sb.append('0');
464         }
465
466         return sb.toString();
467     }
468
469     @Override
470     public final CanonicalValueSupport<Decimal64> support() {
471         return SUPPORT;
472     }
473
474     @Override
475     public final int hashCode() {
476         // We need to normalize the results in order to be consistent with equals()
477         return Long.hashCode(intPart()) * 31 + Long.hashCode(fracPart());
478     }
479
480     @Override
481     public final boolean equals(final @Nullable Object obj) {
482         return this == obj || obj instanceof Decimal64 other && equalsImpl(other);
483     }
484
485     /**
486      * A slightly faster version of {@link #equals(Object)}.
487      *
488      * @param obj Decimal64 object
489      * @return {@code true} if this object is the same as the obj argument; {@code false} otherwise.
490      */
491     public final boolean equals(final @Nullable Decimal64 obj) {
492         return this == obj || obj != null && equalsImpl(obj);
493     }
494
495     @Override
496     public final String toString() {
497         return toCanonicalString();
498     }
499
500     private boolean equalsImpl(final Decimal64 other) {
501         return offset == other.offset ? value == other.value
502                 // We need to normalize both
503                 : intPart() == other.intPart() && fracPart() == other.fracPart();
504     }
505
506     private long intPart() {
507         return value / FACTOR[offset];
508     }
509
510     private long fracPart() {
511         return value % FACTOR[offset];
512     }
513
514     private static byte offsetOf(final int scale) {
515         checkArgument(scale >= 1 && scale <= MAX_SCALE, "Scale %s is not in range [1..%s]", scale, MAX_SCALE);
516         return (byte) (scale - 1);
517     }
518 }