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