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