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