Add type-bound equals() to common int types
[yangtools.git] / yang / yang-common / src / main / java / org / opendaylight / yangtools / yang / common / Decimal64.java
index 5b7bc508be3279ed29b10e2d8c5b097ca248e959..78354f391594807493c9ff3d191ca9265c75b13c 100644 (file)
@@ -13,9 +13,11 @@ 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 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
@@ -24,7 +26,116 @@ import org.opendaylight.yangtools.concepts.Immutable;
  * @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;
@@ -66,6 +177,11 @@ public final class Decimal64 extends Number implements Comparable<Decimal64>, Im
         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);
     }
@@ -75,7 +191,7 @@ public final class Decimal64 extends Number implements Comparable<Decimal64>, Im
     }
 
     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) {
@@ -103,160 +219,120 @@ public final class Decimal64 extends Number implements Comparable<Decimal64>, Im
      * @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
@@ -276,6 +352,43 @@ public final class Decimal64 extends Number implements Comparable<Decimal64>, Im
         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];
     }
@@ -283,11 +396,4 @@ public final class Decimal64 extends Number implements Comparable<Decimal64>, Im
     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';
-    }
 }