2f2de8837a755bfd7d889de6b05074645da2460e
[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     static {
165         verify(SCALE.length == MAX_SCALE);
166     }
167
168     private final byte scaleOffset;
169     private final long value;
170
171     @VisibleForTesting
172     Decimal64(final int scale, final long intPart, final long fracPart, final boolean negative) {
173         scaleOffset = offsetOf(scale);
174
175         final long bits = intPart * SCALE[scaleOffset] + fracPart;
176         value = negative ? -bits : bits;
177     }
178
179     private Decimal64(final byte scaleOffset, final long value) {
180         this.scaleOffset = scaleOffset;
181         this.value = value;
182     }
183
184     protected Decimal64(final Decimal64 other) {
185         this(other.scaleOffset, other.value);
186     }
187
188     /**
189      * Return a {@link Decimal64} with specified scale and unscaled value.
190      *
191      * @param scale scale to use
192      * @param unscaledValue unscaled value to use
193      * @return A Decimal64 instance
194      * @throws IllegalArgumentException if {@code scale} is not in range {@code [1..18]}
195      */
196     public static Decimal64 of(final int scale, final long unscaledValue) {
197         return new Decimal64(offsetOf(scale), unscaledValue);
198     }
199
200     // >>> FIXME: these need to take a scale value and perform a range check. we also need truncating counterparts
201     public static Decimal64 valueOf(final byte byteVal) {
202         return byteVal < 0 ? new Decimal64(1, -byteVal, 0, true) : new Decimal64(1, byteVal, 0, false);
203     }
204
205     public static Decimal64 valueOf(final short shortVal) {
206         return shortVal < 0 ? new Decimal64(1, -shortVal, 0, true) : new Decimal64(1, shortVal, 0, false);
207     }
208
209     public static Decimal64 valueOf(final int intVal) {
210         return intVal < 0 ? new Decimal64(1, - (long)intVal, 0, true) : new Decimal64(1, intVal, 0, false);
211     }
212
213     public static Decimal64 valueOf(final long longVal) {
214         // XXX: we should be able to do something smarter here
215         return valueOf(Long.toString(longVal));
216     }
217     // <<< FIXME
218
219     // FIXME: this should take a RoundingMode and perform rounding
220     // FIXME: this should have a float counterpart
221     // FIXME: this should have a truncating
222     public static Decimal64 valueOf(final double doubleVal) {
223         // XXX: we should be able to do something smarter here
224         return valueOf(Double.toString(doubleVal));
225     }
226
227     public static Decimal64 valueOf(final BigDecimal decimalVal) {
228         // FIXME: we should be able to do something smarter here using BigDecimal.unscaledValue() and BigDecimal.scale()
229         return valueOf(decimalVal.toPlainString());
230     }
231
232     /**
233      * Attempt to parse a String into a Decimal64. This method uses minimum fraction digits required to hold
234      * the entire value.
235      *
236      * @param str String to parser
237      * @return A Decimal64 instance
238      * @throws NullPointerException if value is null.
239      * @throws NumberFormatException if the string does not contain a parsable decimal64.
240      */
241     public static Decimal64 valueOf(final String str) {
242         final Either<Decimal64, CanonicalValueViolation> variant = SUPPORT.fromString(str);
243         final Optional<Decimal64> value = variant.tryFirst();
244         if (value.isPresent()) {
245             return value.get();
246         }
247         final Optional<String> message = variant.getSecond().getMessage();
248         throw message.isPresent() ? new NumberFormatException(message.get()) : new NumberFormatException();
249     }
250
251     /**
252      * Return the scale of this decimal. This is the number of fraction digits, in range {@code [1..18]}.
253      *
254      * @return This decimal's scale
255      */
256     public final int scale() {
257         return scaleOffset + 1;
258     }
259
260     /**
261      * Return the unscaled value of this decimal.
262      *
263      * @return This decimal's unscaled value
264      */
265     public final long unscaledValue() {
266         return value;
267     }
268
269     public final BigDecimal decimalValue() {
270         return BigDecimal.valueOf(value, scale());
271     }
272
273     @Override
274     public final int intValue() {
275         return (int) intPart();
276     }
277
278     @Override
279     public final long longValue() {
280         return intPart();
281     }
282
283     @Override
284     public final float floatValue() {
285         return (float) doubleValue();
286     }
287
288     @Override
289     public final double doubleValue() {
290         return 1.0 * value / SCALE[scaleOffset];
291     }
292
293     /**
294      * Converts this {@code BigDecimal} to a {@code byte}, checking for lost information. If this {@code Decimal64} has
295      * a nonzero fractional part or is out of the possible range for a {@code byte} result then
296      * an {@code ArithmeticException} is thrown.
297      *
298      * @return this {@code Decimal64} converted to a {@code byte}.
299      * @throws ArithmeticException if {@code this} has a nonzero fractional part, or will not fit in a {@code byte}.
300      */
301     public final byte byteValueExact() {
302         final long val = longValueExact();
303         final byte ret = (byte) val;
304         if (val != ret) {
305             throw new ArithmeticException("Value " + val + " is outside of byte range");
306         }
307         return ret;
308     }
309
310     /**
311      * Converts this {@code BigDecimal} to a {@code short}, checking for lost information. If this {@code Decimal64} has
312      * a nonzero fractional part or is out of the possible range for a {@code short} result then
313      * an {@code ArithmeticException} is thrown.
314      *
315      * @return this {@code Decimal64} converted to a {@code short}.
316      * @throws ArithmeticException if {@code this} has a nonzero fractional part, or will not fit in a {@code short}.
317      */
318     public final short shortValueExact() {
319         final long val = longValueExact();
320         final short ret = (short) val;
321         if (val != ret) {
322             throw new ArithmeticException("Value " + val + " is outside of short range");
323         }
324         return ret;
325     }
326
327     /**
328      * Converts this {@code BigDecimal} to an {@code int}, checking for lost information. If this {@code Decimal64} has
329      * a nonzero fractional part or is out of the possible range for an {@code int} result then
330      * an {@code ArithmeticException} is thrown.
331      *
332      * @return this {@code Decimal64} converted to an {@code int}.
333      * @throws ArithmeticException if {@code this} has a nonzero fractional part, or will not fit in an {@code int}.
334      */
335     public final int intValueExact() {
336         final long val = longValueExact();
337         final int ret = (int) val;
338         if (val != ret) {
339             throw new ArithmeticException("Value " + val + " is outside of integer range");
340         }
341         return ret;
342     }
343
344     /**
345      * Converts this {@code BigDecimal} to a {@code long}, checking for lost information.  If this {@code Decimal64} has
346      * a nonzero fractional part then an {@code ArithmeticException} is thrown.
347      *
348      * @return this {@code Decimal64} converted to a {@code long}.
349      * @throws ArithmeticException if {@code this} has a nonzero fractional part.
350      */
351     public final long longValueExact() {
352         if (fracPart() != 0) {
353             throw new ArithmeticException("Conversion of " + this + " would lose fraction");
354         }
355         return intPart();
356     }
357
358     @Override
359     @SuppressWarnings("checkstyle:parameterName")
360     public final int compareTo(final Decimal64 o) {
361         if (this == o) {
362             return 0;
363         }
364         if (scaleOffset == o.scaleOffset) {
365             return Long.compare(value, o.value);
366         }
367
368         // XXX: we could do something smarter here
369         return Double.compare(doubleValue(), o.doubleValue());
370     }
371
372     @Override
373     public final String toCanonicalString() {
374         // https://tools.ietf.org/html/rfc6020#section-9.3.2
375         //
376         // The canonical form of a positive decimal64 does not include the sign
377         // "+".  The decimal point is required.  Leading and trailing zeros are
378         // prohibited, subject to the rule that there MUST be at least one digit
379         // before and after the decimal point.  The value zero is represented as
380         // "0.0".
381         final StringBuilder sb = new StringBuilder(21).append(intPart()).append('.');
382         final long fracPart = fracPart();
383         if (fracPart != 0) {
384             // We may need to zero-pad the fraction part
385             sb.append(Strings.padStart(Long.toString(fracPart), scale(), '0'));
386         } else {
387             sb.append('0');
388         }
389
390         return sb.toString();
391     }
392
393     @Override
394     public final CanonicalValueSupport<Decimal64> support() {
395         return SUPPORT;
396     }
397
398     @Override
399     public final int hashCode() {
400         // We need to normalize the results in order to be consistent with equals()
401         return Long.hashCode(intPart()) * 31 + Long.hashCode(fracPart());
402     }
403
404     @Override
405     public final boolean equals(final @Nullable Object obj) {
406         return this == obj || obj instanceof Decimal64 && equalsImpl((Decimal64) obj);
407     }
408
409     /**
410      * A slightly faster version of {@link #equals(Object)}.
411      *
412      * @param obj Decimal64 object
413      * @return {@code true} if this object is the same as the obj argument; {@code false} otherwise.
414      */
415     public final boolean equals(final @Nullable Decimal64 obj) {
416         return this == obj || obj != null && equalsImpl(obj);
417     }
418
419     @Override
420     public final String toString() {
421         return toCanonicalString();
422     }
423
424     private boolean equalsImpl(final Decimal64 other) {
425         return scaleOffset == other.scaleOffset ? value == other.value
426                 // We need to normalize both
427                 : intPart() == other.intPart() && fracPart() == other.fracPart();
428     }
429
430     private long intPart() {
431         return value / SCALE[scaleOffset];
432     }
433
434     private long fracPart() {
435         return Math.abs(value % SCALE[scaleOffset]);
436     }
437
438     private static byte offsetOf(final int scale) {
439         checkArgument(scale >= 1 && scale <= MAX_SCALE);
440         return (byte) (scale - 1);
441     }
442 }