io.setl.json.primitive.numbers.CJNumber Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of canonical-json Show documentation
Show all versions of canonical-json Show documentation
An implementation of the Canonical JSON format with support for javax.json and Jackson
The newest version!
package io.setl.json.primitive.numbers;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import jakarta.json.JsonNumber;
import jakarta.json.JsonValue;
import io.setl.json.Canonical;
import io.setl.json.exception.NonFiniteNumberException;
import io.setl.json.primitive.CJBase;
import io.setl.json.primitive.CJString;
import io.setl.json.primitive.cache.CacheManager;
import io.setl.json.primitive.cache.ICache;
/**
* A number.
*
* @author Simon Greatrix on 08/01/2020.
*/
public abstract class CJNumber extends CJBase implements JsonNumber {
/** The number is represented by a BigInteger. */
public static final int TYPE_BIG_INT = 2;
/** The number is represented by a BigDecimal. */
public static final int TYPE_DECIMAL = 3;
/** The number is represented by an int. */
public static final int TYPE_INT = 0;
/** The number is represented by a long. */
public static final int TYPE_LONG = 1;
private static final Map, Function> CREATORS = Map.of(
BigDecimal.class, n -> new CJBigDecimal((BigDecimal) n),
BigInteger.class, n -> new CJBigInteger((BigInteger) n),
Integer.class, n -> create(n.intValue()),
Long.class, n -> new CJLong(n.longValue())
);
private static final Map, UnaryOperator> SIMPLIFIERS = Map.of(
AtomicInteger.class, Number::intValue,
AtomicLong.class, n -> simplify(n.longValue()),
BigDecimal.class, n -> simplify((BigDecimal) n),
BigInteger.class, n -> simplify((BigInteger) n, true),
Byte.class, Number::intValue,
Integer.class, Number::intValue,
Long.class, n -> simplify(n.longValue()),
Short.class, Number::intValue,
Double.class, n -> simplify(n.doubleValue()),
Float.class, n -> simplify(n.floatValue())
);
/**
* Convert a number into a JsonValue. IEEE floating point numbers may specify "Not A Number", "Positive Infinity", or "Negative Infinity". These three special
* cases cannot be represented as numbers in JSON and so will result in a NonFiniteNumberException.
*
* @param value the numeric value to convert to a canonical JSON value
*
* @return the canonical JSON representation
*/
public static CJNumber cast(Number value) {
ICache cache = CacheManager.valueCache();
return cache.get(value, CJNumber::create);
}
/**
* Cast a JsonNumber to a PNumber.
*
* @param jsonNumber the JsonNumber
*
* @return the equivalent (or same) PNumber.
*/
public static CJNumber cast(JsonNumber jsonNumber) {
if (jsonNumber instanceof CJNumber) {
return (CJNumber) jsonNumber;
}
Number number = jsonNumber.numberValue();
return cast(number);
}
/**
* Convert a number into a JsonValue. IEEE floating point numbers may specify "Not A Number", "Positive Infinity", or "Negative Infinity". These three special
* cases cannot be represented as numbers in JSON and so are rendered as Strings.
*
* @param value the numeric value to convert to a canonical JSON value
*
* @return the canonical JSON representation
*/
public static Canonical castUnsafe(Number value) {
ICache cache = CacheManager.valueCache();
try {
return cache.get(value, CJNumber::create);
} catch (NonFiniteNumberException e) {
return e.getRepresentation();
}
}
/**
* Create a JsonNumber from a Number value performing appropriate type simplifications.
*
* @param value the value
*
* @return the JsonNumber for that value
*/
private static CJNumber create(Number value) {
Class extends Number> cl = value.getClass();
UnaryOperator operator = SIMPLIFIERS.get(cl);
if (operator == null) {
throw new IllegalArgumentException("Unknown number class: " + value.getClass());
}
Number simple = operator.apply(value);
cl = simple.getClass();
Function function = CREATORS.get(cl);
if (function != null) {
return function.apply(simple);
}
throw new IllegalArgumentException("Unknown number class: " + value.getClass());
}
/**
* Create a PNumber for an int. The actual type will either be a PInt.
*
* @param i the int
*
* @return the PNumber
*/
public static CJNumber create(int i) {
return new CJInt(i);
}
/**
* Create a PNumber for a long. The actual type will either be a PInt or a PLong depending on the scale of the long.
*
* @param l the long
*
* @return the PNumber
*/
public static CJNumber create(long l) {
if (Integer.MIN_VALUE <= l && l <= Integer.MAX_VALUE) {
return new CJInt((int) l);
}
return new CJLong(l);
}
/**
* Simplify a long into either a long or an int.
*
* @param l the long to simplify
*
* @return the new representation
*/
public static Number simplify(long l) {
if (Integer.MIN_VALUE <= l && l <= Integer.MAX_VALUE) {
return (int) l;
}
return l;
}
static Number simplify(double v) {
if (!Double.isFinite(v)) {
throw new NonFiniteNumberException(v);
}
// try to simplify
if ((v % 1.0) == 0.0) {
// it's an integer
if (Integer.MIN_VALUE <= v && v <= Integer.MAX_VALUE) {
return (int) v;
}
if (Long.MIN_VALUE <= v && v <= Long.MAX_VALUE) {
return (long) v;
}
// Have to convert via BigDecimal to allow for numbers like 1E+100
}
return simplify(new BigDecimal(Double.toString(v)));
}
static Number simplify(float v) {
if (!Float.isFinite(v)) {
throw new NonFiniteNumberException(v);
}
// try to simplify
if ((v % 1.0f) == 0.0f) {
// it's an integer
if (Integer.MIN_VALUE <= v && v <= Integer.MAX_VALUE) {
return (int) v;
}
if (Long.MIN_VALUE <= v && v <= Long.MAX_VALUE) {
return (long) v;
}
// Have to convert via BigDecimal to allow for numbers like 1E+100
}
return simplify(new BigDecimal(Float.toString(v)));
}
static Number simplify(BigDecimal bigDecimal) {
bigDecimal = bigDecimal.stripTrailingZeros();
int s = bigDecimal.scale();
// if scale is positive, the number is a floating point.
// if scale is negative, it's an integer, but if it has a lot of trailing zeros, we still use a BigDecimal
if (CJBigInteger.MIN_SCALE <= s && s <= 0) {
return simplify(bigDecimal.toBigIntegerExact(), false);
}
return bigDecimal;
}
static Number simplify(BigInteger bigInteger, boolean checkZeros) {
int bitLength = bigInteger.bitLength();
if (bitLength < 32) {
return bigInteger.intValueExact();
}
if (bitLength < 64) {
return bigInteger.longValueExact();
}
// check for trailing zeros
if (checkZeros && bigInteger.mod(CJBigInteger.MAX_ZEROS).signum() == 0) {
return new BigDecimal(bigInteger).stripTrailingZeros();
}
// It's a big integer after all
return bigInteger;
}
/**
* Recover a double from a canonical, allowing for NaN, Infinity and -Infinity.
*
* @param canonical the value
*
* @return the Double, or null if it wasn't a double
*/
public static Double toDouble(JsonValue canonical) {
if (canonical.getValueType() == ValueType.NUMBER) {
return ((JsonNumber) canonical).doubleValue();
}
if (canonical instanceof CJString) {
CJString pString = (CJString) canonical;
switch (pString.getString().toLowerCase()) {
case "nan":
return Double.NaN;
case "inf": // falls through
case "+inf": // falls through
case "infinity": // falls through
case "+infinity": // falls through
return Double.POSITIVE_INFINITY;
case "-inf":
case "-infinity":
return Double.NEGATIVE_INFINITY;
default:
break;
}
}
return null;
}
/** New instance. */
public CJNumber() {
// do nothing
}
/** Check if the number is valid. */
protected void check() {
// do nothing
}
/**
* The JSON API requires we test for equality via BigDecimal values. As the canonical JSON does not retain trailing zeros, we actually test for equality by
* the total ordering of real numbers.
*/
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof JsonNumber)) {
return false;
}
if (!(o instanceof CJNumber)) {
o = cast(((JsonNumber) o).bigDecimalValue());
}
// API requires comparison as BigDecimals.
CJNumber pNumber = (CJNumber) o;
switch (pNumber.getNumberType()) {
case TYPE_INT:
return equalsValue(pNumber.intValue());
case TYPE_LONG:
return equalsValue(pNumber.longValue());
case TYPE_BIG_INT:
return equalsValue(pNumber.bigIntegerValue());
default:
return equalsValue(pNumber.bigDecimalValue());
}
}
/**
* Test if this equals a 64-bit long.
*
* @param other the long to compare to
*
* @return true if equal in value
*/
protected abstract boolean equalsValue(long other);
/**
* Test if this equals a BigInteger.
*
* @param other the BigInteger to compare to
*
* @return true if equal in value
*/
protected abstract boolean equalsValue(BigInteger other);
/**
* Test if this equals a BigDecimal.
*
* @param other the BigDecimal to compare to
*
* @return true if equal in value
*/
protected abstract boolean equalsValue(BigDecimal other);
/**
* Get the type of number this is. The result will be one of the TYPE_ constants.
*
* @return the type of number
*/
public abstract int getNumberType();
@Override
public ValueType getValueType() {
return ValueType.NUMBER;
}
@Override
public int hashCode() {
return super.hashCode();
}
@Override
public void writeTo(Appendable writer) throws IOException {
writer.append(toString());
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy