org.apache.commons.jexl2.JexlArithmetic Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of commons-jexl Show documentation
Show all versions of commons-jexl Show documentation
Jexl is an implementation of the JSTL Expression Language with extensions.
/*
* 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.apache.commons.jexl2;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.math.BigInteger;
/**
* Perform arithmetic.
*
* All arithmetic operators (+, - , *, /, %) follow the same rules regarding their arguments.
*
* - If both are null, result is 0
* - If either is a floating point number, coerce both to Double and perform operation
* - If both are BigInteger, treat as BigInteger and perform operation
* - If either is a BigDecimal, coerce both to BigDecimal and 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
*
*
*
*
* @since 2.0
*/
public class JexlArithmetic {
/** 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);
/** Whether this JexlArithmetic instance behaves in strict or lenient mode. */
private boolean strict;
/**
* Creates a JexlArithmetic.
* @param lenient whether this arithmetic is lenient or strict
*/
public JexlArithmetic(boolean lenient) {
this.strict = !lenient;
}
/**
* Sets whether this JexlArithmetic instance triggers errors during evaluation when
* null is used as an operand.
* This method is not thread safe; it may be called as an optional step by the JexlEngine
* in its initialization code before expression creation & evaluation.
* @see JexlEngine#setSilent
* @see JexlEngine#setDebug
* @param lenient true means no JexlException will occur, false allows them
*/
void setLenient(boolean lenient) {
this.strict = !lenient;
}
/**
* Checks whether this JexlArithmetic instance triggers errors during evaluation
* when null is used as an operand.
* @return true if lenient, false if strict
*/
public boolean isLenient() {
return !this.strict;
}
/**
* The result of +,/,-,*,% when both operands are null.
* @return null if strict, else Long(0)
*/
protected Object controlNullNullOperands() {
return strict? null : Integer.valueOf(0);
}
/**
* Throw a NPE if arithmetic is strict.
*/
protected void controlNullOperand() {
if (strict) {
throw new NullPointerException(JexlException.NULL_OPERAND);
}
}
/**
* Test if either left or right are either a Float or Double.
* @param left one object to test
* @param right the other
* @return the result of the test.
*/
protected boolean isFloatingPointType(Object left, Object right) {
return left instanceof Float || left instanceof Double || right instanceof Float || right instanceof Double;
}
/**
* 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 String) {
String string = (String) val;
return string.indexOf('.') != -1 || string.indexOf('e') != -1 || string.indexOf('E') != -1;
}
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 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 Integer.valueOf((int) l);
}
return Long.valueOf(l);
}
return bigi;
}
/**
* Given an array of objects, attempt to type it more 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
*
* @param untyped an untyped array
* @return the original array if the attempt to strictly type the array fails, a typed array otherwise
*/
protected Object narrowArrayType(Object[] untyped) {
final int size = untyped.length;
Class> commonClass = null;
if (size > 0) {
// base common class on first entry
commonClass = untyped[0].getClass();
final boolean isNumber = Number.class.isAssignableFrom(commonClass);
// for all children after first...
for (int i = 1; i < size; i++) {
Class> eclass = untyped[i].getClass();
// detect same type for all elements in array
if (!Object.class.equals(commonClass) && !commonClass.equals(eclass)) {
// if both are numbers...
if (isNumber && Number.class.isAssignableFrom(eclass)) {
commonClass = Number.class;
} else {
commonClass = Object.class;
}
}
}
// convert array to the common class if not Object.class
if (!Object.class.equals(commonClass)) {
// if the commonClass has an equivalent primitive type, get it
if (isNumber) {
try {
Field TYPE = commonClass.getField("TYPE");
commonClass = (Class>) TYPE.get(null);
} catch (Exception xany) {
// ignore
}
}
// allocate and fill up the typed array
Object typed = Array.newInstance(commonClass, size);
for(int i = 0; i < size; ++i) {
Array.set(typed, i, untyped[i]);
}
return typed;
}
}
return untyped;
}
/**
* 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 occured and args array has not been modified
*/
protected boolean narrowArguments(Object[] args) {
boolean narrowed = false;
for (int a = 0; a < args.length; ++a) {
Object arg = args[a];
if (arg instanceof Number) {
Object narg = narrow((Number) arg);
if (narg != arg) {
narrowed = true;
}
args[a] = narg;
}
}
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 first value
* @param right second value
* @return left + right.
*/
public Object add(Object left, Object right) {
if (left == null && right == null) {
return controlNullNullOperands();
}
try {
// if either are floating point (double or float) use double
if (isFloatingPointNumber(left) || isFloatingPointNumber(right)) {
double l = toDouble(left);
double r = toDouble(right);
return new Double(l + r);
}
// if both are bigintegers use that type
if (left instanceof BigInteger && right instanceof BigInteger) {
BigInteger l = toBigInteger(left);
BigInteger r = toBigInteger(right);
return l.add(r);
}
// if either are bigdecimal use that type
if (left instanceof BigDecimal || right instanceof BigDecimal) {
BigDecimal l = toBigDecimal(left);
BigDecimal r = toBigDecimal(right);
return l.add(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) {
// Well, use strings!
return toString(left).concat(toString(right));
}
}
/**
* Divide the left value by the right.
* @param left first value
* @param right second value
* @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 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 new Double(l / r);
}
// if both are bigintegers use that type
if (left instanceof BigInteger && right instanceof BigInteger) {
BigInteger l = toBigInteger(left);
BigInteger r = toBigInteger(right);
return l.divide(r);
}
// if either are bigdecimal use that type
if (left instanceof BigDecimal || right instanceof BigDecimal) {
BigDecimal l = toBigDecimal(left);
BigDecimal r = toBigDecimal(right);
BigDecimal d = l.divide(r);
return d;
}
// otherwise treat as integers
BigInteger l = toBigInteger(left);
BigInteger r = toBigInteger(right);
BigInteger result = l.divide(r);
return narrowBigInteger(left, right, result);
}
/**
* left value mod right.
* @param left first value
* @param right second value
* @return left mod right
* @throws ArithmeticException if right == 0.0
*/
public Object mod(Object left, Object right) {
if (left == null && right == null) {
return controlNullNullOperands();
}
// 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 new Double(l % r);
}
// if both are bigintegers use that type
if (left instanceof BigInteger && right instanceof BigInteger) {
BigInteger l = toBigInteger(left);
BigInteger r = toBigInteger(right);
return l.mod(r);
}
// if either are bigdecimal use that type
if (left instanceof BigDecimal || right instanceof BigDecimal) {
BigDecimal l = toBigDecimal(left);
BigDecimal r = toBigDecimal(right);
BigDecimal remainder = l.remainder(r);
return remainder;
}
// otherwise treat as integers
BigInteger l = toBigInteger(left);
BigInteger r = toBigInteger(right);
BigInteger result = l.mod(r);
return narrowBigInteger(left, right, result);
}
/**
* Multiply the left value by the right.
* @param left first value
* @param right second value
* @return left * right.
*/
public Object multiply(Object left, Object right) {
if (left == null && right == null) {
return controlNullNullOperands();
}
// if either are floating point (double or float) use double
if (isFloatingPointNumber(left) || isFloatingPointNumber(right)) {
double l = toDouble(left);
double r = toDouble(right);
return new Double(l * r);
}
// if both are bigintegers use that type
if (left instanceof BigInteger && right instanceof BigInteger) {
BigInteger l = toBigInteger(left);
BigInteger r = toBigInteger(right);
return l.multiply(r);
}
// if either are bigdecimal use that type
if (left instanceof BigDecimal || right instanceof BigDecimal) {
BigDecimal l = toBigDecimal(left);
BigDecimal r = toBigDecimal(right);
return l.multiply(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 first value
* @param right second value
* @return left - right.
*/
public Object subtract(Object left, Object right) {
if (left == null && right == null) {
return controlNullNullOperands();
}
// if either are floating point (double or float) use double
if (isFloatingPointNumber(left) || isFloatingPointNumber(right)) {
double l = toDouble(left);
double r = toDouble(right);
return new Double(l - r);
}
// if both are bigintegers use that type
if (left instanceof BigInteger && right instanceof BigInteger) {
BigInteger l = toBigInteger(left);
BigInteger r = toBigInteger(right);
return l.subtract(r);
}
// if either are bigdecimal use that type
if (left instanceof BigDecimal || right instanceof BigDecimal) {
BigDecimal l = toBigDecimal(left);
BigDecimal r = toBigDecimal(right);
return l.subtract(r);
}
// otherwise treat as integers
BigInteger l = toBigInteger(left);
BigInteger r = toBigInteger(right);
BigInteger result = l.subtract(r);
return narrowBigInteger(left, right, result);
}
/**
* Test if left regexp matches right.
*
* @param left first value
* @param right second value
* @return test result.
*/
public boolean matches(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;
}
final String arg = left.toString();
if (right instanceof java.util.regex.Pattern) {
return ((java.util.regex.Pattern) right).matcher(arg).matches();
} else {
return arg.matches(right.toString());
}
}
/**
* Test if left and right are equal.
*
* @param left first value
* @param right second value
* @return test result.
*/
public boolean equals(Object left, Object right) {
if (left == null && right == null) {
/*
* if both are null L == R
*/
return true;
} else if (left == null || right == null) {
/*
* we know both aren't null, therefore L != R
*/
return false;
} else if (left.getClass().equals(right.getClass())) {
return left.equals(right);
} else if (left instanceof BigDecimal || right instanceof BigDecimal) {
return toBigDecimal(left).compareTo(toBigDecimal(right)) == 0;
} else if (isFloatingPointType(left, right)) {
return toDouble(left) == toDouble(right);
} else if (left instanceof Number || right instanceof Number || left instanceof Character
|| right instanceof Character) {
return toLong(left) == toLong(right);
} else if (left instanceof Boolean || right instanceof Boolean) {
return toBoolean(left) == toBoolean(right);
} else if (left instanceof java.lang.String || right instanceof String) {
return left.toString().equals(right.toString());
}
return left.equals(right);
}
/**
* Test if left < right.
*
* @param left first value
* @param right second value
* @return test result.
*/
public boolean lessThan(Object left, Object right) {
if ((left == right) || (left == null) || (right == null)) {
return false;
} else if (isFloatingPoint(left) || isFloatingPoint(right)) {
double leftDouble = toDouble(left);
double rightDouble = toDouble(right);
return leftDouble < rightDouble;
} else if (left instanceof BigDecimal || right instanceof BigDecimal) {
BigDecimal l = toBigDecimal(left);
BigDecimal r = toBigDecimal(right);
return l.compareTo(r) < 0;
} else if (isNumberable(left) || isNumberable(right)) {
long leftLong = toLong(left);
long rightLong = toLong(right);
return leftLong < rightLong;
} else if (left instanceof String || right instanceof String) {
String leftString = left.toString();
String rightString = right.toString();
return leftString.compareTo(rightString) < 0;
} else if (left instanceof Comparable>) {
@SuppressWarnings("unchecked") // OK because of instanceof check above
final Comparable