Move Decimal64.toInt() method
[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         private static int toInt(final char ch, final int index) {
133             if (ch < '0' || ch > '9') {
134                 throw new NumberFormatException("Illegal character at offset " + index);
135             }
136             return ch - '0';
137         }
138     }
139
140     private static final CanonicalValueSupport<Decimal64> SUPPORT = new Support();
141     private static final long serialVersionUID = 1L;
142
143     private static final int MAX_FRACTION_DIGITS = 18;
144
145     private static final long[] SCALE = {
146         10,
147         100,
148         1000,
149         10000,
150         100000,
151         1000000,
152         10000000,
153         100000000,
154         1000000000,
155         10000000000L,
156         100000000000L,
157         1000000000000L,
158         10000000000000L,
159         100000000000000L,
160         1000000000000000L,
161         10000000000000000L,
162         100000000000000000L,
163         1000000000000000000L
164     };
165
166     static {
167         verify(SCALE.length == MAX_FRACTION_DIGITS);
168     }
169
170     private final byte scaleOffset;
171     private final long value;
172
173     @VisibleForTesting
174     Decimal64(final int fractionDigits, final long intPart, final long fracPart, final boolean negative) {
175         checkArgument(fractionDigits >= 1 && fractionDigits <= MAX_FRACTION_DIGITS);
176         this.scaleOffset = (byte) (fractionDigits - 1);
177
178         final long bits = intPart * SCALE[this.scaleOffset] + fracPart;
179         this.value = negative ? -bits : bits;
180     }
181
182     protected Decimal64(final Decimal64 other) {
183         this.scaleOffset = other.scaleOffset;
184         this.value = other.value;
185     }
186
187     public static Decimal64 valueOf(final byte byteVal) {
188         return byteVal < 0 ? new Decimal64(1, -byteVal, 0, true) : new Decimal64(1, byteVal, 0, false);
189     }
190
191     public static Decimal64 valueOf(final short shortVal) {
192         return shortVal < 0 ? new Decimal64(1, -shortVal, 0, true) : new Decimal64(1, shortVal, 0, false);
193     }
194
195     public static Decimal64 valueOf(final int intVal) {
196         return intVal < 0 ? new Decimal64(1, - (long)intVal, 0, true) : new Decimal64(1, intVal, 0, false);
197     }
198
199     public static Decimal64 valueOf(final long longVal) {
200         // XXX: we should be able to do something smarter here
201         return valueOf(Long.toString(longVal));
202     }
203
204     public static Decimal64 valueOf(final double doubleVal) {
205         // XXX: we should be able to do something smarter here
206         return valueOf(Double.toString(doubleVal));
207     }
208
209     public static Decimal64 valueOf(final BigDecimal decimalVal) {
210         // XXX: we should be able to do something smarter here
211         return valueOf(decimalVal.toPlainString());
212     }
213
214     /**
215      * Attempt to parse a String into a Decimal64. This method uses minimum fraction digits required to hold
216      * the entire value.
217      *
218      * @param str String to parser
219      * @return A Decimal64 instance
220      * @throws NullPointerException if value is null.
221      * @throws NumberFormatException if the string does not contain a parsable decimal64.
222      */
223     public static Decimal64 valueOf(final String str) {
224         final Variant<Decimal64, CanonicalValueViolation> variant = SUPPORT.fromString(str);
225         final Optional<Decimal64> value = variant.tryFirst();
226         if (value.isPresent()) {
227             return value.get();
228         }
229         final Optional<String> message = variant.getSecond().getMessage();
230         throw message.isPresent() ? new NumberFormatException(message.get()) : new NumberFormatException();
231     }
232
233     public final BigDecimal decimalValue() {
234         return BigDecimal.valueOf(value, scaleOffset + 1);
235     }
236
237     @Override
238     public final int intValue() {
239         return (int) intPart();
240     }
241
242     @Override
243     public final long longValue() {
244         return intPart();
245     }
246
247     @Override
248     public final float floatValue() {
249         return (float) doubleValue();
250     }
251
252     @Override
253     public final double doubleValue() {
254         return 1.0 * value / SCALE[scaleOffset];
255     }
256
257     /**
258      * Converts this {@code BigDecimal} to a {@code byte}, checking for lost information. If this {@code Decimal64} has
259      * a nonzero fractional part or is out of the possible range for a {@code byte} result then
260      * an {@code ArithmeticException} is thrown.
261      *
262      * @return this {@code Decimal64} converted to a {@code byte}.
263      * @throws ArithmeticException if {@code this} has a nonzero fractional part, or will not fit in a {@code byte}.
264      */
265     public final byte byteValueExact() {
266         final long val = longValueExact();
267         final byte ret = (byte) val;
268         if (val != ret) {
269             throw new ArithmeticException("Value " + val + " is outside of byte range");
270         }
271         return ret;
272     }
273
274     /**
275      * Converts this {@code BigDecimal} to a {@code short}, checking for lost information. If this {@code Decimal64} has
276      * a nonzero fractional part or is out of the possible range for a {@code short} result then
277      * an {@code ArithmeticException} is thrown.
278      *
279      * @return this {@code Decimal64} converted to a {@code short}.
280      * @throws ArithmeticException if {@code this} has a nonzero fractional part, or will not fit in a {@code short}.
281      */
282     public final short shortValueExact() {
283         final long val = longValueExact();
284         final short ret = (short) val;
285         if (val != ret) {
286             throw new ArithmeticException("Value " + val + " is outside of short range");
287         }
288         return ret;
289     }
290
291     /**
292      * Converts this {@code BigDecimal} to an {@code int}, checking for lost information. If this {@code Decimal64} has
293      * a nonzero fractional part or is out of the possible range for an {@code int} result then
294      * an {@code ArithmeticException} is thrown.
295      *
296      * @return this {@code Decimal64} converted to an {@code int}.
297      * @throws ArithmeticException if {@code this} has a nonzero fractional part, or will not fit in an {@code int}.
298      */
299     public final int intValueExact() {
300         final long val = longValueExact();
301         final int ret = (int) val;
302         if (val != ret) {
303             throw new ArithmeticException("Value " + val + " is outside of integer range");
304         }
305         return ret;
306     }
307
308     /**
309      * Converts this {@code BigDecimal} to a {@code long}, checking for lost information.  If this {@code Decimal64} has
310      * a nonzero fractional part then an {@code ArithmeticException} is thrown.
311      *
312      * @return this {@code Decimal64} converted to a {@code long}.
313      * @throws ArithmeticException if {@code this} has a nonzero fractional part.
314      */
315     public final long longValueExact() {
316         if (fracPart() != 0) {
317             throw new ArithmeticException("Conversion of " + this + " would lose fraction");
318         }
319         return intPart();
320     }
321
322     @Override
323     @SuppressWarnings("checkstyle:parameterName")
324     public final int compareTo(final Decimal64 o) {
325         if (this == o) {
326             return 0;
327         }
328         if (scaleOffset == o.scaleOffset) {
329             return Long.compare(value, o.value);
330         }
331
332         // XXX: we could do something smarter here
333         return Double.compare(doubleValue(), o.doubleValue());
334     }
335
336     @Override
337     public final String toCanonicalString() {
338         // https://tools.ietf.org/html/rfc6020#section-9.3.2
339         //
340         // The canonical form of a positive decimal64 does not include the sign
341         // "+".  The decimal point is required.  Leading and trailing zeros are
342         // prohibited, subject to the rule that there MUST be at least one digit
343         // before and after the decimal point.  The value zero is represented as
344         // "0.0".
345         final StringBuilder sb = new StringBuilder(21).append(intPart()).append('.');
346         final long fracPart = fracPart();
347         if (fracPart != 0) {
348             // We may need to zero-pad the fraction part
349             sb.append(Strings.padStart(Long.toString(fracPart), scaleOffset + 1, '0'));
350         } else {
351             sb.append('0');
352         }
353
354         return sb.toString();
355     }
356
357     @Override
358     public final CanonicalValueSupport<Decimal64> support() {
359         return SUPPORT;
360     }
361
362     @Override
363     public final int hashCode() {
364         // We need to normalize the results in order to be consistent with equals()
365         return Long.hashCode(intPart()) * 31 + Long.hashCode(fracPart());
366     }
367
368     @Override
369     public final boolean equals(final @Nullable Object obj) {
370         if (this == obj) {
371             return true;
372         }
373         if (!(obj instanceof Decimal64)) {
374             return false;
375         }
376         final Decimal64 other = (Decimal64) obj;
377         if (scaleOffset == other.scaleOffset) {
378             return value == other.value;
379         }
380
381         // We need to normalize both
382         return intPart() == other.intPart() && fracPart() == fracPart();
383     }
384
385     @Override
386     public final String toString() {
387         return toCanonicalString();
388     }
389
390     private long intPart() {
391         return value / SCALE[scaleOffset];
392     }
393
394     private long fracPart() {
395         return Math.abs(value % SCALE[scaleOffset]);
396     }
397 }