2 * Copyright (c) 2015 Pantheon Technologies s.r.o. and others. All rights reserved.
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
8 package org.opendaylight.yangtools.yang.common;
10 import static com.google.common.base.Preconditions.checkArgument;
11 import static com.google.common.base.Verify.verify;
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;
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.
26 * @author Robert Varga
30 public class Decimal64 extends Number implements CanonicalValue<Decimal64> {
31 public static final class Support extends AbstractCanonicalValueSupport<Decimal64> {
33 super(Decimal64.class);
37 public Either<Decimal64, CanonicalValueViolation> fromString(final String str) {
38 // https://tools.ietf.org/html/rfc6020#section-9.3.1
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.
45 return CanonicalValueViolation.variantOf("Empty string is not a valid decimal64 representation");
48 // Deal with optional sign
49 final boolean negative;
51 switch (str.charAt(0)) {
65 // Sanity check length
66 if (idx == str.length()) {
67 return CanonicalValueViolation.variantOf("Missing digits after sign");
70 // Character limit, used for caching and cutting trailing zeroes
71 int limit = str.length() - 1;
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') {
81 // Integer part and its length
85 for (; idx <= limit; idx++, intLen++) {
86 final char ch = str.charAt(idx);
91 if (intLen == MAX_SCALE) {
92 return CanonicalValueViolation.variantOf(
93 "Integer part is longer than " + MAX_SCALE + " digits");
96 intPart = 10 * intPart + toInt(ch, idx);
100 // No fraction digits, we are done
101 return Either.ofFirst(new Decimal64((byte)1, intPart, 0, negative));
104 // Bump index to skip over period and check the remainder
107 return CanonicalValueViolation.variantOf("Value '" + str + "' is missing fraction digits");
110 // Trim trailing zeroes, if any
111 while (idx < limit && str.charAt(limit) == '0') {
115 final int fracLimit = MAX_SCALE - intLen + 1;
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");
124 fracPart = 10 * fracPart + toInt(ch, idx);
127 return Either.ofFirst(new Decimal64(fracLen, intPart, fracPart, negative));
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);
138 private static final CanonicalValueSupport<Decimal64> SUPPORT = new Support();
139 private static final long serialVersionUID = 1L;
141 private static final int MAX_SCALE = 18;
143 private static final long[] SCALE = {
164 private static final Decimal64[] MIN_VALUE;
165 private static final Decimal64[] MAX_VALUE;
168 verify(SCALE.length == MAX_SCALE);
170 MIN_VALUE = new Decimal64[MAX_SCALE];
171 MAX_VALUE = new Decimal64[MAX_SCALE];
172 for (byte i = 0; i < MAX_SCALE; ++i) {
173 MIN_VALUE[i] = new Decimal64(i, -9223372036854775808L);
174 MAX_VALUE[i] = new Decimal64(i, 9223372036854775807L);
178 private final byte scaleOffset;
179 private final long value;
182 Decimal64(final int scale, final long intPart, final long fracPart, final boolean negative) {
183 scaleOffset = offsetOf(scale);
185 final long bits = intPart * SCALE[scaleOffset] + fracPart;
186 value = negative ? -bits : bits;
189 private Decimal64(final byte scaleOffset, final long value) {
190 this.scaleOffset = scaleOffset;
194 protected Decimal64(final Decimal64 other) {
195 this(other.scaleOffset, other.value);
199 * Return a {@link Decimal64} with specified scale and unscaled value.
201 * @param scale scale to use
202 * @param unscaledValue unscaled value to use
203 * @return A Decimal64 instance
204 * @throws IllegalArgumentException if {@code scale} is not in range {@code [1..18]}
206 public static Decimal64 of(final int scale, final long unscaledValue) {
207 return new Decimal64(offsetOf(scale), unscaledValue);
211 * Return the minimum value supported in specified scale.
213 * @param scale scale to use
214 * @return Minimum value in that scale
215 * @throws IllegalArgumentException if {@code scale} is not in range {@code [1..18]}
217 public static Decimal64 minValueIn(final int scale) {
218 return MIN_VALUE[offsetOf(scale)];
222 * Return the maximum value supported in specified scale.
224 * @param scale scale to use
225 * @return Maximum value in that scale
226 * @throws IllegalArgumentException if {@code scale} is not in range {@code [1..18]}
228 public static Decimal64 maxValueIn(final int scale) {
229 return MAX_VALUE[offsetOf(scale)];
232 // >>> FIXME: these need to take a scale value and perform a range check. we also need truncating counterparts
233 public static Decimal64 valueOf(final byte byteVal) {
234 return byteVal < 0 ? new Decimal64(1, -byteVal, 0, true) : new Decimal64(1, byteVal, 0, false);
237 public static Decimal64 valueOf(final short shortVal) {
238 return shortVal < 0 ? new Decimal64(1, -shortVal, 0, true) : new Decimal64(1, shortVal, 0, false);
241 public static Decimal64 valueOf(final int intVal) {
242 return intVal < 0 ? new Decimal64(1, - (long)intVal, 0, true) : new Decimal64(1, intVal, 0, false);
245 public static Decimal64 valueOf(final long longVal) {
246 // XXX: we should be able to do something smarter here
247 return valueOf(Long.toString(longVal));
251 // FIXME: this should take a RoundingMode and perform rounding
252 // FIXME: this should have a float counterpart
253 // FIXME: this should have a truncating
254 public static Decimal64 valueOf(final double doubleVal) {
255 // XXX: we should be able to do something smarter here
256 return valueOf(Double.toString(doubleVal));
259 public static Decimal64 valueOf(final BigDecimal decimalVal) {
260 // FIXME: we should be able to do something smarter here using BigDecimal.unscaledValue() and BigDecimal.scale()
261 return valueOf(decimalVal.toPlainString());
265 * Attempt to parse a String into a Decimal64. This method uses minimum fraction digits required to hold
268 * @param str String to parser
269 * @return A Decimal64 instance
270 * @throws NullPointerException if value is null.
271 * @throws NumberFormatException if the string does not contain a parsable decimal64.
273 public static Decimal64 valueOf(final String str) {
274 final Either<Decimal64, CanonicalValueViolation> variant = SUPPORT.fromString(str);
275 final Optional<Decimal64> value = variant.tryFirst();
276 if (value.isPresent()) {
279 final Optional<String> message = variant.getSecond().getMessage();
280 throw message.isPresent() ? new NumberFormatException(message.get()) : new NumberFormatException();
284 * Return the scale of this decimal. This is the number of fraction digits, in range {@code [1..18]}.
286 * @return This decimal's scale
288 public final int scale() {
289 return scaleOffset + 1;
293 * Return the unscaled value of this decimal.
295 * @return This decimal's unscaled value
297 public final long unscaledValue() {
301 public final BigDecimal decimalValue() {
302 return BigDecimal.valueOf(value, scale());
306 public final int intValue() {
307 return (int) intPart();
311 public final long longValue() {
316 public final float floatValue() {
317 return (float) doubleValue();
321 public final double doubleValue() {
322 return 1.0 * value / SCALE[scaleOffset];
326 * Converts this {@code BigDecimal} to a {@code byte}, checking for lost information. If this {@code Decimal64} has
327 * a nonzero fractional part or is out of the possible range for a {@code byte} result then
328 * an {@code ArithmeticException} is thrown.
330 * @return this {@code Decimal64} converted to a {@code byte}.
331 * @throws ArithmeticException if {@code this} has a nonzero fractional part, or will not fit in a {@code byte}.
333 public final byte byteValueExact() {
334 final long val = longValueExact();
335 final byte ret = (byte) val;
337 throw new ArithmeticException("Value " + val + " is outside of byte range");
343 * Converts this {@code BigDecimal} to a {@code short}, checking for lost information. If this {@code Decimal64} has
344 * a nonzero fractional part or is out of the possible range for a {@code short} result then
345 * an {@code ArithmeticException} is thrown.
347 * @return this {@code Decimal64} converted to a {@code short}.
348 * @throws ArithmeticException if {@code this} has a nonzero fractional part, or will not fit in a {@code short}.
350 public final short shortValueExact() {
351 final long val = longValueExact();
352 final short ret = (short) val;
354 throw new ArithmeticException("Value " + val + " is outside of short range");
360 * Converts this {@code BigDecimal} to an {@code int}, checking for lost information. If this {@code Decimal64} has
361 * a nonzero fractional part or is out of the possible range for an {@code int} result then
362 * an {@code ArithmeticException} is thrown.
364 * @return this {@code Decimal64} converted to an {@code int}.
365 * @throws ArithmeticException if {@code this} has a nonzero fractional part, or will not fit in an {@code int}.
367 public final int intValueExact() {
368 final long val = longValueExact();
369 final int ret = (int) val;
371 throw new ArithmeticException("Value " + val + " is outside of integer range");
377 * Converts this {@code BigDecimal} to a {@code long}, checking for lost information. If this {@code Decimal64} has
378 * a nonzero fractional part then an {@code ArithmeticException} is thrown.
380 * @return this {@code Decimal64} converted to a {@code long}.
381 * @throws ArithmeticException if {@code this} has a nonzero fractional part.
383 public final long longValueExact() {
384 if (fracPart() != 0) {
385 throw new ArithmeticException("Conversion of " + this + " would lose fraction");
391 @SuppressWarnings("checkstyle:parameterName")
392 public final int compareTo(final Decimal64 o) {
396 if (scaleOffset == o.scaleOffset) {
397 return Long.compare(value, o.value);
400 // XXX: we could do something smarter here
401 return Double.compare(doubleValue(), o.doubleValue());
405 public final String toCanonicalString() {
406 // https://tools.ietf.org/html/rfc6020#section-9.3.2
408 // The canonical form of a positive decimal64 does not include the sign
409 // "+". The decimal point is required. Leading and trailing zeros are
410 // prohibited, subject to the rule that there MUST be at least one digit
411 // before and after the decimal point. The value zero is represented as
413 final StringBuilder sb = new StringBuilder(21).append(intPart()).append('.');
414 final long fracPart = fracPart();
416 // We may need to zero-pad the fraction part
417 sb.append(Strings.padStart(Long.toString(fracPart), scale(), '0'));
422 return sb.toString();
426 public final CanonicalValueSupport<Decimal64> support() {
431 public final int hashCode() {
432 // We need to normalize the results in order to be consistent with equals()
433 return Long.hashCode(intPart()) * 31 + Long.hashCode(fracPart());
437 public final boolean equals(final @Nullable Object obj) {
438 return this == obj || obj instanceof Decimal64 && equalsImpl((Decimal64) obj);
442 * A slightly faster version of {@link #equals(Object)}.
444 * @param obj Decimal64 object
445 * @return {@code true} if this object is the same as the obj argument; {@code false} otherwise.
447 public final boolean equals(final @Nullable Decimal64 obj) {
448 return this == obj || obj != null && equalsImpl(obj);
452 public final String toString() {
453 return toCanonicalString();
456 private boolean equalsImpl(final Decimal64 other) {
457 return scaleOffset == other.scaleOffset ? value == other.value
458 // We need to normalize both
459 : intPart() == other.intPart() && fracPart() == other.fracPart();
462 private long intPart() {
463 return value / SCALE[scaleOffset];
466 private long fracPart() {
467 return Math.abs(value % SCALE[scaleOffset]);
470 private static byte offsetOf(final int scale) {
471 checkArgument(scale >= 1 && scale <= MAX_SCALE);
472 return (byte) (scale - 1);