import com.google.common.annotations.Beta;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
-import com.google.common.primitives.Longs;
import java.math.BigDecimal;
-import org.opendaylight.yangtools.concepts.Immutable;
+import java.util.Optional;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.yangtools.concepts.Variant;
/**
* Dedicated type for YANG's 'type decimal64' type. This class is similar to {@link BigDecimal}, but provides more
* @author Robert Varga
*/
@Beta
-public final class Decimal64 extends Number implements Comparable<Decimal64>, Immutable {
+@NonNullByDefault
+public class Decimal64 extends Number implements CanonicalValue<Decimal64> {
+ public static final class Support extends AbstractCanonicalValueSupport<Decimal64> {
+ public Support() {
+ super(Decimal64.class);
+ }
+
+ @Override
+ public Variant<Decimal64, CanonicalValueViolation> 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_FRACTION_DIGITS) {
+ return CanonicalValueViolation.variantOf(
+ "Integer part is longer than " + MAX_FRACTION_DIGITS + " digits");
+ }
+
+ intPart = 10 * intPart + toInt(ch, idx);
+ }
+
+ if (idx > limit) {
+ // No fraction digits, we are done
+ return Variant.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_FRACTION_DIGITS - intLen;
+ 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 Variant.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<Decimal64> SUPPORT = new Support();
private static final long serialVersionUID = 1L;
private static final int MAX_FRACTION_DIGITS = 18;
this.value = negative ? -bits : bits;
}
+ protected Decimal64(final Decimal64 other) {
+ this.scaleOffset = other.scaleOffset;
+ this.value = other.value;
+ }
+
public static Decimal64 valueOf(final byte byteVal) {
return byteVal < 0 ? new Decimal64(1, -byteVal, 0, true) : new Decimal64(1, byteVal, 0, false);
}
}
public static Decimal64 valueOf(final int intVal) {
- return intVal < 0 ? new Decimal64(1, -intVal, 0, true) : new Decimal64(1, intVal, 0, false);
+ return intVal < 0 ? new Decimal64(1, - (long)intVal, 0, true) : new Decimal64(1, intVal, 0, false);
}
public static Decimal64 valueOf(final long longVal) {
* @throws NumberFormatException if the string does not contain a parsable decimal64.
*/
public static Decimal64 valueOf(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()) {
- throw new NumberFormatException("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;
+ final Variant<Decimal64, CanonicalValueViolation> variant = SUPPORT.fromString(str);
+ final Optional<Decimal64> value = variant.tryFirst();
+ if (value.isPresent()) {
+ return value.get();
}
-
- // Sanity check length
- if (idx == str.length()) {
- throw new NumberFormatException("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_FRACTION_DIGITS) {
- throw new NumberFormatException("Integer part is longer than " + MAX_FRACTION_DIGITS + " digits");
- }
-
- intPart = 10 * intPart + toInt(ch, idx);
- }
-
- if (idx > limit) {
- // No fraction digits, we are done
- return new Decimal64((byte)1, intPart, 0, negative);
- }
-
- // Bump index to skip over period and check the remainder
- idx++;
- if (idx > limit) {
- throw new NumberFormatException("Value '" + str + "' is missing fraction digits");
- }
-
- // Trim trailing zeroes, if any
- while (idx < limit && str.charAt(limit) == '0') {
- limit--;
- }
-
- final int fracLimit = MAX_FRACTION_DIGITS - intLen;
- byte fracLen = 0;
- long fracPart = 0;
- for (; idx <= limit; idx++, fracLen++) {
- final char ch = str.charAt(idx);
- if (fracLen == fracLimit) {
- throw new NumberFormatException("Fraction part longer than " + fracLimit + " digits");
- }
-
- fracPart = 10 * fracPart + toInt(ch, idx);
- }
-
- return new Decimal64(fracLen, intPart, fracPart, negative);
+ final Optional<String> message = variant.getSecond().getMessage();
+ throw message.isPresent() ? new NumberFormatException(message.get()) : new NumberFormatException();
}
- public BigDecimal decimalValue() {
+ public final BigDecimal decimalValue() {
return BigDecimal.valueOf(value, scaleOffset + 1);
}
@Override
- public int intValue() {
+ public final int intValue() {
return (int) intPart();
}
@Override
- public long longValue() {
+ public final long longValue() {
return intPart();
}
@Override
- public float floatValue() {
+ public final float floatValue() {
return (float) doubleValue();
}
@Override
- public double doubleValue() {
+ public final double doubleValue() {
return 1.0 * value / SCALE[scaleOffset];
}
- @Override
- @SuppressWarnings("checkstyle:parameterName")
- public int compareTo(final Decimal64 o) {
- if (this == o) {
- return 0;
+ /**
+ * 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");
}
- if (scaleOffset == o.scaleOffset) {
- return Long.compare(value, o.value);
+ 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;
+ }
- // XXX: we could do something smarter here
- return Double.compare(doubleValue(), o.doubleValue());
+ /**
+ * 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;
}
- @Override
- public int hashCode() {
- // We need to normalize the results in order to be consistent with equals()
- return Longs.hashCode(intPart()) * 31 + Long.hashCode(fracPart());
+ /**
+ * 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
- public boolean equals(final Object obj) {
- if (this == obj) {
- return true;
- }
- if (!(obj instanceof Decimal64)) {
- return false;
+ @SuppressWarnings("checkstyle:parameterName")
+ public final int compareTo(final Decimal64 o) {
+ if (this == o) {
+ return 0;
}
- final Decimal64 other = (Decimal64) obj;
- if (scaleOffset == other.scaleOffset) {
- return value == other.value;
+ if (scaleOffset == o.scaleOffset) {
+ return Long.compare(value, o.value);
}
- // We need to normalize both
- return intPart() == other.intPart() && fracPart() == fracPart();
+ // XXX: we could do something smarter here
+ return Double.compare(doubleValue(), o.doubleValue());
}
@Override
- public String toString() {
+ 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
return sb.toString();
}
+ @Override
+ public final CanonicalValueSupport<Decimal64> 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 scaleOffset == other.scaleOffset ? value == other.value
+ // We need to normalize both
+ : intPart() == other.intPart() && fracPart() == other.fracPart();
+ }
+
private long intPart() {
return value / SCALE[scaleOffset];
}
private long fracPart() {
return Math.abs(value % SCALE[scaleOffset]);
}
-
- 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';
- }
}