1905d2c1456ba083e94afc113204989946027d82
[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.util.Optional;
18 import org.eclipse.jdt.annotation.NonNullByDefault;
19 import org.eclipse.jdt.annotation.Nullable;
20 import org.opendaylight.yangtools.concepts.Either;
21
22 /**
23  * Dedicated type for YANG's 'type decimal64' type. This class is similar to {@link BigDecimal}, but provides more
24  * efficient storage, as it has fixed precision.
25  *
26  * @author Robert Varga
27  */
28 @Beta
29 @NonNullByDefault
30 public class Decimal64 extends Number implements CanonicalValue<Decimal64> {
31     public static final class Support extends AbstractCanonicalValueSupport<Decimal64> {
32         public Support() {
33             super(Decimal64.class);
34         }
35
36         @Override
37         public Either<Decimal64, CanonicalValueViolation> fromString(final String str) {
38             // https://tools.ietf.org/html/rfc6020#section-9.3.1
39             //
40             // A decimal64 value is lexically represented as an optional sign ("+"
41             // or "-"), followed by a sequence of decimal digits, optionally
42             // followed by a period ('.') as a decimal indicator and a sequence of
43             // decimal digits.  If no sign is specified, "+" is assumed.
44             if (str.isEmpty()) {
45                 return CanonicalValueViolation.variantOf("Empty string is not a valid decimal64 representation");
46             }
47
48             // Deal with optional sign
49             final boolean negative;
50             int idx;
51             switch (str.charAt(0)) {
52                 case '-':
53                     negative = true;
54                     idx = 1;
55                     break;
56                 case '+':
57                     negative = false;
58                     idx = 1;
59                     break;
60                 default:
61                     negative = false;
62                     idx = 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[] SCALE = {
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 Decimal64[] MIN_VALUE;
165     private static final Decimal64[] MAX_VALUE;
166
167     static {
168         verify(SCALE.length == MAX_SCALE);
169
170         MIN_VALUE = new Decimal64[MAX_SCALE];
171         MAX_VALUE = new Decimal64[MAX_SCALE];
172         for (byte i = 0; i < MAX_SCALE; ++i) {
173             MIN_VALUE[i] = new Decimal64(i, -9223372036854775808L);
174             MAX_VALUE[i] = new Decimal64(i, 9223372036854775807L);
175         }
176     }
177
178     private final byte scaleOffset;
179     private final long value;
180
181     @VisibleForTesting
182     Decimal64(final int scale, final long intPart, final long fracPart, final boolean negative) {
183         scaleOffset = offsetOf(scale);
184
185         final long bits = intPart * SCALE[scaleOffset] + fracPart;
186         value = negative ? -bits : bits;
187     }
188
189     private Decimal64(final byte scaleOffset, final long value) {
190         this.scaleOffset = scaleOffset;
191         this.value = value;
192     }
193
194     protected Decimal64(final Decimal64 other) {
195         this(other.scaleOffset, other.value);
196     }
197
198     /**
199      * Return a {@link Decimal64} with specified scale and unscaled value.
200      *
201      * @param scale scale to use
202      * @param unscaledValue unscaled value to use
203      * @return A Decimal64 instance
204      * @throws IllegalArgumentException if {@code scale} is not in range {@code [1..18]}
205      */
206     public static Decimal64 of(final int scale, final long unscaledValue) {
207         return new Decimal64(offsetOf(scale), unscaledValue);
208     }
209
210     /**
211      * Return the minimum value supported in specified scale.
212      *
213      * @param scale scale to use
214      * @return Minimum value in that scale
215      * @throws IllegalArgumentException if {@code scale} is not in range {@code [1..18]}
216      */
217     public static Decimal64 minValueIn(final int scale) {
218         return MIN_VALUE[offsetOf(scale)];
219     }
220
221     /**
222      * Return the maximum value supported in specified scale.
223      *
224      * @param scale scale to use
225      * @return Maximum value in that scale
226      * @throws IllegalArgumentException if {@code scale} is not in range {@code [1..18]}
227      */
228     public static Decimal64 maxValueIn(final int scale) {
229         return MAX_VALUE[offsetOf(scale)];
230     }
231
232     // >>> FIXME: these need to take a scale value and perform a range check. we also need truncating counterparts
233     public static Decimal64 valueOf(final byte byteVal) {
234         return byteVal < 0 ? new Decimal64(1, -byteVal, 0, true) : new Decimal64(1, byteVal, 0, false);
235     }
236
237     public static Decimal64 valueOf(final short shortVal) {
238         return shortVal < 0 ? new Decimal64(1, -shortVal, 0, true) : new Decimal64(1, shortVal, 0, false);
239     }
240
241     public static Decimal64 valueOf(final int intVal) {
242         return intVal < 0 ? new Decimal64(1, - (long)intVal, 0, true) : new Decimal64(1, intVal, 0, false);
243     }
244
245     public static Decimal64 valueOf(final long longVal) {
246         // XXX: we should be able to do something smarter here
247         return valueOf(Long.toString(longVal));
248     }
249     // <<< FIXME
250
251     // FIXME: this should take a RoundingMode and perform rounding
252     // FIXME: this should have a float counterpart
253     // FIXME: this should have a truncating
254     public static Decimal64 valueOf(final double doubleVal) {
255         // XXX: we should be able to do something smarter here
256         return valueOf(Double.toString(doubleVal));
257     }
258
259     public static Decimal64 valueOf(final BigDecimal decimalVal) {
260         // FIXME: we should be able to do something smarter here using BigDecimal.unscaledValue() and BigDecimal.scale()
261         return valueOf(decimalVal.toPlainString());
262     }
263
264     /**
265      * Attempt to parse a String into a Decimal64. This method uses minimum fraction digits required to hold
266      * the entire value.
267      *
268      * @param str String to parser
269      * @return A Decimal64 instance
270      * @throws NullPointerException if value is null.
271      * @throws NumberFormatException if the string does not contain a parsable decimal64.
272      */
273     public static Decimal64 valueOf(final String str) {
274         final Either<Decimal64, CanonicalValueViolation> variant = SUPPORT.fromString(str);
275         final Optional<Decimal64> value = variant.tryFirst();
276         if (value.isPresent()) {
277             return value.get();
278         }
279         final Optional<String> message = variant.getSecond().getMessage();
280         throw message.isPresent() ? new NumberFormatException(message.get()) : new NumberFormatException();
281     }
282
283     /**
284      * Return the scale of this decimal. This is the number of fraction digits, in range {@code [1..18]}.
285      *
286      * @return This decimal's scale
287      */
288     public final int scale() {
289         return scaleOffset + 1;
290     }
291
292     /**
293      * Return the unscaled value of this decimal.
294      *
295      * @return This decimal's unscaled value
296      */
297     public final long unscaledValue() {
298         return value;
299     }
300
301     public final BigDecimal decimalValue() {
302         return BigDecimal.valueOf(value, scale());
303     }
304
305     @Override
306     public final int intValue() {
307         return (int) intPart();
308     }
309
310     @Override
311     public final long longValue() {
312         return intPart();
313     }
314
315     @Override
316     public final float floatValue() {
317         return (float) doubleValue();
318     }
319
320     @Override
321     public final double doubleValue() {
322         return 1.0 * value / SCALE[scaleOffset];
323     }
324
325     /**
326      * Converts this {@code BigDecimal} to a {@code byte}, checking for lost information. If this {@code Decimal64} has
327      * a nonzero fractional part or is out of the possible range for a {@code byte} result then
328      * an {@code ArithmeticException} is thrown.
329      *
330      * @return this {@code Decimal64} converted to a {@code byte}.
331      * @throws ArithmeticException if {@code this} has a nonzero fractional part, or will not fit in a {@code byte}.
332      */
333     public final byte byteValueExact() {
334         final long val = longValueExact();
335         final byte ret = (byte) val;
336         if (val != ret) {
337             throw new ArithmeticException("Value " + val + " is outside of byte range");
338         }
339         return ret;
340     }
341
342     /**
343      * Converts this {@code BigDecimal} to a {@code short}, checking for lost information. If this {@code Decimal64} has
344      * a nonzero fractional part or is out of the possible range for a {@code short} result then
345      * an {@code ArithmeticException} is thrown.
346      *
347      * @return this {@code Decimal64} converted to a {@code short}.
348      * @throws ArithmeticException if {@code this} has a nonzero fractional part, or will not fit in a {@code short}.
349      */
350     public final short shortValueExact() {
351         final long val = longValueExact();
352         final short ret = (short) val;
353         if (val != ret) {
354             throw new ArithmeticException("Value " + val + " is outside of short range");
355         }
356         return ret;
357     }
358
359     /**
360      * Converts this {@code BigDecimal} to an {@code int}, checking for lost information. If this {@code Decimal64} has
361      * a nonzero fractional part or is out of the possible range for an {@code int} result then
362      * an {@code ArithmeticException} is thrown.
363      *
364      * @return this {@code Decimal64} converted to an {@code int}.
365      * @throws ArithmeticException if {@code this} has a nonzero fractional part, or will not fit in an {@code int}.
366      */
367     public final int intValueExact() {
368         final long val = longValueExact();
369         final int ret = (int) val;
370         if (val != ret) {
371             throw new ArithmeticException("Value " + val + " is outside of integer range");
372         }
373         return ret;
374     }
375
376     /**
377      * Converts this {@code BigDecimal} to a {@code long}, checking for lost information.  If this {@code Decimal64} has
378      * a nonzero fractional part then an {@code ArithmeticException} is thrown.
379      *
380      * @return this {@code Decimal64} converted to a {@code long}.
381      * @throws ArithmeticException if {@code this} has a nonzero fractional part.
382      */
383     public final long longValueExact() {
384         if (fracPart() != 0) {
385             throw new ArithmeticException("Conversion of " + this + " would lose fraction");
386         }
387         return intPart();
388     }
389
390     @Override
391     @SuppressWarnings("checkstyle:parameterName")
392     public final int compareTo(final Decimal64 o) {
393         if (this == o) {
394             return 0;
395         }
396         if (scaleOffset == o.scaleOffset) {
397             return Long.compare(value, o.value);
398         }
399
400         // XXX: we could do something smarter here
401         return Double.compare(doubleValue(), o.doubleValue());
402     }
403
404     @Override
405     public final String toCanonicalString() {
406         // https://tools.ietf.org/html/rfc6020#section-9.3.2
407         //
408         // The canonical form of a positive decimal64 does not include the sign
409         // "+".  The decimal point is required.  Leading and trailing zeros are
410         // prohibited, subject to the rule that there MUST be at least one digit
411         // before and after the decimal point.  The value zero is represented as
412         // "0.0".
413         final StringBuilder sb = new StringBuilder(21).append(intPart()).append('.');
414         final long fracPart = fracPart();
415         if (fracPart != 0) {
416             // We may need to zero-pad the fraction part
417             sb.append(Strings.padStart(Long.toString(fracPart), scale(), '0'));
418         } else {
419             sb.append('0');
420         }
421
422         return sb.toString();
423     }
424
425     @Override
426     public final CanonicalValueSupport<Decimal64> support() {
427         return SUPPORT;
428     }
429
430     @Override
431     public final int hashCode() {
432         // We need to normalize the results in order to be consistent with equals()
433         return Long.hashCode(intPart()) * 31 + Long.hashCode(fracPart());
434     }
435
436     @Override
437     public final boolean equals(final @Nullable Object obj) {
438         return this == obj || obj instanceof Decimal64 && equalsImpl((Decimal64) obj);
439     }
440
441     /**
442      * A slightly faster version of {@link #equals(Object)}.
443      *
444      * @param obj Decimal64 object
445      * @return {@code true} if this object is the same as the obj argument; {@code false} otherwise.
446      */
447     public final boolean equals(final @Nullable Decimal64 obj) {
448         return this == obj || obj != null && equalsImpl(obj);
449     }
450
451     @Override
452     public final String toString() {
453         return toCanonicalString();
454     }
455
456     private boolean equalsImpl(final Decimal64 other) {
457         return scaleOffset == other.scaleOffset ? value == other.value
458                 // We need to normalize both
459                 : intPart() == other.intPart() && fracPart() == other.fracPart();
460     }
461
462     private long intPart() {
463         return value / SCALE[scaleOffset];
464     }
465
466     private long fracPart() {
467         return Math.abs(value % SCALE[scaleOffset]);
468     }
469
470     private static byte offsetOf(final int scale) {
471         checkArgument(scale >= 1 && scale <= MAX_SCALE);
472         return (byte) (scale - 1);
473     }
474 }