BUG-4661: Introduce Decimal64, Empty, Uint{8,16,32,64}
[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, -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     @Override
222     @SuppressWarnings("checkstyle:parameterName")
223     public int compareTo(final Decimal64 o) {
224         if (this == o) {
225             return 0;
226         }
227         if (scaleOffset == o.scaleOffset) {
228             return Long.compare(value, o.value);
229         }
230
231         // XXX: we could do something smarter here
232         return Double.compare(doubleValue(), o.doubleValue());
233     }
234
235     @Override
236     public int hashCode() {
237         // We need to normalize the results in order to be consistent with equals()
238         return Longs.hashCode(intPart()) * 31 + Long.hashCode(fracPart());
239     }
240
241     @Override
242     public boolean equals(final Object obj) {
243         if (this == obj) {
244             return true;
245         }
246         if (!(obj instanceof Decimal64)) {
247             return false;
248         }
249         final Decimal64 other = (Decimal64) obj;
250         if (scaleOffset == other.scaleOffset) {
251             return value == other.value;
252         }
253
254         // We need to normalize both
255         return intPart() == other.intPart() && fracPart() == fracPart();
256     }
257
258     @Override
259     public String toString() {
260         // https://tools.ietf.org/html/rfc6020#section-9.3.2
261         //
262         // The canonical form of a positive decimal64 does not include the sign
263         // "+".  The decimal point is required.  Leading and trailing zeros are
264         // prohibited, subject to the rule that there MUST be at least one digit
265         // before and after the decimal point.  The value zero is represented as
266         // "0.0".
267         final StringBuilder sb = new StringBuilder(21).append(intPart()).append('.');
268         final long fracPart = fracPart();
269         if (fracPart != 0) {
270             // We may need to zero-pad the fraction part
271             sb.append(Strings.padStart(Long.toString(fracPart), scaleOffset + 1, '0'));
272         } else {
273             sb.append('0');
274         }
275
276         return sb.toString();
277     }
278
279     private long intPart() {
280         return value / SCALE[scaleOffset];
281     }
282
283     private long fracPart() {
284         return Math.abs(value % SCALE[scaleOffset]);
285     }
286
287     private static int toInt(final char ch, final int index) {
288         if (ch < '0' || ch > '9') {
289             throw new NumberFormatException("Illegal character at offset " + index);
290         }
291         return ch - '0';
292     }
293 }