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.math.RoundingMode;
18 import java.util.Optional;
19 import org.eclipse.jdt.annotation.NonNullByDefault;
20 import org.eclipse.jdt.annotation.Nullable;
21 import org.opendaylight.yangtools.concepts.Either;
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.
27 * @author Robert Varga
31 public class Decimal64 extends Number implements CanonicalValue<Decimal64> {
32 public static final class Support extends AbstractCanonicalValueSupport<Decimal64> {
34 super(Decimal64.class);
38 public Either<Decimal64, CanonicalValueViolation> fromString(final String str) {
39 // https://tools.ietf.org/html/rfc6020#section-9.3.1
41 // A decimal64 value is lexically represented as an optional sign ("+"
42 // or "-"), followed by a sequence of decimal digits, optionally
43 // followed by a period ('.') as a decimal indicator and a sequence of
44 // decimal digits. If no sign is specified, "+" is assumed.
46 return CanonicalValueViolation.variantOf("Empty string is not a valid decimal64 representation");
49 // Deal with optional sign
50 final boolean negative;
52 switch (str.charAt(0)) {
66 // Sanity check length
67 if (idx == str.length()) {
68 return CanonicalValueViolation.variantOf("Missing digits after sign");
71 // Character limit, used for caching and cutting trailing zeroes
72 int limit = str.length() - 1;
74 // Skip any leading zeroes, but leave at least one
75 for (; idx < limit && str.charAt(idx) == '0'; idx++) {
76 final char ch = str.charAt(idx + 1);
77 if (ch < '0' || ch > '9') {
82 // Integer part and its length
86 for (; idx <= limit; idx++, intLen++) {
87 final char ch = str.charAt(idx);
92 if (intLen == MAX_SCALE) {
93 return CanonicalValueViolation.variantOf(
94 "Integer part is longer than " + MAX_SCALE + " digits");
97 intPart = 10 * intPart + toInt(ch, idx);
101 // No fraction digits, we are done
102 return Either.ofFirst(new Decimal64((byte)1, intPart, 0, negative));
105 // Bump index to skip over period and check the remainder
108 return CanonicalValueViolation.variantOf("Value '" + str + "' is missing fraction digits");
111 // Trim trailing zeroes, if any
112 while (idx < limit && str.charAt(limit) == '0') {
116 final int fracLimit = MAX_SCALE - intLen + 1;
119 for (; idx <= limit; idx++, fracLen++) {
120 final char ch = str.charAt(idx);
121 if (fracLen == fracLimit) {
122 return CanonicalValueViolation.variantOf("Fraction part longer than " + fracLimit + " digits");
125 fracPart = 10 * fracPart + toInt(ch, idx);
128 return Either.ofFirst(new Decimal64(fracLen, intPart, fracPart, negative));
131 private static int toInt(final char ch, final int index) {
132 if (ch < '0' || ch > '9') {
133 throw new NumberFormatException("Illegal character at offset " + index);
139 private static final CanonicalValueSupport<Decimal64> SUPPORT = new Support();
140 private static final long serialVersionUID = 1L;
142 private static final int MAX_SCALE = 18;
144 private static final long[] FACTOR = {
165 private static final Decimal64Conversion[] CONVERSION = Decimal64Conversion.values();
166 private static final Decimal64[] MIN_VALUE;
167 private static final Decimal64[] MAX_VALUE;
170 verify(CONVERSION.length == MAX_SCALE);
171 verify(FACTOR.length == MAX_SCALE);
173 MIN_VALUE = new Decimal64[MAX_SCALE];
174 MAX_VALUE = new Decimal64[MAX_SCALE];
175 for (byte i = 0; i < MAX_SCALE; ++i) {
176 MIN_VALUE[i] = new Decimal64(i, -9223372036854775808L);
177 MAX_VALUE[i] = new Decimal64(i, 9223372036854775807L);
181 private final byte offset;
182 private final long value;
185 Decimal64(final int scale, final long intPart, final long fracPart, final boolean negative) {
186 offset = offsetOf(scale);
188 final long bits = intPart * FACTOR[offset] + fracPart;
189 value = negative ? -bits : bits;
192 private Decimal64(final byte offset, final long intPart, final boolean negative) {
193 this.offset = offset;
194 final long bits = intPart * FACTOR[offset];
195 value = negative ? -bits : bits;
198 private Decimal64(final byte offset, final long value) {
199 this.offset = offset;
203 protected Decimal64(final Decimal64 other) {
204 this(other.offset, other.value);
208 * Return a {@link Decimal64} with specified scale and unscaled value.
210 * @param scale scale to use
211 * @param unscaledValue unscaled value to use
212 * @return A Decimal64 instance
213 * @throws IllegalArgumentException if {@code scale} is not in range {@code [1..18]}
215 public static Decimal64 of(final int scale, final long unscaledValue) {
216 return new Decimal64(offsetOf(scale), unscaledValue);
220 * Return the minimum value supported in specified scale.
222 * @param scale scale to use
223 * @return Minimum value in that scale
224 * @throws IllegalArgumentException if {@code scale} is not in range {@code [1..18]}
226 public static Decimal64 minValueIn(final int scale) {
227 return MIN_VALUE[offsetOf(scale)];
231 * Return the maximum value supported in specified scale.
233 * @param scale scale to use
234 * @return Maximum value in that scale
235 * @throws IllegalArgumentException if {@code scale} is not in range {@code [1..18]}
237 public static Decimal64 maxValueIn(final int scale) {
238 return MAX_VALUE[offsetOf(scale)];
241 // >>> FIXME: these need truncating counterparts
242 public static Decimal64 valueOf(final int scale, final byte byteVal) {
243 final byte offset = offsetOf(scale);
244 final var conv = CONVERSION[offset];
245 if (byteVal < conv.minByte || byteVal > conv.maxByte) {
246 throw new IllegalArgumentException("Value " + byteVal + " is not in range ["
247 + conv.minByte + ".." + conv.maxByte + "] to fit scale " + scale);
249 return byteVal < 0 ? new Decimal64(offset, -byteVal, true) : new Decimal64(offset, byteVal, false);
252 public static Decimal64 valueOf(final int scale, final short shortVal) {
253 final byte offset = offsetOf(scale);
254 final var conv = CONVERSION[offset];
255 if (shortVal < conv.minShort || shortVal > conv.maxShort) {
256 throw new IllegalArgumentException("Value " + shortVal + " is not in range ["
257 + conv.minShort + ".." + conv.maxShort + "] to fit scale " + scale);
259 return shortVal < 0 ? new Decimal64(offset, -shortVal, true) : new Decimal64(offset, shortVal, false);
262 public static Decimal64 valueOf(final int scale, final int intVal) {
263 final byte offset = offsetOf(scale);
264 final var conv = CONVERSION[offset];
265 if (intVal < conv.minInt || intVal > conv.maxInt) {
266 throw new IllegalArgumentException("Value " + intVal + " is not in range ["
267 + conv.minInt + ".." + conv.maxInt + "] to fit scale " + scale);
269 return intVal < 0 ? new Decimal64(offset, - (long)intVal, true) : new Decimal64(offset, intVal, false);
272 public static Decimal64 valueOf(final int scale, final long longVal) {
273 final byte offset = offsetOf(scale);
274 final var conv = CONVERSION[offset];
275 if (longVal < conv.minLong || longVal > conv.maxLong) {
276 throw new IllegalArgumentException("Value " + longVal + " is not in range ["
277 + conv.minLong + ".." + conv.maxLong + "] to fit scale " + scale);
279 return longVal < 0 ? new Decimal64(offset, -longVal, true) : new Decimal64(offset, longVal, false);
283 // FIXME: this should take a RoundingMode and perform rounding
284 // FIXME: this should have a truncating counterpart
285 public static Decimal64 valueOf(final float floatVal, final RoundingMode rounding) {
286 // XXX: we should be able to do something smarter here
287 return valueOf(Float.toString(floatVal));
290 // FIXME: this should take a RoundingMode and perform rounding
291 // FIXME: this should have a truncating counterpart
292 public static Decimal64 valueOf(final double doubleVal, final RoundingMode rounding) {
293 // XXX: we should be able to do something smarter here
294 return valueOf(Double.toString(doubleVal));
297 public static Decimal64 valueOf(final BigDecimal decimalVal) {
298 // FIXME: we should be able to do something smarter here using BigDecimal.unscaledValue() and BigDecimal.scale()
299 return valueOf(decimalVal.toPlainString());
303 * Attempt to parse a String into a Decimal64. This method uses minimum fraction digits required to hold
306 * @param str String to parser
307 * @return A Decimal64 instance
308 * @throws NullPointerException if value is null.
309 * @throws NumberFormatException if the string does not contain a parsable decimal64.
311 public static Decimal64 valueOf(final String str) {
312 final Either<Decimal64, CanonicalValueViolation> variant = SUPPORT.fromString(str);
313 final Optional<Decimal64> value = variant.tryFirst();
314 if (value.isPresent()) {
317 final Optional<String> message = variant.getSecond().getMessage();
318 throw message.isPresent() ? new NumberFormatException(message.get()) : new NumberFormatException();
322 * Return the scale of this decimal. This is the number of fraction digits, in range {@code [1..18]}.
324 * @return This decimal's scale
326 public final int scale() {
331 * Return the unscaled value of this decimal.
333 * @return This decimal's unscaled value
335 public final long unscaledValue() {
339 public final BigDecimal decimalValue() {
340 return BigDecimal.valueOf(value, scale());
344 public final int intValue() {
345 return (int) intPart();
349 public final long longValue() {
354 public final float floatValue() {
355 return (float) doubleValue();
359 public final double doubleValue() {
360 return 1.0 * value / FACTOR[offset];
364 * Converts this {@code BigDecimal} to a {@code byte}, checking for lost information. If this {@code Decimal64} has
365 * a nonzero fractional part or is out of the possible range for a {@code byte} result then
366 * an {@code ArithmeticException} is thrown.
368 * @return this {@code Decimal64} converted to a {@code byte}.
369 * @throws ArithmeticException if {@code this} has a nonzero fractional part, or will not fit in a {@code byte}.
371 public final byte byteValueExact() {
372 final long val = longValueExact();
373 final byte ret = (byte) val;
375 throw new ArithmeticException("Value " + val + " is outside of byte range");
381 * Converts this {@code BigDecimal} to a {@code short}, checking for lost information. If this {@code Decimal64} has
382 * a nonzero fractional part or is out of the possible range for a {@code short} result then
383 * an {@code ArithmeticException} is thrown.
385 * @return this {@code Decimal64} converted to a {@code short}.
386 * @throws ArithmeticException if {@code this} has a nonzero fractional part, or will not fit in a {@code short}.
388 public final short shortValueExact() {
389 final long val = longValueExact();
390 final short ret = (short) val;
392 throw new ArithmeticException("Value " + val + " is outside of short range");
398 * Converts this {@code BigDecimal} to an {@code int}, checking for lost information. If this {@code Decimal64} has
399 * a nonzero fractional part or is out of the possible range for an {@code int} result then
400 * an {@code ArithmeticException} is thrown.
402 * @return this {@code Decimal64} converted to an {@code int}.
403 * @throws ArithmeticException if {@code this} has a nonzero fractional part, or will not fit in an {@code int}.
405 public final int intValueExact() {
406 final long val = longValueExact();
407 final int ret = (int) val;
409 throw new ArithmeticException("Value " + val + " is outside of integer range");
415 * Converts this {@code BigDecimal} to a {@code long}, checking for lost information. If this {@code Decimal64} has
416 * a nonzero fractional part then an {@code ArithmeticException} is thrown.
418 * @return this {@code Decimal64} converted to a {@code long}.
419 * @throws ArithmeticException if {@code this} has a nonzero fractional part.
421 public final long longValueExact() {
422 if (fracPart() != 0) {
423 throw new ArithmeticException("Conversion of " + this + " would lose fraction");
429 @SuppressWarnings("checkstyle:parameterName")
430 public final int compareTo(final Decimal64 o) {
434 if (offset == o.offset) {
435 return Long.compare(value, o.value);
438 // XXX: we could do something smarter here
439 return Double.compare(doubleValue(), o.doubleValue());
443 public final String toCanonicalString() {
444 // https://tools.ietf.org/html/rfc6020#section-9.3.2
446 // The canonical form of a positive decimal64 does not include the sign
447 // "+". The decimal point is required. Leading and trailing zeros are
448 // prohibited, subject to the rule that there MUST be at least one digit
449 // before and after the decimal point. The value zero is represented as
451 final StringBuilder sb = new StringBuilder(21).append(intPart()).append('.');
452 final long fracPart = fracPart();
454 // We may need to zero-pad the fraction part
455 sb.append(Strings.padStart(Long.toString(fracPart), scale(), '0'));
460 return sb.toString();
464 public final CanonicalValueSupport<Decimal64> support() {
469 public final int hashCode() {
470 // We need to normalize the results in order to be consistent with equals()
471 return Long.hashCode(intPart()) * 31 + Long.hashCode(fracPart());
475 public final boolean equals(final @Nullable Object obj) {
476 return this == obj || obj instanceof Decimal64 && equalsImpl((Decimal64) obj);
480 * A slightly faster version of {@link #equals(Object)}.
482 * @param obj Decimal64 object
483 * @return {@code true} if this object is the same as the obj argument; {@code false} otherwise.
485 public final boolean equals(final @Nullable Decimal64 obj) {
486 return this == obj || obj != null && equalsImpl(obj);
490 public final String toString() {
491 return toCanonicalString();
494 private boolean equalsImpl(final Decimal64 other) {
495 return offset == other.offset ? value == other.value
496 // We need to normalize both
497 : intPart() == other.intPart() && fracPart() == other.fracPart();
500 private long intPart() {
501 return value / FACTOR[offset];
504 private long fracPart() {
505 return Math.abs(value % FACTOR[offset]);
508 private static byte offsetOf(final int scale) {
509 checkArgument(scale >= 1 && scale <= MAX_SCALE);
510 return (byte) (scale - 1);