c91e42f417935de228eca5f743cfbb06747c1c9b
[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;
52             switch (str.charAt(0)) {
53                 case '-':
54                     negative = true;
55                     idx = 1;
56                     break;
57                 case '+':
58                     negative = false;
59                     idx = 1;
60                     break;
61                 default:
62                     negative = false;
63                     idx = 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     private static final CanonicalValueSupport<Decimal64> SUPPORT = new Support();
140     private static final long serialVersionUID = 1L;
141
142     private static final int MAX_SCALE = 18;
143
144     private static final long[] FACTOR = {
145         10,
146         100,
147         1000,
148         10000,
149         100000,
150         1000000,
151         10000000,
152         100000000,
153         1000000000,
154         10000000000L,
155         100000000000L,
156         1000000000000L,
157         10000000000000L,
158         100000000000000L,
159         1000000000000000L,
160         10000000000000000L,
161         100000000000000000L,
162         1000000000000000000L
163     };
164
165     private static final Decimal64Conversion[] CONVERSION = Decimal64Conversion.values();
166     private static final Decimal64[] MIN_VALUE;
167     private static final Decimal64[] MAX_VALUE;
168
169     static {
170         verify(CONVERSION.length == MAX_SCALE);
171         verify(FACTOR.length == MAX_SCALE);
172
173         MIN_VALUE = new Decimal64[MAX_SCALE];
174         MAX_VALUE = new Decimal64[MAX_SCALE];
175         for (byte i = 0; i < MAX_SCALE; ++i) {
176             MIN_VALUE[i] = new Decimal64(i, -9223372036854775808L);
177             MAX_VALUE[i] = new Decimal64(i, 9223372036854775807L);
178         }
179     }
180
181     private final byte offset;
182     private final long value;
183
184     @VisibleForTesting
185     Decimal64(final int scale, final long intPart, final long fracPart, final boolean negative) {
186         offset = offsetOf(scale);
187
188         final long bits = intPart * FACTOR[offset] + fracPart;
189         value = negative ? -bits : bits;
190     }
191
192     private Decimal64(final byte offset, final long intPart, final boolean negative) {
193         this.offset = offset;
194         final long bits = intPart * FACTOR[offset];
195         value = negative ? -bits : bits;
196     }
197
198     private Decimal64(final byte offset, final long value) {
199         this.offset = offset;
200         this.value = value;
201     }
202
203     protected Decimal64(final Decimal64 other) {
204         this(other.offset, other.value);
205     }
206
207     /**
208      * Return a {@link Decimal64} with specified scale and unscaled value.
209      *
210      * @param scale scale to use
211      * @param unscaledValue unscaled value to use
212      * @return A Decimal64 instance
213      * @throws IllegalArgumentException if {@code scale} is not in range {@code [1..18]}
214      */
215     public static Decimal64 of(final int scale, final long unscaledValue) {
216         return new Decimal64(offsetOf(scale), unscaledValue);
217     }
218
219     /**
220      * Return the minimum value supported in specified scale.
221      *
222      * @param scale scale to use
223      * @return Minimum value in that scale
224      * @throws IllegalArgumentException if {@code scale} is not in range {@code [1..18]}
225      */
226     public static Decimal64 minValueIn(final int scale) {
227         return MIN_VALUE[offsetOf(scale)];
228     }
229
230     /**
231      * Return the maximum value supported in specified scale.
232      *
233      * @param scale scale to use
234      * @return Maximum value in that scale
235      * @throws IllegalArgumentException if {@code scale} is not in range {@code [1..18]}
236      */
237     public static Decimal64 maxValueIn(final int scale) {
238         return MAX_VALUE[offsetOf(scale)];
239     }
240
241     // >>> FIXME: these need truncating counterparts
242     public static Decimal64 valueOf(final int scale, final byte byteVal) {
243         final byte offset = offsetOf(scale);
244         final var conv = CONVERSION[offset];
245         if (byteVal < conv.minByte || byteVal > conv.maxByte) {
246             throw new IllegalArgumentException("Value " + byteVal + " is not in range ["
247                 + conv.minByte + ".." + conv.maxByte + "] to fit scale " + scale);
248         }
249         return byteVal < 0 ? new Decimal64(offset, -byteVal, true) : new Decimal64(offset, byteVal, false);
250     }
251
252     public static Decimal64 valueOf(final int scale, final short shortVal) {
253         final byte offset = offsetOf(scale);
254         final var conv = CONVERSION[offset];
255         if (shortVal < conv.minShort || shortVal > conv.maxShort) {
256             throw new IllegalArgumentException("Value " + shortVal + " is not in range ["
257                 + conv.minShort + ".." + conv.maxShort + "] to fit scale " + scale);
258         }
259         return shortVal < 0 ? new Decimal64(offset, -shortVal, true) : new Decimal64(offset, shortVal, false);
260     }
261
262     public static Decimal64 valueOf(final int scale, final int intVal) {
263         final byte offset = offsetOf(scale);
264         final var conv = CONVERSION[offset];
265         if (intVal < conv.minInt || intVal > conv.maxInt) {
266             throw new IllegalArgumentException("Value " + intVal + " is not in range ["
267                 + conv.minInt + ".." + conv.maxInt + "] to fit scale " + scale);
268         }
269         return intVal < 0 ? new Decimal64(offset, - (long)intVal, true) : new Decimal64(offset, intVal, false);
270     }
271
272     public static Decimal64 valueOf(final int scale, final long longVal) {
273         final byte offset = offsetOf(scale);
274         final var conv = CONVERSION[offset];
275         if (longVal < conv.minLong || longVal > conv.maxLong) {
276             throw new IllegalArgumentException("Value " + longVal + " is not in range ["
277                 + conv.minLong + ".." + conv.maxLong + "] to fit scale " + scale);
278         }
279         return longVal < 0 ? new Decimal64(offset, -longVal, true) : new Decimal64(offset, longVal, false);
280     }
281     // <<< FIXME
282
283     // FIXME: this should take a RoundingMode and perform rounding
284     // FIXME: this should have a truncating counterpart
285     public static Decimal64 valueOf(final float floatVal, final RoundingMode rounding) {
286         // XXX: we should be able to do something smarter here
287         return valueOf(Float.toString(floatVal));
288     }
289
290     // FIXME: this should take a RoundingMode and perform rounding
291     // FIXME: this should have a truncating counterpart
292     public static Decimal64 valueOf(final double doubleVal, final RoundingMode rounding) {
293         // XXX: we should be able to do something smarter here
294         return valueOf(Double.toString(doubleVal));
295     }
296
297     public static Decimal64 valueOf(final BigDecimal decimalVal) {
298         // FIXME: we should be able to do something smarter here using BigDecimal.unscaledValue() and BigDecimal.scale()
299         return valueOf(decimalVal.toPlainString());
300     }
301
302     /**
303      * Attempt to parse a String into a Decimal64. This method uses minimum fraction digits required to hold
304      * the entire value.
305      *
306      * @param str String to parser
307      * @return A Decimal64 instance
308      * @throws NullPointerException if value is null.
309      * @throws NumberFormatException if the string does not contain a parsable decimal64.
310      */
311     public static Decimal64 valueOf(final String str) {
312         final Either<Decimal64, CanonicalValueViolation> variant = SUPPORT.fromString(str);
313         final Optional<Decimal64> value = variant.tryFirst();
314         if (value.isPresent()) {
315             return value.get();
316         }
317         final Optional<String> message = variant.getSecond().getMessage();
318         throw message.isPresent() ? new NumberFormatException(message.get()) : new NumberFormatException();
319     }
320
321     /**
322      * Return the scale of this decimal. This is the number of fraction digits, in range {@code [1..18]}.
323      *
324      * @return This decimal's scale
325      */
326     public final int scale() {
327         return offset + 1;
328     }
329
330     /**
331      * Return the unscaled value of this decimal.
332      *
333      * @return This decimal's unscaled value
334      */
335     public final long unscaledValue() {
336         return value;
337     }
338
339     public final BigDecimal decimalValue() {
340         return BigDecimal.valueOf(value, scale());
341     }
342
343     @Override
344     public final int intValue() {
345         return (int) intPart();
346     }
347
348     @Override
349     public final long longValue() {
350         return intPart();
351     }
352
353     @Override
354     public final float floatValue() {
355         return (float) doubleValue();
356     }
357
358     @Override
359     public final double doubleValue() {
360         return 1.0 * value / FACTOR[offset];
361     }
362
363     /**
364      * Converts this {@code BigDecimal} to a {@code byte}, checking for lost information. If this {@code Decimal64} has
365      * a nonzero fractional part or is out of the possible range for a {@code byte} result then
366      * an {@code ArithmeticException} is thrown.
367      *
368      * @return this {@code Decimal64} converted to a {@code byte}.
369      * @throws ArithmeticException if {@code this} has a nonzero fractional part, or will not fit in a {@code byte}.
370      */
371     public final byte byteValueExact() {
372         final long val = longValueExact();
373         final byte ret = (byte) val;
374         if (val != ret) {
375             throw new ArithmeticException("Value " + val + " is outside of byte range");
376         }
377         return ret;
378     }
379
380     /**
381      * Converts this {@code BigDecimal} to a {@code short}, checking for lost information. If this {@code Decimal64} has
382      * a nonzero fractional part or is out of the possible range for a {@code short} result then
383      * an {@code ArithmeticException} is thrown.
384      *
385      * @return this {@code Decimal64} converted to a {@code short}.
386      * @throws ArithmeticException if {@code this} has a nonzero fractional part, or will not fit in a {@code short}.
387      */
388     public final short shortValueExact() {
389         final long val = longValueExact();
390         final short ret = (short) val;
391         if (val != ret) {
392             throw new ArithmeticException("Value " + val + " is outside of short range");
393         }
394         return ret;
395     }
396
397     /**
398      * Converts this {@code BigDecimal} to an {@code int}, checking for lost information. If this {@code Decimal64} has
399      * a nonzero fractional part or is out of the possible range for an {@code int} result then
400      * an {@code ArithmeticException} is thrown.
401      *
402      * @return this {@code Decimal64} converted to an {@code int}.
403      * @throws ArithmeticException if {@code this} has a nonzero fractional part, or will not fit in an {@code int}.
404      */
405     public final int intValueExact() {
406         final long val = longValueExact();
407         final int ret = (int) val;
408         if (val != ret) {
409             throw new ArithmeticException("Value " + val + " is outside of integer range");
410         }
411         return ret;
412     }
413
414     /**
415      * Converts this {@code BigDecimal} to a {@code long}, checking for lost information.  If this {@code Decimal64} has
416      * a nonzero fractional part then an {@code ArithmeticException} is thrown.
417      *
418      * @return this {@code Decimal64} converted to a {@code long}.
419      * @throws ArithmeticException if {@code this} has a nonzero fractional part.
420      */
421     public final long longValueExact() {
422         if (fracPart() != 0) {
423             throw new ArithmeticException("Conversion of " + this + " would lose fraction");
424         }
425         return intPart();
426     }
427
428     @Override
429     @SuppressWarnings("checkstyle:parameterName")
430     public final int compareTo(final Decimal64 o) {
431         if (this == o) {
432             return 0;
433         }
434         if (offset == o.offset) {
435             return Long.compare(value, o.value);
436         }
437
438         // XXX: we could do something smarter here
439         return Double.compare(doubleValue(), o.doubleValue());
440     }
441
442     @Override
443     public final String toCanonicalString() {
444         // https://tools.ietf.org/html/rfc6020#section-9.3.2
445         //
446         // The canonical form of a positive decimal64 does not include the sign
447         // "+".  The decimal point is required.  Leading and trailing zeros are
448         // prohibited, subject to the rule that there MUST be at least one digit
449         // before and after the decimal point.  The value zero is represented as
450         // "0.0".
451         final StringBuilder sb = new StringBuilder(21).append(intPart()).append('.');
452         final long fracPart = fracPart();
453         if (fracPart != 0) {
454             // We may need to zero-pad the fraction part
455             sb.append(Strings.padStart(Long.toString(fracPart), scale(), '0'));
456         } else {
457             sb.append('0');
458         }
459
460         return sb.toString();
461     }
462
463     @Override
464     public final CanonicalValueSupport<Decimal64> support() {
465         return SUPPORT;
466     }
467
468     @Override
469     public final int hashCode() {
470         // We need to normalize the results in order to be consistent with equals()
471         return Long.hashCode(intPart()) * 31 + Long.hashCode(fracPart());
472     }
473
474     @Override
475     public final boolean equals(final @Nullable Object obj) {
476         return this == obj || obj instanceof Decimal64 && equalsImpl((Decimal64) obj);
477     }
478
479     /**
480      * A slightly faster version of {@link #equals(Object)}.
481      *
482      * @param obj Decimal64 object
483      * @return {@code true} if this object is the same as the obj argument; {@code false} otherwise.
484      */
485     public final boolean equals(final @Nullable Decimal64 obj) {
486         return this == obj || obj != null && equalsImpl(obj);
487     }
488
489     @Override
490     public final String toString() {
491         return toCanonicalString();
492     }
493
494     private boolean equalsImpl(final Decimal64 other) {
495         return offset == other.offset ? value == other.value
496                 // We need to normalize both
497                 : intPart() == other.intPart() && fracPart() == other.fracPart();
498     }
499
500     private long intPart() {
501         return value / FACTOR[offset];
502     }
503
504     private long fracPart() {
505         return Math.abs(value % FACTOR[offset]);
506     }
507
508     private static byte offsetOf(final int scale) {
509         checkArgument(scale >= 1 && scale <= MAX_SCALE);
510         return (byte) (scale - 1);
511     }
512 }