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