Convert base types to implement CanonicalValue
[yangtools.git] / yang / 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 org.eclipse.jdt.annotation.NonNullByDefault;
18 import org.eclipse.jdt.annotation.Nullable;
19
20 /**
21  * Dedicated type for YANG's 'type decimal64' type. This class is similar to {@link BigDecimal}, but provides more
22  * efficient storage, as it has fixed precision.
23  *
24  * @author Robert Varga
25  */
26 @Beta
27 @NonNullByDefault
28 public class Decimal64 extends Number implements CanonicalValue<Decimal64> {
29     private static final class Support extends AbstractCanonicalValueSupport<Decimal64> {
30         Support() {
31             super(Decimal64.class);
32         }
33
34         @Override
35         public Decimal64 fromString(final String str) {
36             return Decimal64.valueOf(str);
37         }
38     }
39
40     private static final CanonicalValueSupport<Decimal64> SUPPORT = new Support();
41     private static final long serialVersionUID = 1L;
42
43     private static final int MAX_FRACTION_DIGITS = 18;
44
45     private static final long[] SCALE = {
46         10,
47         100,
48         1000,
49         10000,
50         100000,
51         1000000,
52         10000000,
53         100000000,
54         1000000000,
55         10000000000L,
56         100000000000L,
57         1000000000000L,
58         10000000000000L,
59         100000000000000L,
60         1000000000000000L,
61         10000000000000000L,
62         100000000000000000L,
63         1000000000000000000L
64     };
65
66     static {
67         verify(SCALE.length == MAX_FRACTION_DIGITS);
68     }
69
70     private final byte scaleOffset;
71     private final long value;
72
73     @VisibleForTesting
74     Decimal64(final int fractionDigits, final long intPart, final long fracPart, final boolean negative) {
75         checkArgument(fractionDigits >= 1 && fractionDigits <= MAX_FRACTION_DIGITS);
76         this.scaleOffset = (byte) (fractionDigits - 1);
77
78         final long bits = intPart * SCALE[this.scaleOffset] + fracPart;
79         this.value = negative ? -bits : bits;
80     }
81
82     protected Decimal64(final Decimal64 other) {
83         this.scaleOffset = other.scaleOffset;
84         this.value = other.value;
85     }
86
87     public static Decimal64 valueOf(final byte byteVal) {
88         return byteVal < 0 ? new Decimal64(1, -byteVal, 0, true) : new Decimal64(1, byteVal, 0, false);
89     }
90
91     public static Decimal64 valueOf(final short shortVal) {
92         return shortVal < 0 ? new Decimal64(1, -shortVal, 0, true) : new Decimal64(1, shortVal, 0, false);
93     }
94
95     public static Decimal64 valueOf(final int intVal) {
96         return intVal < 0 ? new Decimal64(1, - (long)intVal, 0, true) : new Decimal64(1, intVal, 0, false);
97     }
98
99     public static Decimal64 valueOf(final long longVal) {
100         // XXX: we should be able to do something smarter here
101         return valueOf(Long.toString(longVal));
102     }
103
104     public static Decimal64 valueOf(final double doubleVal) {
105         // XXX: we should be able to do something smarter here
106         return valueOf(Double.toString(doubleVal));
107     }
108
109     public static Decimal64 valueOf(final BigDecimal decimalVal) {
110         // XXX: we should be able to do something smarter here
111         return valueOf(decimalVal.toPlainString());
112     }
113
114     /**
115      * Attempt to parse a String into a Decimal64. This method uses minimum fraction digits required to hold
116      * the entire value.
117      *
118      * @param str String to parser
119      * @return A Decimal64 instance
120      * @throws NullPointerException if value is null.
121      * @throws NumberFormatException if the string does not contain a parsable decimal64.
122      */
123     public static Decimal64 valueOf(final String str) {
124         // https://tools.ietf.org/html/rfc6020#section-9.3.1
125         //
126         // A decimal64 value is lexically represented as an optional sign ("+"
127         // or "-"), followed by a sequence of decimal digits, optionally
128         // followed by a period ('.') as a decimal indicator and a sequence of
129         // decimal digits.  If no sign is specified, "+" is assumed.
130         if (str.isEmpty()) {
131             throw new NumberFormatException("Empty string is not a valid decimal64 representation");
132         }
133
134         // Deal with optional sign
135         final boolean negative;
136         int idx;
137         switch (str.charAt(0)) {
138             case '-':
139                 negative = true;
140                 idx = 1;
141                 break;
142             case '+':
143                 negative = false;
144                 idx = 1;
145                 break;
146             default:
147                 negative = false;
148                 idx = 0;
149         }
150
151         // Sanity check length
152         if (idx == str.length()) {
153             throw new NumberFormatException("Missing digits after sign");
154         }
155
156         // Character limit, used for caching and cutting trailing zeroes
157         int limit = str.length() - 1;
158
159         // Skip any leading zeroes, but leave at least one
160         for (; idx < limit && str.charAt(idx) == '0'; idx++) {
161             final char ch = str.charAt(idx + 1);
162             if (ch < '0' || ch > '9') {
163                 break;
164             }
165         }
166
167         // Integer part and its length
168         int intLen = 0;
169         long intPart = 0;
170
171         for (; idx <= limit; idx++, intLen++) {
172             final char ch = str.charAt(idx);
173             if (ch == '.') {
174                 // Fractions are next
175                 break;
176             }
177             if (intLen == MAX_FRACTION_DIGITS) {
178                 throw new NumberFormatException("Integer part is longer than " + MAX_FRACTION_DIGITS + " digits");
179             }
180
181             intPart = 10 * intPart + toInt(ch, idx);
182         }
183
184         if (idx > limit) {
185             // No fraction digits, we are done
186             return new Decimal64((byte)1, intPart, 0, negative);
187         }
188
189         // Bump index to skip over period and check the remainder
190         idx++;
191         if (idx > limit) {
192             throw new NumberFormatException("Value '" + str + "' is missing fraction digits");
193         }
194
195         // Trim trailing zeroes, if any
196         while (idx < limit && str.charAt(limit) == '0') {
197             limit--;
198         }
199
200         final int fracLimit = MAX_FRACTION_DIGITS - intLen;
201         byte fracLen = 0;
202         long fracPart = 0;
203         for (; idx <= limit; idx++, fracLen++) {
204             final char ch = str.charAt(idx);
205             if (fracLen == fracLimit) {
206                 throw new NumberFormatException("Fraction part longer than " + fracLimit + " digits");
207             }
208
209             fracPart = 10 * fracPart + toInt(ch, idx);
210         }
211
212         return new Decimal64(fracLen, intPart, fracPart, negative);
213     }
214
215     public final BigDecimal decimalValue() {
216         return BigDecimal.valueOf(value, scaleOffset + 1);
217     }
218
219     @Override
220     public final int intValue() {
221         return (int) intPart();
222     }
223
224     @Override
225     public final long longValue() {
226         return intPart();
227     }
228
229     @Override
230     public final float floatValue() {
231         return (float) doubleValue();
232     }
233
234     @Override
235     public final double doubleValue() {
236         return 1.0 * value / SCALE[scaleOffset];
237     }
238
239     /**
240      * Converts this {@code BigDecimal} to a {@code byte}, checking for lost information. If this {@code Decimal64} has
241      * a nonzero fractional part or is out of the possible range for a {@code byte} result then
242      * an {@code ArithmeticException} is thrown.
243      *
244      * @return this {@code Decimal64} converted to a {@code byte}.
245      * @throws ArithmeticException if {@code this} has a nonzero fractional part, or will not fit in a {@code byte}.
246      */
247     public final byte byteValueExact() {
248         final long val = longValueExact();
249         final byte ret = (byte) val;
250         if (val != ret) {
251             throw new ArithmeticException("Value " + val + " is outside of byte range");
252         }
253         return ret;
254     }
255
256     /**
257      * Converts this {@code BigDecimal} to a {@code short}, checking for lost information. If this {@code Decimal64} has
258      * a nonzero fractional part or is out of the possible range for a {@code short} result then
259      * an {@code ArithmeticException} is thrown.
260      *
261      * @return this {@code Decimal64} converted to a {@code short}.
262      * @throws ArithmeticException if {@code this} has a nonzero fractional part, or will not fit in a {@code short}.
263      */
264     public final short shortValueExact() {
265         final long val = longValueExact();
266         final short ret = (short) val;
267         if (val != ret) {
268             throw new ArithmeticException("Value " + val + " is outside of short range");
269         }
270         return ret;
271     }
272
273     /**
274      * Converts this {@code BigDecimal} to an {@code int}, checking for lost information. If this {@code Decimal64} has
275      * a nonzero fractional part or is out of the possible range for an {@code int} result then
276      * an {@code ArithmeticException} is thrown.
277      *
278      * @return this {@code Decimal64} converted to an {@code int}.
279      * @throws ArithmeticException if {@code this} has a nonzero fractional part, or will not fit in an {@code int}.
280      */
281     public final int intValueExact() {
282         final long val = longValueExact();
283         final int ret = (int) val;
284         if (val != ret) {
285             throw new ArithmeticException("Value " + val + " is outside of integer range");
286         }
287         return ret;
288     }
289
290     /**
291      * Converts this {@code BigDecimal} to a {@code long}, checking for lost information.  If this {@code Decimal64} has
292      * a nonzero fractional part then an {@code ArithmeticException} is thrown.
293      *
294      * @return this {@code Decimal64} converted to a {@code long}.
295      * @throws ArithmeticException if {@code this} has a nonzero fractional part.
296      */
297     public final long longValueExact() {
298         if (fracPart() != 0) {
299             throw new ArithmeticException("Conversion of " + this + " would lose fraction");
300         }
301         return intPart();
302     }
303
304     @Override
305     @SuppressWarnings("checkstyle:parameterName")
306     public final int compareTo(final Decimal64 o) {
307         if (this == o) {
308             return 0;
309         }
310         if (scaleOffset == o.scaleOffset) {
311             return Long.compare(value, o.value);
312         }
313
314         // XXX: we could do something smarter here
315         return Double.compare(doubleValue(), o.doubleValue());
316     }
317
318     @Override
319     public final String toCanonicalString() {
320         // https://tools.ietf.org/html/rfc6020#section-9.3.2
321         //
322         // The canonical form of a positive decimal64 does not include the sign
323         // "+".  The decimal point is required.  Leading and trailing zeros are
324         // prohibited, subject to the rule that there MUST be at least one digit
325         // before and after the decimal point.  The value zero is represented as
326         // "0.0".
327         final StringBuilder sb = new StringBuilder(21).append(intPart()).append('.');
328         final long fracPart = fracPart();
329         if (fracPart != 0) {
330             // We may need to zero-pad the fraction part
331             sb.append(Strings.padStart(Long.toString(fracPart), scaleOffset + 1, '0'));
332         } else {
333             sb.append('0');
334         }
335
336         return sb.toString();
337     }
338
339     @Override
340     public final CanonicalValueSupport<Decimal64> support() {
341         return SUPPORT;
342     }
343
344     @Override
345     public final int hashCode() {
346         // We need to normalize the results in order to be consistent with equals()
347         return Long.hashCode(intPart()) * 31 + Long.hashCode(fracPart());
348     }
349
350     @Override
351     public final boolean equals(final @Nullable Object obj) {
352         if (this == obj) {
353             return true;
354         }
355         if (!(obj instanceof Decimal64)) {
356             return false;
357         }
358         final Decimal64 other = (Decimal64) obj;
359         if (scaleOffset == other.scaleOffset) {
360             return value == other.value;
361         }
362
363         // We need to normalize both
364         return intPart() == other.intPart() && fracPart() == fracPart();
365     }
366
367     @Override
368     public final String toString() {
369         return toCanonicalString();
370     }
371
372     private long intPart() {
373         return value / SCALE[scaleOffset];
374     }
375
376     private long fracPart() {
377         return Math.abs(value % SCALE[scaleOffset]);
378     }
379
380     private static int toInt(final char ch, final int index) {
381         if (ch < '0' || ch > '9') {
382             throw new NumberFormatException("Illegal character at offset " + index);
383         }
384         return ch - '0';
385     }
386 }