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.kohsuke.MetaInfServices;
21 import org.opendaylight.yangtools.concepts.Variant;
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 @MetaInfServices(value = CanonicalValueSupport.class)
33 public static final class Support extends AbstractCanonicalValueSupport<Decimal64> {
35 super(Decimal64.class);
39 public Variant<Decimal64, CanonicalValueViolation> fromString(final String str) {
40 // https://tools.ietf.org/html/rfc6020#section-9.3.1
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.
47 return CanonicalValueViolation.variantOf("Empty string is not a valid decimal64 representation");
50 // Deal with optional sign
51 final boolean negative;
53 switch (str.charAt(0)) {
67 // Sanity check length
68 if (idx == str.length()) {
69 return CanonicalValueViolation.variantOf("Missing digits after sign");
72 // Character limit, used for caching and cutting trailing zeroes
73 int limit = str.length() - 1;
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') {
83 // Integer part and its length
87 for (; idx <= limit; idx++, intLen++) {
88 final char ch = str.charAt(idx);
93 if (intLen == MAX_FRACTION_DIGITS) {
94 return CanonicalValueViolation.variantOf(
95 "Integer part is longer than " + MAX_FRACTION_DIGITS + " digits");
98 intPart = 10 * intPart + toInt(ch, idx);
102 // No fraction digits, we are done
103 return Variant.ofFirst(new Decimal64((byte)1, intPart, 0, negative));
106 // Bump index to skip over period and check the remainder
109 return CanonicalValueViolation.variantOf("Value '" + str + "' is missing fraction digits");
112 // Trim trailing zeroes, if any
113 while (idx < limit && str.charAt(limit) == '0') {
117 final int fracLimit = MAX_FRACTION_DIGITS - intLen;
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");
126 fracPart = 10 * fracPart + toInt(ch, idx);
129 return Variant.ofFirst(new Decimal64(fracLen, intPart, fracPart, negative));
133 private static final CanonicalValueSupport<Decimal64> SUPPORT = new Support();
134 private static final long serialVersionUID = 1L;
136 private static final int MAX_FRACTION_DIGITS = 18;
138 private static final long[] SCALE = {
160 verify(SCALE.length == MAX_FRACTION_DIGITS);
163 private final byte scaleOffset;
164 private final long value;
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);
171 final long bits = intPart * SCALE[this.scaleOffset] + fracPart;
172 this.value = negative ? -bits : bits;
175 protected Decimal64(final Decimal64 other) {
176 this.scaleOffset = other.scaleOffset;
177 this.value = other.value;
180 public static Decimal64 valueOf(final byte byteVal) {
181 return byteVal < 0 ? new Decimal64(1, -byteVal, 0, true) : new Decimal64(1, byteVal, 0, false);
184 public static Decimal64 valueOf(final short shortVal) {
185 return shortVal < 0 ? new Decimal64(1, -shortVal, 0, true) : new Decimal64(1, shortVal, 0, false);
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);
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));
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));
202 public static Decimal64 valueOf(final BigDecimal decimalVal) {
203 // XXX: we should be able to do something smarter here
204 return valueOf(decimalVal.toPlainString());
208 * Attempt to parse a String into a Decimal64. This method uses minimum fraction digits required to hold
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.
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()) {
222 final Optional<String> message = variant.getSecond().getMessage();
223 throw message.isPresent() ? new NumberFormatException(message.get()) : new NumberFormatException();
226 public final BigDecimal decimalValue() {
227 return BigDecimal.valueOf(value, scaleOffset + 1);
231 public final int intValue() {
232 return (int) intPart();
236 public final long longValue() {
241 public final float floatValue() {
242 return (float) doubleValue();
246 public final double doubleValue() {
247 return 1.0 * value / SCALE[scaleOffset];
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.
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}.
258 public final byte byteValueExact() {
259 final long val = longValueExact();
260 final byte ret = (byte) val;
262 throw new ArithmeticException("Value " + val + " is outside of byte range");
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.
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}.
275 public final short shortValueExact() {
276 final long val = longValueExact();
277 final short ret = (short) val;
279 throw new ArithmeticException("Value " + val + " is outside of short range");
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.
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}.
292 public final int intValueExact() {
293 final long val = longValueExact();
294 final int ret = (int) val;
296 throw new ArithmeticException("Value " + val + " is outside of integer range");
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.
305 * @return this {@code Decimal64} converted to a {@code long}.
306 * @throws ArithmeticException if {@code this} has a nonzero fractional part.
308 public final long longValueExact() {
309 if (fracPart() != 0) {
310 throw new ArithmeticException("Conversion of " + this + " would lose fraction");
316 @SuppressWarnings("checkstyle:parameterName")
317 public final int compareTo(final Decimal64 o) {
321 if (scaleOffset == o.scaleOffset) {
322 return Long.compare(value, o.value);
325 // XXX: we could do something smarter here
326 return Double.compare(doubleValue(), o.doubleValue());
330 public final String toCanonicalString() {
331 // https://tools.ietf.org/html/rfc6020#section-9.3.2
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
338 final StringBuilder sb = new StringBuilder(21).append(intPart()).append('.');
339 final long fracPart = fracPart();
341 // We may need to zero-pad the fraction part
342 sb.append(Strings.padStart(Long.toString(fracPart), scaleOffset + 1, '0'));
347 return sb.toString();
351 public final CanonicalValueSupport<Decimal64> support() {
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());
362 public final boolean equals(final @Nullable Object obj) {
366 if (!(obj instanceof Decimal64)) {
369 final Decimal64 other = (Decimal64) obj;
370 if (scaleOffset == other.scaleOffset) {
371 return value == other.value;
374 // We need to normalize both
375 return intPart() == other.intPart() && fracPart() == other.fracPart();
379 public final String toString() {
380 return toCanonicalString();
383 private long intPart() {
384 return value / SCALE[scaleOffset];
387 private long fracPart() {
388 return Math.abs(value % SCALE[scaleOffset]);
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);