org.testifyproject.jexl3.JexlArithmetic Maven / Gradle / Ivy
Show all versions of core Show documentation
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.testifyproject.jexl3;
import org.testifyproject.jexl3.introspection.JexlMethod;
import java.lang.reflect.Array;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.MathContext;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Perform arithmetic, implements JexlOperator methods.
*
* This is the class to derive to implement new operator behaviors.
*
* The 5 base arithmetic operators (+, - , *, /, %) follow the same evaluation rules regarding their arguments.
*
* - If both are null, result is 0
* - If either is a BigDecimal, coerce both to BigDecimal and perform operation
* - If either is a floating point number, coerce both to Double and perform operation
* - Else treat as BigInteger, perform operation and attempt to narrow result:
*
* - if both arguments can be narrowed to Integer, narrow result to Integer
* - if both arguments can be narrowed to Long, narrow result to Long
* - Else return result as BigInteger
*
*
*
*
* Note that the only exception thrown by JexlArithmetic is and must be ArithmeticException.
*
* @see JexlOperator
* @since 2.0
*/
public class JexlArithmetic {
/** Marker class for null operand exceptions. */
public static class NullOperand extends ArithmeticException {}
/** Double.MAX_VALUE as BigDecimal. */
protected static final BigDecimal BIGD_DOUBLE_MAX_VALUE = BigDecimal.valueOf(Double.MAX_VALUE);
/** Double.MIN_VALUE as BigDecimal. */
protected static final BigDecimal BIGD_DOUBLE_MIN_VALUE = BigDecimal.valueOf(Double.MIN_VALUE);
/** Long.MAX_VALUE as BigInteger. */
protected static final BigInteger BIGI_LONG_MAX_VALUE = BigInteger.valueOf(Long.MAX_VALUE);
/** Long.MIN_VALUE as BigInteger. */
protected static final BigInteger BIGI_LONG_MIN_VALUE = BigInteger.valueOf(Long.MIN_VALUE);
/** Default BigDecimal scale. */
protected static final int BIGD_SCALE = -1;
/** Whether this JexlArithmetic instance behaves in strict or lenient mode. */
private final boolean strict;
/** The big decimal math context. */
private final MathContext mathContext;
/** The big decimal scale. */
private final int mathScale;
/**
* Creates a JexlArithmetic.
*
* @param astrict whether this arithmetic is strict or lenient
*/
public JexlArithmetic(boolean astrict) {
this(astrict, null, Integer.MIN_VALUE);
}
/**
* Creates a JexlArithmetic.
*
* @param astrict whether this arithmetic is lenient or strict
* @param bigdContext the math context instance to use for +,-,/,*,% operations on big decimals.
* @param bigdScale the scale used for big decimals.
*/
public JexlArithmetic(boolean astrict, MathContext bigdContext, int bigdScale) {
this.strict = astrict;
this.mathContext = bigdContext == null ? MathContext.DECIMAL128 : bigdContext;
this.mathScale = bigdScale == Integer.MIN_VALUE ? BIGD_SCALE : bigdScale;
}
/**
* Apply options to this arithmetic which eventually may create another instance.
* @see #createWithOptions(boolean, java.math.MathContext, int)
*
* @param options the {@link JexlEngine.Options} to use
* @return an arithmetic with those options set
*/
public JexlArithmetic options(JexlEngine.Options options) {
Boolean ostrict = options.isStrictArithmetic();
if (ostrict == null) {
ostrict = isStrict();
}
MathContext bigdContext = options.getArithmeticMathContext();
if (bigdContext == null) {
bigdContext = getMathContext();
}
int bigdScale = options.getArithmeticMathScale();
if (bigdScale == Integer.MIN_VALUE) {
bigdScale = getMathScale();
}
if (ostrict != isStrict()
|| bigdScale != getMathScale()
|| bigdContext != getMathContext()) {
return createWithOptions(ostrict, bigdContext, bigdScale);
}
return this;
}
/**
* Apply options to this arithmetic which eventually may create another instance.
* @see #createWithOptions(boolean, java.math.MathContext, int)
*
* @param context the context that may extend {@link JexlEngine.Options} to use
* @return a new arithmetic instance or this
* @since 3.1
*/
public JexlArithmetic options(JexlContext context) {
return context instanceof JexlEngine.Options
? options((JexlEngine.Options) context)
: this;
}
/**
* Creates a JexlArithmetic instance.
* Called by options(...) method when another instance of the same class of arithmetic is required.
* @see #options(org.testifyproject.jexl3.JexlEngine.Options)
*
* @param astrict whether this arithmetic is lenient or strict
* @param bigdContext the math context instance to use for +,-,/,*,% operations on big decimals.
* @param bigdScale the scale used for big decimals.
* @return default is a new JexlArithmetic instance
* @since 3.1
*/
protected JexlArithmetic createWithOptions(boolean astrict, MathContext bigdContext, int bigdScale) {
return new JexlArithmetic(astrict, bigdContext, bigdScale);
}
/**
* The interface that uberspects JexlArithmetic classes.
* This allows overloaded operator methods discovery.
*/
public interface Uberspect {
/**
* Checks whether this uberspect has overloads for a given operator.
*
* @param operator the operator to check
* @return true if an overload exists, false otherwise
*/
boolean overloads(JexlOperator operator);
/**
* Gets the most specific method for an operator.
*
* @param operator the operator
* @param arg the arguments
* @return the most specific method or null if no specific override could be found
*/
JexlMethod getOperator(JexlOperator operator, Object... arg);
}
/**
* Helper interface used when creating an array literal.
*
* The default implementation creates an array and attempts to type it strictly.
*
*
* - If all objects are of the same type, the array returned will be an array of that same type
* - If all objects are Numbers, the array returned will be an array of Numbers
* - If all objects are convertible to a primitive type, the array returned will be an array
* of the primitive type
*
*/
public interface ArrayBuilder {
/**
* Adds a literal to the array.
*
* @param value the item to add
*/
void add(Object value);
/**
* Creates the actual "array" instance.
*
* @param extended true when the last argument is ', ...'
* @return the array
*/
Object create(boolean extended);
}
/**
* Called by the interpreter when evaluating a literal array.
*
* @param size the number of elements in the array
* @return the array builder
*/
public ArrayBuilder arrayBuilder(int size) {
return new org.testifyproject.jexl3.internal.ArrayBuilder(size);
}
/**
* Helper interface used when creating a set literal.
* The default implementation creates a java.util.HashSet.
*/
public interface SetBuilder {
/**
* Adds a literal to the set.
*
* @param value the item to add
*/
void add(Object value);
/**
* Creates the actual "set" instance.
*
* @return the set
*/
Object create();
}
/**
* Called by the interpreter when evaluating a literal set.
*
* @param size the number of elements in the set
* @return the array builder
*/
public SetBuilder setBuilder(int size) {
return new org.testifyproject.jexl3.internal.SetBuilder(size);
}
/**
* Helper interface used when creating a map literal.
* The default implementation creates a java.util.HashMap.
*/
public interface MapBuilder {
/**
* Adds a new entry to the map.
*
* @param key the map entry key
* @param value the map entry value
*/
void put(Object key, Object value);
/**
* Creates the actual "map" instance.
*
* @return the map
*/
Object create();
}
/**
* Called by the interpreter when evaluating a literal map.
*
* @param size the number of elements in the map
* @return the map builder
*/
public MapBuilder mapBuilder(int size) {
return new org.testifyproject.jexl3.internal.MapBuilder(size);
}
/**
* Creates a literal range.
* The default implementation only accepts integers and longs.
*
* @param from the included lower bound value (null if none)
* @param to the included upper bound value (null if none)
* @return the range as an iterable
* @throws ArithmeticException as an option if creation fails
*/
public Iterable> createRange(Object from, Object to) throws ArithmeticException {
final long lfrom = toLong(from);
final long lto = toLong(to);
if ((lfrom >= Integer.MIN_VALUE && lfrom <= Integer.MAX_VALUE)
&& (lto >= Integer.MIN_VALUE && lto <= Integer.MAX_VALUE)) {
return org.testifyproject.jexl3.internal.IntegerRange.create((int) lfrom, (int) lto);
} else {
return org.testifyproject.jexl3.internal.LongRange.create(lfrom, lto);
}
}
/**
* Checks whether this JexlArithmetic instance
* strictly considers null as an error when used as operand unexpectedly.
*
* @return true if strict, false if lenient
*/
public boolean isStrict() {
return this.strict;
}
/**
* The MathContext instance used for +,-,/,*,% operations on big decimals.
*
* @return the math context
*/
public MathContext getMathContext() {
return mathContext;
}
/**
* The BigDecimal scale used for comparison and coericion operations.
*
* @return the scale
*/
public int getMathScale() {
return mathScale;
}
/**
* Ensure a big decimal is rounded by this arithmetic scale and rounding mode.
*
* @param number the big decimal to round
* @return the rounded big decimal
*/
protected BigDecimal roundBigDecimal(final BigDecimal number) {
int mscale = getMathScale();
if (mscale >= 0) {
return number.setScale(mscale, getMathContext().getRoundingMode());
} else {
return number;
}
}
/**
* The result of +,/,-,*,% when both operands are null.
*
* @return Integer(0) if lenient
* @throws ArithmeticException if strict
*/
protected Object controlNullNullOperands() {
if (isStrict()) {
throw new NullOperand();
}
return 0;
}
/**
* Throw a NPE if arithmetic is strict.
*
* @throws ArithmeticException if strict
*/
protected void controlNullOperand() {
if (isStrict()) {
throw new NullOperand();
}
}
/**
* The float regular expression pattern.
*
* The decimal and exponent parts are optional and captured allowing to determine if the number is a real
* by checking whether one of these 2 capturing groups is not empty.
*/
public static final Pattern FLOAT_PATTERN = Pattern.compile("^[+-]?\\d*(\\.\\d*)?([eE][+-]?\\d+)?$");
/**
* Test if the passed value is a floating point number, i.e. a float, double
* or string with ( "." | "E" | "e").
*
* @param val the object to be tested
* @return true if it is, false otherwise.
*/
protected boolean isFloatingPointNumber(Object val) {
if (val instanceof Float || val instanceof Double) {
return true;
}
if (val instanceof CharSequence) {
final Matcher m = FLOAT_PATTERN.matcher((CharSequence) val);
// first group is decimal, second is exponent;
// one of them must exist hence start({1,2}) >= 0
return m.matches() && (m.start(1) >= 0 || m.start(2) >= 0);
}
return false;
}
/**
* Is Object a floating point number.
*
* @param o Object to be analyzed.
* @return true if it is a Float or a Double.
*/
protected boolean isFloatingPoint(final Object o) {
return o instanceof Float || o instanceof Double;
}
/**
* Is Object a whole number.
*
* @param o Object to be analyzed.
* @return true if Integer, Long, Byte, Short or Character.
*/
protected boolean isNumberable(final Object o) {
return o instanceof Integer
|| o instanceof Long
|| o instanceof Byte
|| o instanceof Short
|| o instanceof Character;
}
/**
* Given a Number, return back the value using the smallest type the result
* will fit into.
*
This works hand in hand with parameter 'widening' in java
* method calls, e.g. a call to substring(int,int) with an int and a long
* will fail, but a call to substring(int,int) with an int and a short will
* succeed.
*
* @param original the original number.
* @return a value of the smallest type the original number will fit into.
*/
public Number narrow(Number original) {
return narrowNumber(original, null);
}
/**
* Whether we consider the narrow class as a potential candidate for narrowing the source.
*
* @param narrow the target narrow class
* @param source the orginal source class
* @return true if attempt to narrow source to target is accepted
*/
protected boolean narrowAccept(Class> narrow, Class> source) {
return narrow == null || narrow.equals(source);
}
/**
* Given a Number, return back the value attempting to narrow it to a target class.
*
* @param original the original number
* @param narrow the attempted target class
* @return the narrowed number or the source if no narrowing was possible
*/
public Number narrowNumber(Number original, Class> narrow) {
if (original == null) {
return null;
}
Number result = original;
if (original instanceof BigDecimal) {
BigDecimal bigd = (BigDecimal) original;
// if it's bigger than a double it can't be narrowed
if (bigd.compareTo(BIGD_DOUBLE_MAX_VALUE) > 0
|| bigd.compareTo(BIGD_DOUBLE_MIN_VALUE) < 0) {
return original;
} else {
try {
long l = bigd.longValueExact();
// coerce to int when possible (int being so often used in method parms)
if (narrowAccept(narrow, Integer.class)
&& l <= Integer.MAX_VALUE
&& l >= Integer.MIN_VALUE) {
return (int) l;
} else if (narrowAccept(narrow, Long.class)) {
return l;
}
} catch (ArithmeticException xa) {
// ignore, no exact value possible
}
}
}
if (original instanceof Double || original instanceof Float) {
double value = original.doubleValue();
if (narrowAccept(narrow, Float.class)
&& value <= Float.MAX_VALUE
&& value >= Float.MIN_VALUE) {
result = result.floatValue();
}
// else it fits in a double only
} else {
if (original instanceof BigInteger) {
BigInteger bigi = (BigInteger) original;
// if it's bigger than a Long it can't be narrowed
if (bigi.compareTo(BIGI_LONG_MAX_VALUE) > 0
|| bigi.compareTo(BIGI_LONG_MIN_VALUE) < 0) {
return original;
}
}
long value = original.longValue();
if (narrowAccept(narrow, Byte.class)
&& value <= Byte.MAX_VALUE
&& value >= Byte.MIN_VALUE) {
// it will fit in a byte
result = (byte) value;
} else if (narrowAccept(narrow, Short.class)
&& value <= Short.MAX_VALUE
&& value >= Short.MIN_VALUE) {
result = (short) value;
} else if (narrowAccept(narrow, Integer.class)
&& value <= Integer.MAX_VALUE
&& value >= Integer.MIN_VALUE) {
result = (int) value;
}
// else it fits in a long
}
return result;
}
/**
* Given a BigInteger, narrow it to an Integer or Long if it fits and the arguments
* class allow it.
*
* The rules are:
* if either arguments is a BigInteger, no narrowing will occur
* if either arguments is a Long, no narrowing to Integer will occur
*
*
* @param lhs the left hand side operand that lead to the bigi result
* @param rhs the right hand side operand that lead to the bigi result
* @param bigi the BigInteger to narrow
* @return an Integer or Long if narrowing is possible, the original BigInteger otherwise
*/
protected Number narrowBigInteger(Object lhs, Object rhs, BigInteger bigi) {
//coerce to long if possible
if (!(lhs instanceof BigInteger || rhs instanceof BigInteger)
&& bigi.compareTo(BIGI_LONG_MAX_VALUE) <= 0
&& bigi.compareTo(BIGI_LONG_MIN_VALUE) >= 0) {
// coerce to int if possible
long l = bigi.longValue();
// coerce to int when possible (int being so often used in method parms)
if (!(lhs instanceof Long || rhs instanceof Long)
&& l <= Integer.MAX_VALUE
&& l >= Integer.MIN_VALUE) {
return (int) l;
}
return l;
}
return bigi;
}
/**
* Given a BigDecimal, attempt to narrow it to an Integer or Long if it fits if
* one of the arguments is a numberable.
*
* @param lhs the left hand side operand that lead to the bigd result
* @param rhs the right hand side operand that lead to the bigd result
* @param bigd the BigDecimal to narrow
* @return an Integer or Long if narrowing is possible, the original BigInteger otherwise
*/
protected Number narrowBigDecimal(Object lhs, Object rhs, BigDecimal bigd) {
if (isNumberable(lhs) || isNumberable(rhs)) {
try {
long l = bigd.longValueExact();
// coerce to int when possible (int being so often used in method parms)
if (l <= Integer.MAX_VALUE && l >= Integer.MIN_VALUE) {
return (int) l;
} else {
return l;
}
} catch (ArithmeticException xa) {
// ignore, no exact value possible
}
}
return bigd;
}
/**
* Replace all numbers in an arguments array with the smallest type that will fit.
*
* @param args the argument array
* @return true if some arguments were narrowed and args array is modified,
* false if no narrowing occurred and args array has not been modified
*/
public boolean narrowArguments(Object[] args) {
boolean narrowed = false;
for (int a = 0; a < args.length; ++a) {
Object arg = args[a];
if (arg instanceof Number) {
Number narg = (Number) arg;
Number narrow = narrow(narg);
if (!narg.equals(narrow)) {
args[a] = narrow;
narrowed = true;
}
}
}
return narrowed;
}
/**
* Add two values together.
*
* If any numeric add fails on coercion to the appropriate type,
* treat as Strings and do concatenation.
*
*
* @param left left argument
* @param right right argument
* @return left + right.
*/
public Object add(Object left, Object right) {
if (left == null && right == null) {
return controlNullNullOperands();
}
boolean strconcat = strict
? left instanceof String || right instanceof String
: left instanceof String && right instanceof String;
if (!strconcat) {
try {
// if either are bigdecimal use that type
if (left instanceof BigDecimal || right instanceof BigDecimal) {
BigDecimal l = toBigDecimal(left);
BigDecimal r = toBigDecimal(right);
BigDecimal result = l.add(r, getMathContext());
return narrowBigDecimal(left, right, result);
}
// if either are floating point (double or float) use double
if (isFloatingPointNumber(left) || isFloatingPointNumber(right)) {
double l = toDouble(left);
double r = toDouble(right);
return l + r;
}
// otherwise treat as integers
BigInteger l = toBigInteger(left);
BigInteger r = toBigInteger(right);
BigInteger result = l.add(r);
return narrowBigInteger(left, right, result);
} catch (java.lang.NumberFormatException nfe) {
if (left == null || right == null) {
controlNullOperand();
}
}
}
return toString(left).concat(toString(right));
}
/**
* Divide the left value by the right.
*
* @param left left argument
* @param right right argument
* @return left / right
* @throws ArithmeticException if right == 0
*/
public Object divide(Object left, Object right) {
if (left == null && right == null) {
return controlNullNullOperands();
}
// if either are bigdecimal use that type
if (left instanceof BigDecimal || right instanceof BigDecimal) {
BigDecimal l = toBigDecimal(left);
BigDecimal r = toBigDecimal(right);
if (BigDecimal.ZERO.equals(r)) {
throw new ArithmeticException("/");
}
BigDecimal result = l.divide(r, getMathContext());
return narrowBigDecimal(left, right, result);
}
// if either are floating point (double or float) use double
if (isFloatingPointNumber(left) || isFloatingPointNumber(right)) {
double l = toDouble(left);
double r = toDouble(right);
if (r == 0.0) {
throw new ArithmeticException("/");
}
return l / r;
}
// otherwise treat as integers
BigInteger l = toBigInteger(left);
BigInteger r = toBigInteger(right);
if (BigInteger.ZERO.equals(r)) {
throw new ArithmeticException("/");
}
BigInteger result = l.divide(r);
return narrowBigInteger(left, right, result);
}
/**
* left value modulo right.
*
* @param left left argument
* @param right right argument
* @return left % right
* @throws ArithmeticException if right == 0.0
*/
public Object mod(Object left, Object right) {
if (left == null && right == null) {
return controlNullNullOperands();
}
// if either are bigdecimal use that type
if (left instanceof BigDecimal || right instanceof BigDecimal) {
BigDecimal l = toBigDecimal(left);
BigDecimal r = toBigDecimal(right);
if (BigDecimal.ZERO.equals(r)) {
throw new ArithmeticException("%");
}
BigDecimal remainder = l.remainder(r, getMathContext());
return narrowBigDecimal(left, right, remainder);
}
// if either are floating point (double or float) use double
if (isFloatingPointNumber(left) || isFloatingPointNumber(right)) {
double l = toDouble(left);
double r = toDouble(right);
if (r == 0.0) {
throw new ArithmeticException("%");
}
return l % r;
}
// otherwise treat as integers
BigInteger l = toBigInteger(left);
BigInteger r = toBigInteger(right);
if (BigInteger.ZERO.equals(r)) {
throw new ArithmeticException("%");
}
BigInteger result = l.mod(r);
return narrowBigInteger(left, right, result);
}
/**
* Multiply the left value by the right.
*
* @param left left argument
* @param right right argument
* @return left * right.
*/
public Object multiply(Object left, Object right) {
if (left == null && right == null) {
return controlNullNullOperands();
}
// if either are bigdecimal use that type
if (left instanceof BigDecimal || right instanceof BigDecimal) {
BigDecimal l = toBigDecimal(left);
BigDecimal r = toBigDecimal(right);
BigDecimal result = l.multiply(r, getMathContext());
return narrowBigDecimal(left, right, result);
}
// if either are floating point (double or float) use double
if (isFloatingPointNumber(left) || isFloatingPointNumber(right)) {
double l = toDouble(left);
double r = toDouble(right);
return l * r;
}
// otherwise treat as integers
BigInteger l = toBigInteger(left);
BigInteger r = toBigInteger(right);
BigInteger result = l.multiply(r);
return narrowBigInteger(left, right, result);
}
/**
* Subtract the right value from the left.
*
* @param left left argument
* @param right right argument
* @return left - right.
*/
public Object subtract(Object left, Object right) {
if (left == null && right == null) {
return controlNullNullOperands();
}
// if either are bigdecimal use that type
if (left instanceof BigDecimal || right instanceof BigDecimal) {
BigDecimal l = toBigDecimal(left);
BigDecimal r = toBigDecimal(right);
BigDecimal result = l.subtract(r, getMathContext());
return narrowBigDecimal(left, right, result);
}
// if either are floating point (double or float) use double
if (isFloatingPointNumber(left) || isFloatingPointNumber(right)) {
double l = toDouble(left);
double r = toDouble(right);
return l - r;
}
// otherwise treat as integers
BigInteger l = toBigInteger(left);
BigInteger r = toBigInteger(right);
BigInteger result = l.subtract(r);
return narrowBigInteger(left, right, result);
}
/**
* Negates a value (unary minus for numbers).
*
* @param val the value to negate
* @return the negated value
*/
public Object negate(Object val) {
if (val instanceof Integer) {
return -((Integer) val);
} else if (val instanceof Double) {
return - ((Double) val);
} else if (val instanceof Long) {
return -((Long) val);
} else if (val instanceof BigDecimal) {
return ((BigDecimal) val).negate();
} else if (val instanceof BigInteger) {
return ((BigInteger) val).negate();
} else if (val instanceof Float) {
return -((Float) val);
} else if (val instanceof Short) {
return (short) -((Short) val);
} else if (val instanceof Byte) {
return (byte) -((Byte) val);
} else if (val instanceof Boolean) {
return ((Boolean) val) ? Boolean.FALSE : Boolean.TRUE;
} else if (val instanceof AtomicBoolean) {
return ((AtomicBoolean) val).get() ? Boolean.FALSE : Boolean.TRUE;
}
throw new ArithmeticException("Object negation:(" + val + ")");
}
/**
* Test if left contains right (right matches/in left).
* Beware that this method arguments are the opposite of the operator arguments.
* 'x in y' means 'y contains x'.
*
* @param container the container
* @param value the value
* @return test result or null if there is no arithmetic solution
*/
public Boolean contains(Object container, Object value) {
if (value == null && container == null) {
//if both are null L == R
return true;
}
if (value == null || container == null) {
// we know both aren't null, therefore L != R
return false;
}
// use arithmetic / pattern matching ?
if (container instanceof java.util.regex.Pattern) {
return ((java.util.regex.Pattern) container).matcher(value.toString()).matches();
}
if (container instanceof String) {
return value.toString().matches(container.toString());
}
// try contains on map key
if (container instanceof Map, ?>) {
if (value instanceof Map, ?>) {
return ((Map, ?>) container).keySet().containsAll(((Map, ?>) value).keySet());
}
return ((Map, ?>) container).containsKey(value);
}
// try contains on collection
if (container instanceof Collection>) {
if (value instanceof Collection>) {
return ((Collection>) container).containsAll((Collection>) value);
}
// left in right ? <=> right.contains(left) ?
return ((Collection>) container).contains(value);
}
return null;
}
/**
* Test if left ends with right.
*
* @param left left argument
* @param right right argument
* @return left $= right if there is no arithmetic solution
*/
public Boolean endsWith(Object left, Object right) {
if (left == null && right == null) {
//if both are null L == R
return true;
}
if (left == null || right == null) {
// we know both aren't null, therefore L != R
return false;
}
if (left instanceof String) {
return ((String) left).endsWith(toString(right));
}
return null;
}
/**
* Test if left starts with right.
*
* @param left left argument
* @param right right argument
* @return left ^= right or null if there is no arithmetic solution
*/
public Boolean startsWith(Object left, Object right) {
if (left == null && right == null) {
//if both are null L == R
return true;
}
if (left == null || right == null) {
// we know both aren't null, therefore L != R
return false;
}
if (left instanceof String) {
return ((String) left).startsWith(toString(right));
}
return null;
}
/**
* Check for emptyness of various types: Number, Collection, Array, Map, String.
*
* @param object the object to check the emptyness of
* @return the boolean or null of there is no arithmetic solution
*/
public Boolean isEmpty(Object object) {
if (object instanceof Number) {
double d = ((Number) object).doubleValue();
return Double.isNaN(d) || d == 0.d ? Boolean.TRUE : Boolean.FALSE;
}
if (object instanceof String) {
return "".equals(object) ? Boolean.TRUE : Boolean.FALSE;
}
if (object.getClass().isArray()) {
return Array.getLength(object) == 0 ? Boolean.TRUE : Boolean.FALSE;
}
if (object instanceof Collection>) {
return ((Collection>) object).isEmpty() ? Boolean.TRUE : Boolean.FALSE;
}
// Map isn't a collection
if (object instanceof Map, ?>) {
return ((Map, ?>) object).isEmpty() ? Boolean.TRUE : Boolean.FALSE;
}
return null;
}
/**
* Calculate the size
of various types: Collection, Array, Map, String.
*
* @param object the object to get the size of
* @return the size of object or null if there is no arithmetic solution
*/
public Integer size(Object object) {
if (object instanceof String) {
return ((String) object).length();
}
if (object.getClass().isArray()) {
return Array.getLength(object);
}
if (object instanceof Collection>) {
return ((Collection>) object).size();
}
if (object instanceof Map, ?>) {
return ((Map, ?>) object).size();
}
return null;
}
/**
* Performs a bitwise and.
*
* @param left the left operand
* @param right the right operator
* @return left & right
*/
public Object and(Object left, Object right) {
long l = toLong(left);
long r = toLong(right);
return l & r;
}
/**
* Performs a bitwise or.
*
* @param left the left operand
* @param right the right operator
* @return left | right
*/
public Object or(Object left, Object right) {
long l = toLong(left);
long r = toLong(right);
return l | r;
}
/**
* Performs a bitwise xor.
*
* @param left the left operand
* @param right the right operator
* @return left ^ right
*/
public Object xor(Object left, Object right) {
long l = toLong(left);
long r = toLong(right);
return l ^ r;
}
/**
* Performs a bitwise complement.
*
* @param val the operand
* @return ~val
*/
public Object complement(Object val) {
long l = toLong(val);
return ~l;
}
/**
* Performs a logical not.
*
* @param val the operand
* @return !val
*/
public Object not(Object val) {
return toBoolean(val) ? Boolean.FALSE : Boolean.TRUE;
}
/**
* Performs a comparison.
*
* @param left the left operand
* @param right the right operator
* @param operator the operator
* @return -1 if left < right; +1 if left > right; 0 if left == right
* @throws ArithmeticException if either left or right is null
*/
protected int compare(Object left, Object right, String operator) {
if (left != null && right != null) {
if (left instanceof BigDecimal || right instanceof BigDecimal) {
BigDecimal l = toBigDecimal(left);
BigDecimal r = toBigDecimal(right);
return l.compareTo(r);
} else if (left instanceof BigInteger || right instanceof BigInteger) {
BigInteger l = toBigInteger(left);
BigInteger r = toBigInteger(right);
return l.compareTo(r);
} else if (isFloatingPoint(left) || isFloatingPoint(right)) {
double lhs = toDouble(left);
double rhs = toDouble(right);
if (Double.isNaN(lhs)) {
if (Double.isNaN(rhs)) {
return 0;
} else {
return -1;
}
} else if (Double.isNaN(rhs)) {
// lhs is not NaN
return +1;
} else if (lhs < rhs) {
return -1;
} else if (lhs > rhs) {
return +1;
} else {
return 0;
}
} else if (isNumberable(left) || isNumberable(right)) {
long lhs = toLong(left);
long rhs = toLong(right);
if (lhs < rhs) {
return -1;
} else if (lhs > rhs) {
return +1;
} else {
return 0;
}
} else if (left instanceof String || right instanceof String) {
return toString(left).compareTo(toString(right));
} else if ("==".equals(operator)) {
return left.equals(right) ? 0 : -1;
} else if (left instanceof Comparable>) {
@SuppressWarnings("unchecked") // OK because of instanceof check above
final Comparable