All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.felix.gogo.runtime.Expression Maven / Gradle / Ivy

There is a newer version: 1.1.6
Show newest version
/*
 * 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.
 */
/*
 * Copyright 2012 Udo Klimaschewski
 *
 * http://UdoJava.com/
 * http://about.me/udo.klimaschewski
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 */
package org.apache.felix.gogo.runtime;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.MathContext;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Stack;

/**
 * Enhanced to provide assignment operators and variables from a map, comparison operators, string operations and more.
 */
/**
 * 

EvalEx - Java Expression Evaluator

* *

Introduction

* EvalEx is a handy expression evaluator for Java, that allows to evaluate simple mathematical and boolean expressions. *
* Key Features: *
    *
  • Uses BigDecimal for calculation and result
  • *
  • Single class implementation, very compact
  • *
  • No dependencies to external libraries
  • *
  • Precision and rounding mode can be set
  • *
  • Supports variables
  • *
  • Standard boolean and mathematical operators
  • *
  • Standard basic mathematical and boolean functions
  • *
  • Custom functions and operators can be added at runtime
  • *
*
*

Examples

*
 *  BigDecimal result = null;
 *
 *  Expression expression = new Expression("1+1/3");
 *  result = expression.eval():
 *  expression.setPrecision(2);
 *  result = expression.eval():
 *
 *  result = new Expression("(3.4 + -4.1)/2").eval();
 *
 *  result = new Expression("SQRT(a^2 + b^2").with("a","2.4").and("b","9.253").eval();
 *
 *  BigDecimal a = new BigDecimal("2.4");
 *  BigDecimal b = new BigDecimal("9.235");
 *  result = new Expression("SQRT(a^2 + b^2").with("a",a).and("b",b).eval();
 *
 *  result = new Expression("2.4/PI").setPrecision(128).setRoundingMode(RoundingMode.UP).eval();
 *
 *  result = new Expression("random() > 0.5").eval();
 *
 *  result = new Expression("not(x < 7 || sqrt(max(x,9)) <= 3))").with("x","22.9").eval();
 * 
*
*

Supported Operators

* * * * * * * * * *
Mathematical Operators
OperatorDescription
+Additive operator
-Subtraction operator
*Multiplication operator
/Division operator
%Remainder operator (Modulo)
^Power operator
*
* * * * * * * * * * * * * *
Boolean Operators*
OperatorDescription
=Equals
==Equals
!=Not equals
<>Not equals
<Less than
<=Less than or equal to
>Greater than
>=Greater than or equal to
&&Boolean and
||Boolean or
* *Boolean operators result always in a BigDecimal value of 1 or 0 (zero). Any non-zero value is treated as a _true_ value. Boolean _not_ is implemented by a function. *
*

Supported Functions

* * * * * * * * * * * * * * * * * * * * * *
Function*Description
NOT(expression)Boolean negation, 1 (means true) if the expression is not zero
IF(condition,value_if_true,value_if_false)Returns one value if the condition evaluates to true or the other if it evaluates to false
RANDOM()Produces a random number between 0 and 1
MIN(e1,e2)Returns the smaller of both expressions
MAX(e1,e2)Returns the bigger of both expressions
ABS(expression)Returns the absolute (non-negative) value of the expression
ROUND(expression,precision)Rounds a value to a certain number of digits, uses the current rounding mode
FLOOR(expression)Rounds the value down to the nearest integer
CEILING(expression)Rounds the value up to the nearest integer
LOG(expression)Returns the natural logarithm (base e) of an expression
SQRT(expression)Returns the square root of an expression
SIN(expression)Returns the trigonometric sine of an angle (in degrees)
COS(expression)Returns the trigonometric cosine of an angle (in degrees)
TAN(expression)Returns the trigonometric tangens of an angle (in degrees)
SINH(expression)Returns the hyperbolic sine of a value
COSH(expression)Returns the hyperbolic cosine of a value
TANH(expression)Returns the hyperbolic tangens of a value
RAD(expression)Converts an angle measured in degrees to an approximately equivalent angle measured in radians
DEG(expression)Converts an angle measured in radians to an approximately equivalent angle measured in degrees
* *Functions names are case insensitive. *
*

Supported Constants

* * * * * *
ConstantDescription
PIThe value of PI, exact to 100 digits
TRUEThe value one
FALSEThe value zero
* *

Add Custom Operators

* * Custom operators can be added easily, simply create an instance of `Expression.Operator` and add it to the expression. * Parameters are the operator string, its precedence and if it is left associative. The operators `eval()` method will be called with the BigDecimal values of the operands. * All existing operators can also be overridden. *
* For example, add an operator `x >> n`, that moves the decimal point of _x_ _n_ digits to the right: * *
 * Expression e = new Expression("2.1234 >> 2");
 *
 * e.addOperator(e.new Operator(">>", 30, true) {
 *     {@literal @}Override
 *     public BigDecimal eval(BigDecimal v1, BigDecimal v2) {
 *         return v1.movePointRight(v2.toBigInteger().intValue());
 *     }
 * });
 *
 * e.eval(); // returns 212.34
 * 
*
*

Add Custom Functions

* * Adding custom functions is as easy as adding custom operators. Create an instance of `Expression.Function`and add it to the expression. * Parameters are the function name and the count of required parameters. The functions `eval()` method will be called with a list of the BigDecimal parameters. * All existing functions can also be overridden. *
* For example, add a function `average(a,b,c)`, that will calculate the average value of a, b and c: *
*
 * Expression e = new Expression("2 * average(12,4,8)");
 *
 * e.addFunction(e.new Function("average", 3) {
 *     {@literal @}Override
 *     public BigDecimal eval(List<BigDecimal> parameters) {
 *         BigDecimal sum = parameters.get(0).add(parameters.get(1)).add(parameters.get(2));
 *         return sum.divide(new BigDecimal(3));
 *     }
 * });
 *
 * e.eval(); // returns 16
 * 
* The software is licensed under the MIT Open Source license (see LICENSE file). *
*
    *
  • The *power of* operator (^) implementation was copied from [Stack Overflow](http://stackoverflow.com/questions/3579779/how-to-do-a-fractional-power-on-bigdecimal-in-java) Thanks to Gene Marin
  • *
  • The SQRT() function implementation was taken from the book [The Java Programmers Guide To numerical Computing](http://www.amazon.de/Java-Number-Cruncher-Programmers-Numerical/dp/0130460419) (Ronald Mak, 2002)
  • *
* *@author Udo Klimaschewski (http://about.me/udo.klimaschewski) */ public class Expression { /** * Definition of PI as a constant, can be used in expressions as variable. */ public static final BigDecimal PI = new BigDecimal( "3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679"); /** * The {@link MathContext} to use for calculations. */ private MathContext mc = MathContext.DECIMAL32; /** * The original infix expression. */ private String expression = null; /** * The cached RPN (Reverse Polish Notation) of the expression. */ private List rpn = null; /** * All defined operators with name and implementation. */ private Map operators = new HashMap<>(); /** * All defined functions with name and implementation. */ private Map functions = new HashMap<>(); /** * All defined variables with name and value. */ private Map constants = new HashMap<>(); /** * What character to use for decimal separators. */ private final char decimalSeparator = '.'; /** * What character to use for minus sign (negative values). */ private final char minusSign = '-'; /** * The expression evaluators exception class. */ public class ExpressionException extends RuntimeException { private static final long serialVersionUID = 1118142866870779047L; public ExpressionException(String message) { super(message); } } interface Token { } public class Constant implements Token { private final Object value; public Constant(Object value) { this.value = value; } public Object getValue() { return value; } } public class Variable implements Token { private final String name; public Variable(String name) { this.name = name; } public String getName() { return name; } @Override public String toString() { return name; } } public class LeftParen implements Token { @Override public String toString() { return "("; } } public class Comma implements Token { } /** * Abstract definition of a supported expression function. A function is * defined by a name, the number of parameters and the actual processing * implementation. */ public abstract class Function implements Token { /** * Name of this function. */ private String name; /** * Number of parameters expected for this function. */ private int numParams; /** * Creates a new function with given name and parameter count. * * @param name * The name of the function. * @param numParams * The number of parameters for this function. */ public Function(String name, int numParams) { this.name = name.toUpperCase(); this.numParams = numParams; } public String getName() { return name; } public int getNumParams() { return numParams; } public BigDecimal eval(Map variables, List parameters) { List numericParameters = new ArrayList<>(); for (Object o : parameters) { numericParameters.add(toBigDecimal(variables, o)); } return eval(numericParameters); } /** * Implementation for this function. * * @param parameters * Parameters will be passed by the expression evaluator as a * {@link List} of {@link BigDecimal} values. * @return The function must return a new {@link BigDecimal} value as a * computing result. */ public abstract BigDecimal eval(List parameters); @Override public String toString() { return name; } } /** * Abstract definition of a supported operator. An operator is defined by * its name (pattern), precedence and if it is left- or right associative. */ public abstract class Operator implements Token { /** * This operators name (pattern). */ private String oper; /** * Operators precedence. */ private int precedence; /** * Operator is left associative. */ private boolean leftAssoc; /** * Creates a new operator. * * @param oper * The operator name (pattern). * @param precedence * The operators precedence. * @param leftAssoc * true if the operator is left associative, * else false. */ public Operator(String oper, int precedence, boolean leftAssoc) { this.oper = oper; this.precedence = precedence; this.leftAssoc = leftAssoc; } public String getOper() { return oper; } public int getPrecedence() { return precedence; } public boolean isLeftAssoc() { return leftAssoc; } public Object eval(Map variables, Object v1, Object v2) { if (v1 instanceof Variable) { v1 = variables.get(((Variable) v1).getName()); } if (v2 instanceof Variable) { v2 = variables.get(((Variable) v2).getName()); } boolean numeric = isNumber(v1) && isNumber(v2); if (numeric) { return eval(toBigDecimal(variables, v1), toBigDecimal(variables, v2)); } else { return eval(v1 != null ? v1.toString() : "", v2 != null ? v2.toString() : ""); } } public Object eval(String v1, String v2) { return eval(toBigDecimal(v1), toBigDecimal(v2)); } /** * Implementation for this operator. * * @param v1 * Operand 1. * @param v2 * Operand 2. * @return The result of the operation. */ public abstract BigDecimal eval(BigDecimal v1, BigDecimal v2); @Override public String toString() { return oper; } } public abstract class Comparator extends Operator { public Comparator(String oper, int precedence) { super(oper, precedence, false); } @Override public BigDecimal eval(BigDecimal v1, BigDecimal v2) { return compare(v1, v2) ? BigDecimal.ONE : BigDecimal.ZERO; } @Override public Object eval(String v1, String v2) { return compare(v1, v2) ? BigDecimal.ONE : BigDecimal.ZERO; } /** * This method actually implements the comparison. * It will be called with either 2 BigIntegers or 2 Strings. * * @param v1 * Operand 1. * @param v2 * Operand 2. * @return The result of the comparison. */ public abstract boolean compare(Comparable v1, Comparable v2); } /** * Marker class for assignment operators. * Those operators need a variable on the left hand side. */ public abstract class Assignment extends Operator { public Assignment(String assign, int precedence) { super(assign, precedence, true); } public Object eval(Map variables, Object v1, Object v2) { if (!(v1 instanceof Variable)) { throw new IllegalArgumentException("Left hand side of operator " + getOper() + " should be a variable but found " + v1.toString()); } String name = ((Variable) v1).getName(); Object r = super.eval(variables, v1, v2); if (r instanceof Number) { r = toResult(toBigDecimal(r)); } variables.put(name, r); return r; } } /** * Expression tokenizer that allows to iterate over a {@link String} * expression token by token. Blank characters will be skipped. */ private class Tokenizer implements Iterator { /** * Actual position in expression string. */ private int pos = 0; /** * The original input expression. */ private String input; /** * The previous token or null if none. */ private String previousToken; /** * Creates a new tokenizer for an expression. * * @param input * The expression string. */ public Tokenizer(String input) { this.input = input; } public boolean hasNext() { return (pos < input.length()); } /** * Peek at the next character, without advancing the iterator. * * @return The next character or character 0, if at end of string. */ private char peekNextChar() { if (pos < (input.length() - 1)) { return input.charAt(pos + 1); } else { return 0; } } public String next() { StringBuilder token = new StringBuilder(); if (pos >= input.length()) { return previousToken = null; } char ch = input.charAt(pos); while (Character.isWhitespace(ch) && pos < input.length()) { ch = input.charAt(++pos); } if (Character.isDigit(ch)) { while ((Character.isDigit(ch) || ch == decimalSeparator) && (pos < input.length())) { token.append(input.charAt(pos++)); ch = pos == input.length() ? 0 : input.charAt(pos); } } else if (ch == minusSign && Character.isDigit(peekNextChar()) && ("(".equals(previousToken) || ",".equals(previousToken) || previousToken == null || operators .containsKey(previousToken))) { token.append(minusSign); pos++; token.append(next()); } else if (Character.isLetter(ch)) { while ((Character.isLetter(ch) || Character.isDigit(ch) || (ch == '_')) && (pos < input.length())) { token.append(input.charAt(pos++)); ch = pos == input.length() ? 0 : input.charAt(pos); } } else if (ch == '"') { boolean escaped = false; token.append(input.charAt(pos++)); do { if (pos == input.length()) { throw new IllegalArgumentException("Non terminated quote"); } ch = input.charAt(pos++); escaped = (!escaped && ch == '\\'); token.append(ch); } while (escaped || ch != '"'); } else if (ch == '(' || ch == ')' || ch == ',') { token.append(ch); pos++; } else { while (!Character.isLetter(ch) && !Character.isDigit(ch) && !Character.isWhitespace(ch) && ch != '(' && ch != ')' && ch != ',' && (pos < input.length())) { token.append(input.charAt(pos)); pos++; ch = pos == input.length() ? 0 : input.charAt(pos); if (ch == minusSign) { break; } } if (!operators.containsKey(token.toString())) { throw new ExpressionException("Unknown operator '" + token + "' at position " + (pos - token.length() + 1)); } } return previousToken = token.toString(); } public void remove() { throw new ExpressionException("remove() not supported"); } /** * Get the actual character position in the string. * * @return The actual character position. */ public int getPos() { return pos; } } /** * Creates a new expression instance from an expression string. * * @param expression * The expression. E.g. "2.4*sin(3)/(2-4)" or * "sin(y)>0 & max(z, 3)>3" */ public Expression(String expression) { this.expression = expression; addOperator(new Assignment("=", 5) { @Override public BigDecimal eval(BigDecimal v1, BigDecimal v2) { return v2; } }); addOperator(new Assignment("+=", 5) { @Override public BigDecimal eval(BigDecimal v1, BigDecimal v2) { return v1.add(v2, mc); } @Override public Object eval(String v1, String v2) { return v1 + v2; } }); addOperator(new Assignment("-=", 5) { @Override public BigDecimal eval(BigDecimal v1, BigDecimal v2) { return v1.subtract(v2, mc); } }); addOperator(new Assignment("*=", 5) { @Override public BigDecimal eval(BigDecimal v1, BigDecimal v2) { return v1.multiply(v2, mc); } }); addOperator(new Assignment("/=", 5) { @Override public BigDecimal eval(BigDecimal v1, BigDecimal v2) { return v1.divide(v2, mc); } }); addOperator(new Assignment("%=", 5) { @Override public BigDecimal eval(BigDecimal v1, BigDecimal v2) { return v1.remainder(v2, mc); } }); addOperator(new Assignment("|=", 5) { @Override public BigDecimal eval(BigDecimal v1, BigDecimal v2) { return new BigDecimal(v1.toBigInteger().or(v2.toBigInteger()), mc); } }); addOperator(new Assignment("&=", 5) { @Override public BigDecimal eval(BigDecimal v1, BigDecimal v2) { return new BigDecimal(v1.toBigInteger().and(v2.toBigInteger()), mc); } }); addOperator(new Assignment("^=", 5) { @Override public BigDecimal eval(BigDecimal v1, BigDecimal v2) { return new BigDecimal(v1.toBigInteger().xor(v2.toBigInteger()), mc); } }); addOperator(new Assignment("<<=", 5) { @Override public BigDecimal eval(BigDecimal v1, BigDecimal v2) { return new BigDecimal(v1.toBigInteger().shiftLeft(v2.intValue()), mc); } }); addOperator(new Assignment(">>=", 5) { @Override public BigDecimal eval(BigDecimal v1, BigDecimal v2) { return new BigDecimal(v1.toBigInteger().shiftRight(v2.intValue()), mc); } }); addOperator(new Operator("<<", 10, true) { @Override public BigDecimal eval(BigDecimal v1, BigDecimal v2) { return new BigDecimal(v1.toBigInteger().shiftLeft(v2.intValue()), mc); } }); addOperator(new Operator(">>", 10, true) { @Override public BigDecimal eval(BigDecimal v1, BigDecimal v2) { return new BigDecimal(v1.toBigInteger().shiftRight(v2.intValue()), mc); } }); addOperator(new Operator("|", 15, true) { @Override public BigDecimal eval(BigDecimal v1, BigDecimal v2) { return new BigDecimal(v1.toBigInteger().or(v2.toBigInteger()), mc); } }); addOperator(new Operator("&", 15, true) { @Override public BigDecimal eval(BigDecimal v1, BigDecimal v2) { return new BigDecimal(v1.toBigInteger().and(v2.toBigInteger()), mc); } }); addOperator(new Operator("^", 15, true) { @Override public BigDecimal eval(BigDecimal v1, BigDecimal v2) { return new BigDecimal(v1.toBigInteger().xor(v2.toBigInteger()), mc); } }); addOperator(new Operator("+", 20, true) { @Override public BigDecimal eval(BigDecimal v1, BigDecimal v2) { return v1.add(v2, mc); } @Override public Object eval(String v1, String v2) { return v1 + v2; } }); addOperator(new Operator("-", 20, true) { @Override public BigDecimal eval(BigDecimal v1, BigDecimal v2) { return v1.subtract(v2, mc); } }); addOperator(new Operator("*", 30, true) { @Override public BigDecimal eval(BigDecimal v1, BigDecimal v2) { return v1.multiply(v2, mc); } }); addOperator(new Operator("/", 30, true) { @Override public BigDecimal eval(BigDecimal v1, BigDecimal v2) { return v1.divide(v2, mc); } }); addOperator(new Operator("%", 30, true) { @Override public BigDecimal eval(BigDecimal v1, BigDecimal v2) { return v1.remainder(v2, mc); } }); addOperator(new Operator("**", 40, false) { @Override public BigDecimal eval(BigDecimal v1, BigDecimal v2) { /*- * Thanks to Gene Marin: * http://stackoverflow.com/questions/3579779/how-to-do-a-fractional-power-on-bigdecimal-in-java */ int signOf2 = v2.signum(); double dn1 = v1.doubleValue(); v2 = v2.multiply(new BigDecimal(signOf2)); // n2 is now positive BigDecimal remainderOf2 = v2.remainder(BigDecimal.ONE); BigDecimal n2IntPart = v2.subtract(remainderOf2); BigDecimal intPow = v1.pow(n2IntPart.intValueExact(), mc); BigDecimal doublePow = new BigDecimal(Math.pow(dn1, remainderOf2.doubleValue())); BigDecimal result = intPow.multiply(doublePow, mc); if (signOf2 == -1) { result = BigDecimal.ONE.divide(result, mc.getPrecision(), RoundingMode.HALF_UP); } return result; } }); addOperator(new Operator("&&", 4, false) { @Override public BigDecimal eval(BigDecimal v1, BigDecimal v2) { boolean b1 = !v1.equals(BigDecimal.ZERO); boolean b2 = !v2.equals(BigDecimal.ZERO); return b1 && b2 ? BigDecimal.ONE : BigDecimal.ZERO; } }); addOperator(new Operator("||", 2, false) { @Override public BigDecimal eval(BigDecimal v1, BigDecimal v2) { boolean b1 = !v1.equals(BigDecimal.ZERO); boolean b2 = !v2.equals(BigDecimal.ZERO); return b1 || b2 ? BigDecimal.ONE : BigDecimal.ZERO; } }); addOperator(new Comparator(">", 10) { @Override @SuppressWarnings({ "unchecked", "rawtypes" }) public boolean compare(Comparable v1, Comparable v2) { return v1.compareTo(v2) > 0; } }); addOperator(new Comparator(">=", 10) { @Override @SuppressWarnings({ "unchecked", "rawtypes" }) public boolean compare(Comparable v1, Comparable v2) { return v1.compareTo(v2) >= 0; } }); addOperator(new Comparator("<", 10) { @Override @SuppressWarnings({ "unchecked", "rawtypes" }) public boolean compare(Comparable v1, Comparable v2) { return v1.compareTo(v2) < 0; } }); addOperator(new Comparator("<=", 10) { @Override @SuppressWarnings({ "unchecked", "rawtypes" }) public boolean compare(Comparable v1, Comparable v2) { return v1.compareTo(v2) <= 0; } }); addOperator(new Comparator("==", 7) { @Override @SuppressWarnings({ "unchecked", "rawtypes" }) public boolean compare(Comparable v1, Comparable v2) { return v1.compareTo(v2) == 0; } }); addOperator(new Comparator("!=", 7) { @Override @SuppressWarnings({ "unchecked", "rawtypes" }) public boolean compare(Comparable v1, Comparable v2) { return v1.compareTo(v2) != 0; } }); addFunction(new Function("NOT", 1) { @Override public BigDecimal eval(List parameters) { boolean zero = parameters.get(0).compareTo(BigDecimal.ZERO) == 0; return zero ? BigDecimal.ONE : BigDecimal.ZERO; } }); addFunction(new Function("IF", 3) { @Override public BigDecimal eval(List parameters) { boolean isTrue = !parameters.get(0).equals(BigDecimal.ZERO); return isTrue ? parameters.get(1) : parameters.get(2); } }); addFunction(new Function("RANDOM", 0) { @Override public BigDecimal eval(List parameters) { double d = Math.random(); return new BigDecimal(d, mc); } }); addFunction(new Function("SIN", 1) { @Override public BigDecimal eval(List parameters) { double d = Math.sin(Math.toRadians(parameters.get(0) .doubleValue())); return new BigDecimal(d, mc); } }); addFunction(new Function("COS", 1) { @Override public BigDecimal eval(List parameters) { double d = Math.cos(Math.toRadians(parameters.get(0) .doubleValue())); return new BigDecimal(d, mc); } }); addFunction(new Function("TAN", 1) { @Override public BigDecimal eval(List parameters) { double d = Math.tan(Math.toRadians(parameters.get(0) .doubleValue())); return new BigDecimal(d, mc); } }); addFunction(new Function("SINH", 1) { @Override public BigDecimal eval(List parameters) { double d = Math.sinh(parameters.get(0).doubleValue()); return new BigDecimal(d, mc); } }); addFunction(new Function("COSH", 1) { @Override public BigDecimal eval(List parameters) { double d = Math.cosh(parameters.get(0).doubleValue()); return new BigDecimal(d, mc); } }); addFunction(new Function("TANH", 1) { @Override public BigDecimal eval(List parameters) { double d = Math.tanh(parameters.get(0).doubleValue()); return new BigDecimal(d, mc); } }); addFunction(new Function("RAD", 1) { @Override public BigDecimal eval(List parameters) { double d = Math.toRadians(parameters.get(0).doubleValue()); return new BigDecimal(d, mc); } }); addFunction(new Function("DEG", 1) { @Override public BigDecimal eval(List parameters) { double d = Math.toDegrees(parameters.get(0).doubleValue()); return new BigDecimal(d, mc); } }); addFunction(new Function("MAX", 2) { @Override public BigDecimal eval(List parameters) { BigDecimal v1 = parameters.get(0); BigDecimal v2 = parameters.get(1); return v1.compareTo(v2) > 0 ? v1 : v2; } }); addFunction(new Function("MIN", 2) { @Override public BigDecimal eval(List parameters) { BigDecimal v1 = parameters.get(0); BigDecimal v2 = parameters.get(1); return v1.compareTo(v2) < 0 ? v1 : v2; } }); addFunction(new Function("ABS", 1) { @Override public BigDecimal eval(List parameters) { return parameters.get(0).abs(mc); } }); addFunction(new Function("LOG", 1) { @Override public BigDecimal eval(List parameters) { double d = Math.log(parameters.get(0).doubleValue()); return new BigDecimal(d, mc); } }); addFunction(new Function("ROUND", 2) { @Override public BigDecimal eval(List parameters) { BigDecimal toRound = parameters.get(0); int precision = parameters.get(1).intValue(); return toRound.setScale(precision, mc.getRoundingMode()); } }); addFunction(new Function("FLOOR", 1) { @Override public BigDecimal eval(List parameters) { BigDecimal toRound = parameters.get(0); return toRound.setScale(0, RoundingMode.FLOOR); } }); addFunction(new Function("CEILING", 1) { @Override public BigDecimal eval(List parameters) { BigDecimal toRound = parameters.get(0); return toRound.setScale(0, RoundingMode.CEILING); } }); addFunction(new Function("SQRT", 1) { @Override public BigDecimal eval(List parameters) { /* * From The Java Programmers Guide To numerical Computing * (Ronald Mak, 2003) */ BigDecimal x = parameters.get(0); if (x.compareTo(BigDecimal.ZERO) == 0) { return new BigDecimal(0); } if (x.signum() < 0) { throw new ExpressionException( "Argument to SQRT() function must not be negative"); } BigInteger n = x.movePointRight(mc.getPrecision() << 1) .toBigInteger(); int bits = (n.bitLength() + 1) >> 1; BigInteger ix = n.shiftRight(bits); BigInteger ixPrev; do { ixPrev = ix; ix = ix.add(n.divide(ix)).shiftRight(1); // Give other threads a chance to work; Thread.yield(); } while (ix.compareTo(ixPrev) != 0); return new BigDecimal(ix, mc.getPrecision()); } }); constants.put("PI", PI); constants.put("TRUE", Boolean.TRUE); constants.put("FALSE", Boolean.FALSE); } /** * Is the string a number? * * @param st * The string. * @return true, if the input string is a number. */ private boolean isNumber(String st) { if (st == null || st.isEmpty() || st.charAt(0) == minusSign && st.length() == 1) return false; for (char ch : st.toCharArray()) { if (!Character.isDigit(ch) && ch != minusSign && ch != decimalSeparator) return false; } return true; } private boolean isNumber(Object obj) { if (obj instanceof Number) { return true; } else if (obj != null) { return isNumber(obj.toString()); } else { return false; } } /** * Implementation of the Shunting Yard algorithm to transform an * infix expression to a RPN expression. * * @param expression * The input expression in infx. * @return A RPN representation of the expression, with each token as a list * member. */ private List shuntingYard(String expression) { List outputQueue = new ArrayList<>(); Stack stack = new Stack<>(); Tokenizer tokenizer = new Tokenizer(expression); String previousToken = null; while (tokenizer.hasNext()) { String token = tokenizer.next(); if (token.charAt(0) == '"') { StringBuilder sb = new StringBuilder(); boolean escaped = false; for (int i = 1; i < token.length() - 1; i++) { char ch = token.charAt(i); if (escaped || ch != '\\') { sb.append(ch); } else { escaped = true; } } outputQueue.add(new Constant(sb.toString())); } else if (isNumber(token)) { outputQueue.add(new Constant(toBigDecimal(token))); } else if (constants.containsKey(token)) { outputQueue.add(new Constant(constants.get(token))); } else if (functions.containsKey(token.toUpperCase())) { stack.push(functions.get(token.toUpperCase())); } else if (Character.isLetter(token.charAt(0))) { outputQueue.add(new Variable(token)); } else if (",".equals(token)) { while (!stack.isEmpty() && !(stack.peek() instanceof LeftParen)) { outputQueue.add(stack.pop()); } if (stack.isEmpty()) { outputQueue.add(new Comma()); } } else if (operators.containsKey(token)) { Operator o1 = operators.get(token); Token token2 = stack.isEmpty() ? null : stack.peek(); while (token2 instanceof Operator && ((o1.isLeftAssoc() && o1.getPrecedence() <= ((Operator) token2).getPrecedence()) || (o1.getPrecedence() < ((Operator) token2).getPrecedence()))) { outputQueue.add(stack.pop()); token2 = stack.isEmpty() ? null : stack.peek(); } stack.push(o1); } else if ("(".equals(token)) { if (previousToken != null) { if (isNumber(previousToken)) { throw new ExpressionException("Missing operator at character position " + tokenizer.getPos()); } } stack.push(new LeftParen()); } else if (")".equals(token)) { while (!stack.isEmpty() && !(stack.peek() instanceof LeftParen)) { outputQueue.add(stack.pop()); } if (stack.isEmpty()) { throw new RuntimeException("Mismatched parentheses"); } stack.pop(); if (!stack.isEmpty() && stack.peek() instanceof Function) { outputQueue.add(stack.pop()); } } previousToken = token; } while (!stack.isEmpty()) { Token element = stack.pop(); if (element instanceof LeftParen) { throw new RuntimeException("Mismatched parentheses"); } if (!(element instanceof Operator)) { throw new RuntimeException("Unknown operator or function: " + element); } outputQueue.add(element); } return outputQueue; } /** * Evaluates the expression. * * @return The result of the expression. */ public Object eval() { return eval(new HashMap<>()); } /** * Evaluates the expression. * * @param variables the variables * @return The result of the expression. */ public Object eval(Map variables) { Stack stack = new Stack<>(); for (Token token : getRPN()) { if (token instanceof Operator) { Object v1 = stack.pop(); Object v2 = stack.pop(); Object oResult = ((Operator) token).eval(variables, v2, v1); stack.push(oResult); } else if (token instanceof Constant) { stack.push(((Constant) token).getValue()); } else if (token instanceof Function) { Function f = (Function) token; List p = new ArrayList<>(f.getNumParams()); for (int i = 0; i < f.numParams; i++) { p.add(0, stack.pop()); } Object fResult = f.eval(variables, p); stack.push(fResult); } else if (token instanceof Comma) { stack.pop(); } else { stack.push(token); } } if (stack.size() > 1) { throw new IllegalArgumentException("Missing operator"); } Object result = stack.pop(); if (result instanceof Variable) { result = variables.get(((Variable) result).getName()); } if (result instanceof BigDecimal) { result = toResult((BigDecimal) result); } return result; } private Number toResult(BigDecimal r) { long l = r.longValue(); if (new BigDecimal(l).compareTo(r) == 0) { return l; } double d = r.doubleValue(); if (new BigDecimal(d).compareTo(r) == 0) { return d; } else { return r.stripTrailingZeros(); } } private BigDecimal toBigDecimal(Map variables, Object o) { if (o instanceof Variable) { o = variables.get(((Variable) o).getName()); } if (o instanceof String) { if (isNumber((String) o)) { return new BigDecimal((String) o, mc); } else if (Character.isLetter(((String) o).charAt(0))) { o = variables.get(o); } } return toBigDecimal(o); } private BigDecimal toBigDecimal(Object o) { if (o == null) { return BigDecimal.ZERO; } else if (o instanceof Boolean) { return ((Boolean) o) ? BigDecimal.ONE : BigDecimal.ZERO; } else if (o instanceof BigDecimal) { return ((BigDecimal) o).round(mc); } else if (o instanceof BigInteger) { return new BigDecimal((BigInteger) o, mc); } else if (o instanceof Number) { return new BigDecimal(((Number) o).doubleValue(), mc); } else { try { return new BigDecimal(o.toString(), mc); } catch (NumberFormatException e) { return new BigDecimal(Double.NaN); } } } /** * Sets the precision for expression evaluation. * * @param precision * The new precision. * * @return The expression, allows to chain methods. */ public Expression setPrecision(int precision) { this.mc = new MathContext(precision); return this; } /** * Sets the rounding mode for expression evaluation. * * @param roundingMode * The new rounding mode. * @return The expression, allows to chain methods. */ public Expression setRoundingMode(RoundingMode roundingMode) { this.mc = new MathContext(mc.getPrecision(), roundingMode); return this; } /** * Adds an operator to the list of supported operators. * * @param operator * The operator to add. * @return The previous operator with that name, or null if * there was none. */ public Operator addOperator(Operator operator) { return operators.put(operator.getOper(), operator); } /** * Adds a function to the list of supported functions * * @param function * The function to add. * @return The previous operator with that name, or null if * there was none. */ public Function addFunction(Function function) { return functions.put(function.getName(), function); } /** * Sets a constant value. * * @param name * The constant name. * @param value * The constant value. * @return The expression, allows to chain methods. */ public Expression addConstant(String name, Object value) { constants.put(name, value); return this; } /** * Get an iterator for this expression, allows iterating over an expression * token by token. * * @return A new iterator instance for this expression. */ public Iterator getExpressionTokenizer() { return new Tokenizer(this.expression); } /** * Cached access to the RPN notation of this expression, ensures only one * calculation of the RPN per expression instance. If no cached instance * exists, a new one will be created and put to the cache. * * @return The cached RPN instance. */ private List getRPN() { if (rpn == null) { rpn = shuntingYard(this.expression); } return rpn; } /** * Get a string representation of the RPN (Reverse Polish Notation) for this * expression. * * @return A string with the RPN representation for this expression. */ public String toRPN() { StringBuilder result = new StringBuilder(); for (Token st : getRPN()) { if (result.length() > 0) { result.append(" "); } result.append(st); } return result.toString(); } }