/* * Copyright (c) 2015 Pantheon Technologies s.r.o. and others. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v1.0 which accompanies this distribution, * and is available at http://www.eclipse.org/legal/epl-v10.html */ package org.opendaylight.yangtools.yang.common; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Verify.verify; import com.google.common.annotations.Beta; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.Optional; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.opendaylight.yangtools.concepts.Either; /** * Dedicated type for YANG's 'type decimal64' type. This class is similar to {@link BigDecimal}, but provides more * efficient storage, as it has fixed precision. * * @author Robert Varga */ @Beta @NonNullByDefault public class Decimal64 extends Number implements CanonicalValue { public static final class Support extends AbstractCanonicalValueSupport { public Support() { super(Decimal64.class); } @Override public Either fromString(final String str) { // https://tools.ietf.org/html/rfc6020#section-9.3.1 // // A decimal64 value is lexically represented as an optional sign ("+" // or "-"), followed by a sequence of decimal digits, optionally // followed by a period ('.') as a decimal indicator and a sequence of // decimal digits. If no sign is specified, "+" is assumed. if (str.isEmpty()) { return CanonicalValueViolation.variantOf("Empty string is not a valid decimal64 representation"); } // Deal with optional sign final boolean negative; int idx; switch (str.charAt(0)) { case '-': negative = true; idx = 1; break; case '+': negative = false; idx = 1; break; default: negative = false; idx = 0; } // Sanity check length if (idx == str.length()) { return CanonicalValueViolation.variantOf("Missing digits after sign"); } // Character limit, used for caching and cutting trailing zeroes int limit = str.length() - 1; // Skip any leading zeroes, but leave at least one for (; idx < limit && str.charAt(idx) == '0'; idx++) { final char ch = str.charAt(idx + 1); if (ch < '0' || ch > '9') { break; } } // Integer part and its length int intLen = 0; long intPart = 0; for (; idx <= limit; idx++, intLen++) { final char ch = str.charAt(idx); if (ch == '.') { // Fractions are next break; } if (intLen == MAX_SCALE) { return CanonicalValueViolation.variantOf( "Integer part is longer than " + MAX_SCALE + " digits"); } intPart = 10 * intPart + toInt(ch, idx); } if (idx > limit) { // No fraction digits, we are done return Either.ofFirst(new Decimal64((byte)1, intPart, 0, negative)); } // Bump index to skip over period and check the remainder idx++; if (idx > limit) { return CanonicalValueViolation.variantOf("Value '" + str + "' is missing fraction digits"); } // Trim trailing zeroes, if any while (idx < limit && str.charAt(limit) == '0') { limit--; } final int fracLimit = MAX_SCALE - intLen + 1; byte fracLen = 0; long fracPart = 0; for (; idx <= limit; idx++, fracLen++) { final char ch = str.charAt(idx); if (fracLen == fracLimit) { return CanonicalValueViolation.variantOf("Fraction part longer than " + fracLimit + " digits"); } fracPart = 10 * fracPart + toInt(ch, idx); } return Either.ofFirst(new Decimal64(fracLen, intPart, fracPart, negative)); } private static int toInt(final char ch, final int index) { if (ch < '0' || ch > '9') { throw new NumberFormatException("Illegal character at offset " + index); } return ch - '0'; } } private static final CanonicalValueSupport SUPPORT = new Support(); private static final long serialVersionUID = 1L; private static final int MAX_SCALE = 18; private static final long[] FACTOR = { 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000, 10000000000L, 100000000000L, 1000000000000L, 10000000000000L, 100000000000000L, 1000000000000000L, 10000000000000000L, 100000000000000000L, 1000000000000000000L }; private static final Decimal64Conversion[] CONVERSION = Decimal64Conversion.values(); private static final Decimal64[] MIN_VALUE; private static final Decimal64[] MAX_VALUE; static { verify(CONVERSION.length == MAX_SCALE); verify(FACTOR.length == MAX_SCALE); MIN_VALUE = new Decimal64[MAX_SCALE]; MAX_VALUE = new Decimal64[MAX_SCALE]; for (byte i = 0; i < MAX_SCALE; ++i) { MIN_VALUE[i] = new Decimal64(i, Long.MIN_VALUE); MAX_VALUE[i] = new Decimal64(i, Long.MAX_VALUE); } } private final byte offset; private final long value; @VisibleForTesting Decimal64(final int scale, final long intPart, final long fracPart, final boolean negative) { offset = offsetOf(scale); final long bits = intPart * FACTOR[offset] + fracPart; value = negative ? -bits : bits; } private Decimal64(final byte offset, final long intPart, final boolean negative) { this.offset = offset; final long bits = intPart * FACTOR[offset]; value = negative ? -bits : bits; } private Decimal64(final byte offset, final long value) { this.offset = offset; this.value = value; } protected Decimal64(final Decimal64 other) { this(other.offset, other.value); } /** * Return a {@link Decimal64} with specified scale and unscaled value. * * @param scale scale to use * @param unscaledValue unscaled value to use * @return A Decimal64 instance * @throws IllegalArgumentException if {@code scale} is not in range {@code [1..18]} */ public static Decimal64 of(final int scale, final long unscaledValue) { return new Decimal64(offsetOf(scale), unscaledValue); } /** * Return the minimum value supported in specified scale. * * @param scale scale to use * @return Minimum value in that scale * @throws IllegalArgumentException if {@code scale} is not in range {@code [1..18]} */ public static Decimal64 minValueIn(final int scale) { return MIN_VALUE[offsetOf(scale)]; } /** * Return the maximum value supported in specified scale. * * @param scale scale to use * @return Maximum value in that scale * @throws IllegalArgumentException if {@code scale} is not in range {@code [1..18]} */ public static Decimal64 maxValueIn(final int scale) { return MAX_VALUE[offsetOf(scale)]; } // >>> FIXME: these need truncating counterparts public static Decimal64 valueOf(final int scale, final byte byteVal) { final byte offset = offsetOf(scale); final var conv = CONVERSION[offset]; if (byteVal < conv.minByte || byteVal > conv.maxByte) { throw new IllegalArgumentException("Value " + byteVal + " is not in range [" + conv.minByte + ".." + conv.maxByte + "] to fit scale " + scale); } return byteVal < 0 ? new Decimal64(offset, -byteVal, true) : new Decimal64(offset, byteVal, false); } public static Decimal64 valueOf(final int scale, final short shortVal) { final byte offset = offsetOf(scale); final var conv = CONVERSION[offset]; if (shortVal < conv.minShort || shortVal > conv.maxShort) { throw new IllegalArgumentException("Value " + shortVal + " is not in range [" + conv.minShort + ".." + conv.maxShort + "] to fit scale " + scale); } return shortVal < 0 ? new Decimal64(offset, -shortVal, true) : new Decimal64(offset, shortVal, false); } public static Decimal64 valueOf(final int scale, final int intVal) { final byte offset = offsetOf(scale); final var conv = CONVERSION[offset]; if (intVal < conv.minInt || intVal > conv.maxInt) { throw new IllegalArgumentException("Value " + intVal + " is not in range [" + conv.minInt + ".." + conv.maxInt + "] to fit scale " + scale); } return intVal < 0 ? new Decimal64(offset, - (long)intVal, true) : new Decimal64(offset, intVal, false); } public static Decimal64 valueOf(final int scale, final long longVal) { final byte offset = offsetOf(scale); final var conv = CONVERSION[offset]; if (longVal < conv.minLong || longVal > conv.maxLong) { throw new IllegalArgumentException("Value " + longVal + " is not in range [" + conv.minLong + ".." + conv.maxLong + "] to fit scale " + scale); } return longVal < 0 ? new Decimal64(offset, -longVal, true) : new Decimal64(offset, longVal, false); } // <<< FIXME // FIXME: this should take a RoundingMode and perform rounding // FIXME: this should have a truncating counterpart public static Decimal64 valueOf(final float floatVal, final RoundingMode rounding) { // XXX: we should be able to do something smarter here return valueOf(Float.toString(floatVal)); } // FIXME: this should take a RoundingMode and perform rounding // FIXME: this should have a truncating counterpart public static Decimal64 valueOf(final double doubleVal, final RoundingMode rounding) { // XXX: we should be able to do something smarter here return valueOf(Double.toString(doubleVal)); } public static Decimal64 valueOf(final BigDecimal decimalVal) { // FIXME: we should be able to do something smarter here using BigDecimal.unscaledValue() and BigDecimal.scale() return valueOf(decimalVal.toPlainString()); } /** * Attempt to parse a String into a Decimal64. This method uses minimum fraction digits required to hold * the entire value. * * @param str String to parser * @return A Decimal64 instance * @throws NullPointerException if value is null. * @throws NumberFormatException if the string does not contain a parsable decimal64. */ public static Decimal64 valueOf(final String str) { final Either variant = SUPPORT.fromString(str); final Optional value = variant.tryFirst(); if (value.isPresent()) { return value.get(); } final Optional message = variant.getSecond().getMessage(); throw message.isPresent() ? new NumberFormatException(message.get()) : new NumberFormatException(); } /** * Return the scale of this decimal. This is the number of fraction digits, in range {@code [1..18]}. * * @return This decimal's scale */ public final int scale() { return offset + 1; } /** * Return the unscaled value of this decimal. * * @return This decimal's unscaled value */ public final long unscaledValue() { return value; } public final BigDecimal decimalValue() { return BigDecimal.valueOf(value, scale()); } @Override public final int intValue() { return (int) intPart(); } @Override public final long longValue() { return intPart(); } @Override public final float floatValue() { return (float) doubleValue(); } @Override public final double doubleValue() { return 1.0 * value / FACTOR[offset]; } /** * Converts this {@code BigDecimal} to a {@code byte}, checking for lost information. If this {@code Decimal64} has * a nonzero fractional part or is out of the possible range for a {@code byte} result then * an {@code ArithmeticException} is thrown. * * @return this {@code Decimal64} converted to a {@code byte}. * @throws ArithmeticException if {@code this} has a nonzero fractional part, or will not fit in a {@code byte}. */ public final byte byteValueExact() { final long val = longValueExact(); final byte ret = (byte) val; if (val != ret) { throw new ArithmeticException("Value " + val + " is outside of byte range"); } return ret; } /** * Converts this {@code BigDecimal} to a {@code short}, checking for lost information. If this {@code Decimal64} has * a nonzero fractional part or is out of the possible range for a {@code short} result then * an {@code ArithmeticException} is thrown. * * @return this {@code Decimal64} converted to a {@code short}. * @throws ArithmeticException if {@code this} has a nonzero fractional part, or will not fit in a {@code short}. */ public final short shortValueExact() { final long val = longValueExact(); final short ret = (short) val; if (val != ret) { throw new ArithmeticException("Value " + val + " is outside of short range"); } return ret; } /** * Converts this {@code BigDecimal} to an {@code int}, checking for lost information. If this {@code Decimal64} has * a nonzero fractional part or is out of the possible range for an {@code int} result then * an {@code ArithmeticException} is thrown. * * @return this {@code Decimal64} converted to an {@code int}. * @throws ArithmeticException if {@code this} has a nonzero fractional part, or will not fit in an {@code int}. */ public final int intValueExact() { final long val = longValueExact(); final int ret = (int) val; if (val != ret) { throw new ArithmeticException("Value " + val + " is outside of integer range"); } return ret; } /** * Converts this {@code BigDecimal} to a {@code long}, checking for lost information. If this {@code Decimal64} has * a nonzero fractional part then an {@code ArithmeticException} is thrown. * * @return this {@code Decimal64} converted to a {@code long}. * @throws ArithmeticException if {@code this} has a nonzero fractional part. */ public final long longValueExact() { if (fracPart() != 0) { throw new ArithmeticException("Conversion of " + this + " would lose fraction"); } return intPart(); } @Override @SuppressWarnings("checkstyle:parameterName") public final int compareTo(final Decimal64 o) { if (this == o) { return 0; } if (offset == o.offset) { return Long.compare(value, o.value); } // XXX: we could do something smarter here return Double.compare(doubleValue(), o.doubleValue()); } @Override public final String toCanonicalString() { // https://tools.ietf.org/html/rfc6020#section-9.3.2 // // The canonical form of a positive decimal64 does not include the sign // "+". The decimal point is required. Leading and trailing zeros are // prohibited, subject to the rule that there MUST be at least one digit // before and after the decimal point. The value zero is represented as // "0.0". final long intPart = intPart(); final long fracPart = fracPart(); final StringBuilder sb = new StringBuilder(21); if (intPart == 0 && fracPart < 0) { sb.append('-'); } sb.append(intPart).append('.'); if (fracPart != 0) { // We may need to zero-pad the fraction part sb.append(Strings.padStart(Long.toString(Math.abs(fracPart)), scale(), '0')); } else { sb.append('0'); } return sb.toString(); } @Override public final CanonicalValueSupport support() { return SUPPORT; } @Override public final int hashCode() { // We need to normalize the results in order to be consistent with equals() return Long.hashCode(intPart()) * 31 + Long.hashCode(fracPart()); } @Override public final boolean equals(final @Nullable Object obj) { return this == obj || obj instanceof Decimal64 && equalsImpl((Decimal64) obj); } /** * A slightly faster version of {@link #equals(Object)}. * * @param obj Decimal64 object * @return {@code true} if this object is the same as the obj argument; {@code false} otherwise. */ public final boolean equals(final @Nullable Decimal64 obj) { return this == obj || obj != null && equalsImpl(obj); } @Override public final String toString() { return toCanonicalString(); } private boolean equalsImpl(final Decimal64 other) { return offset == other.offset ? value == other.value // We need to normalize both : intPart() == other.intPart() && fracPart() == other.fracPart(); } private long intPart() { return value / FACTOR[offset]; } private long fracPart() { return value % FACTOR[offset]; } private static byte offsetOf(final int scale) { checkArgument(scale >= 1 && scale <= MAX_SCALE, "Scale %s is not in range [1..%s]", scale, MAX_SCALE); return (byte) (scale - 1); } }