[FELIX-4671] Provide an expression parser
git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1632015 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/gogo/runtime/NOTICE b/gogo/runtime/NOTICE
index 6447d3e..cb48c52 100644
--- a/gogo/runtime/NOTICE
+++ b/gogo/runtime/NOTICE
@@ -4,3 +4,7 @@
This product includes software developed at
The Apache Software Foundation (http://www.apache.org/).
Licensed under the Apache License 2.0.
+
+This product includes software developped by
+Udo Klimaschewski (http://UdoJava.com/).
+Licensed under the MIT License.
diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Closure.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Closure.java
index 896c972..7dec8ec 100644
--- a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Closure.java
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Closure.java
@@ -19,11 +19,7 @@
package org.apache.felix.gogo.runtime;
import java.io.EOFException;
-import java.util.AbstractList;
-import java.util.ArrayList;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
import java.util.Map.Entry;
import org.apache.felix.gogo.runtime.Tokenizer.Type;
@@ -293,6 +289,10 @@
v = t.type;
break;
+ case EXPR:
+ v = expr(t.value);
+ break;
+
default:
throw new SyntaxError(t.line, t.column, "unexpected token: " + t.type);
}
@@ -534,6 +534,11 @@
return value;
}
+ private Object expr(CharSequence expr) throws Exception
+ {
+ return session.expr(expr);
+ }
+
private Object array(Token array) throws Exception
{
List<Token> list = new ArrayList<Token>();
diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/CommandProcessorImpl.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/CommandProcessorImpl.java
index 6f41986..e89878d 100644
--- a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/CommandProcessorImpl.java
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/CommandProcessorImpl.java
@@ -21,6 +21,7 @@
import java.io.InputStream;
import java.io.PrintStream;
import java.lang.reflect.Method;
+import java.math.BigDecimal;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@@ -356,4 +357,7 @@
}
}
+ public Object expr(CommandSessionImpl session, CharSequence expr) {
+ return new Expression(expr.toString()).eval(session.variables);
+ }
}
diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/CommandSessionImpl.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/CommandSessionImpl.java
index 63e4f98..ee3c09b 100644
--- a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/CommandSessionImpl.java
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/CommandSessionImpl.java
@@ -396,4 +396,9 @@
}
}
+ public Object expr(CharSequence expr)
+ {
+ return processor.expr(this, expr);
+ }
+
}
diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Expression.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Expression.java
new file mode 100644
index 0000000..b34c805
--- /dev/null
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Expression.java
@@ -0,0 +1,1332 @@
+/*
+ * 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.*;
+
+/**
+ * Enhanced to provide assignment operators and variables from a map, comparison operators, string operations and more.
+ */
+/**
+ * <h1>EvalEx - Java Expression Evaluator</h1>
+ *
+ * <h2>Introduction</h2>
+ * EvalEx is a handy expression evaluator for Java, that allows to evaluate simple mathematical and boolean expressions.
+ * <br>
+ * Key Features:
+ * <ul>
+ * <li>Uses BigDecimal for calculation and result</li>
+ * <li>Single class implementation, very compact</li>
+ * <li>No dependencies to external libraries</li>
+ * <li>Precision and rounding mode can be set</li>
+ * <li>Supports variables</li>
+ * <li>Standard boolean and mathematical operators</li>
+ * <li>Standard basic mathematical and boolean functions</li>
+ * <li>Custom functions and operators can be added at runtime</li>
+ * </ul>
+ * <br>
+ * <h2>Examples</h2>
+ * <pre>
+ * 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();
+ * </pre>
+ * <br>
+ * <h2>Supported Operators</h2>
+ * <table>
+ * <tr><th>Mathematical Operators</th></tr>
+ * <tr><th>Operator</th><th>Description</th></tr>
+ * <tr><td>+</td><td>Additive operator</td></tr>
+ * <tr><td>-</td><td>Subtraction operator</td></tr>
+ * <tr><td>*</td><td>Multiplication operator</td></tr>
+ * <tr><td>/</td><td>Division operator</td></tr>
+ * <tr><td>%</td><td>Remainder operator (Modulo)</td></tr>
+ * <tr><td>^</td><td>Power operator</td></tr>
+ * </table>
+ * <br>
+ * <table>
+ * <tr><th>Boolean Operators<sup>*</sup></th></tr>
+ * <tr><th>Operator</th><th>Description</th></tr>
+ * <tr><td>=</td><td>Equals</td></tr>
+ * <tr><td>==</td><td>Equals</td></tr>
+ * <tr><td>!=</td><td>Not equals</td></tr>
+ * <tr><td><></td><td>Not equals</td></tr>
+ * <tr><td><</td><td>Less than</td></tr>
+ * <tr><td><=</td><td>Less than or equal to</td></tr>
+ * <tr><td>></td><td>Greater than</td></tr>
+ * <tr><td>>=</td><td>Greater than or equal to</td></tr>
+ * <tr><td>&&</td><td>Boolean and</td></tr>
+ * <tr><td>||</td><td>Boolean or</td></tr>
+ * </table>
+ * *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.
+ * <br>
+ * <h2>Supported Functions</h2>
+ * <table>
+ * <tr><th>Function<sup>*</sup></th><th>Description</th></tr>
+ * <tr><td>NOT(<i>expression</i>)</td><td>Boolean negation, 1 (means true) if the expression is not zero</td></tr>
+ * <tr><td>IF(<i>condition</i>,<i>value_if_true</i>,<i>value_if_false</i>)</td><td>Returns one value if the condition evaluates to true or the other if it evaluates to false</td></tr>
+ * <tr><td>RANDOM()</td><td>Produces a random number between 0 and 1</td></tr>
+ * <tr><td>MIN(<i>e1</i>,<i>e2</i>)</td><td>Returns the smaller of both expressions</td></tr>
+ * <tr><td>MAX(<i>e1</i>,<i>e2</i>)</td><td>Returns the bigger of both expressions</td></tr>
+ * <tr><td>ABS(<i>expression</i>)</td><td>Returns the absolute (non-negative) value of the expression</td></tr>
+ * <tr><td>ROUND(<i>expression</i>,precision)</td><td>Rounds a value to a certain number of digits, uses the current rounding mode</td></tr>
+ * <tr><td>FLOOR(<i>expression</i>)</td><td>Rounds the value down to the nearest integer</td></tr>
+ * <tr><td>CEILING(<i>expression</i>)</td><td>Rounds the value up to the nearest integer</td></tr>
+ * <tr><td>LOG(<i>expression</i>)</td><td>Returns the natural logarithm (base e) of an expression</td></tr>
+ * <tr><td>SQRT(<i>expression</i>)</td><td>Returns the square root of an expression</td></tr>
+ * <tr><td>SIN(<i>expression</i>)</td><td>Returns the trigonometric sine of an angle (in degrees)</td></tr>
+ * <tr><td>COS(<i>expression</i>)</td><td>Returns the trigonometric cosine of an angle (in degrees)</td></tr>
+ * <tr><td>TAN(<i>expression</i>)</td><td>Returns the trigonometric tangens of an angle (in degrees)</td></tr>
+ * <tr><td>SINH(<i>expression</i>)</td><td>Returns the hyperbolic sine of a value</td></tr>
+ * <tr><td>COSH(<i>expression</i>)</td><td>Returns the hyperbolic cosine of a value</td></tr>
+ * <tr><td>TANH(<i>expression</i>)</td><td>Returns the hyperbolic tangens of a value</td></tr>
+ * <tr><td>RAD(<i>expression</i>)</td><td>Converts an angle measured in degrees to an approximately equivalent angle measured in radians</td></tr>
+ * <tr><td>DEG(<i>expression</i>)</td><td>Converts an angle measured in radians to an approximately equivalent angle measured in degrees</td></tr>
+ * </table>
+ * *Functions names are case insensitive.
+ * <br>
+ * <h2>Supported Constants</h2>
+ * <table>
+ * <tr><th>Constant</th><th>Description</th></tr>
+ * <tr><td>PI</td><td>The value of <i>PI</i>, exact to 100 digits</td></tr>
+ * <tr><td>TRUE</td><td>The value one</td></tr>
+ * <tr><td>FALSE</td><td>The value zero</td></tr>
+ * </table>
+ *
+ * <h2>Add Custom Operators</h2>
+ *
+ * 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.
+ * <br>
+ * For example, add an operator `x >> n`, that moves the decimal point of _x_ _n_ digits to the right:
+ *
+ * <pre>
+ * 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
+ * </pre>
+ * <br>
+ * <h2>Add Custom Functions</h2>
+ *
+ * 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.
+ * <br>
+ * For example, add a function `average(a,b,c)`, that will calculate the average value of a, b and c:
+ * <br>
+ * <pre>
+ * 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
+ * </pre>
+ * The software is licensed under the MIT Open Source license (see LICENSE file).
+ * <br>
+ * <ul>
+ * <li>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</li>
+ * <li>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)</li>
+ * </ul>
+ *
+ *@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<Token> rpn = null;
+
+ /**
+ * All defined operators with name and implementation.
+ */
+ private Map<String, Operator> operators = new HashMap<String, Expression.Operator>();
+
+ /**
+ * All defined functions with name and implementation.
+ */
+ private Map<String, Function> functions = new HashMap<String, Expression.Function>();
+
+ /**
+ * All defined variables with name and value.
+ */
+ private Map<String, Object> constants = new HashMap<String, Object>();
+
+ /**
+ * 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<String, Object> variables, List<Object> parameters) {
+ List<BigDecimal> numericParameters = new ArrayList<BigDecimal>(parameters.size());
+ 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<BigDecimal> 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
+ * <code>true</code> if the operator is left associative,
+ * else <code>false</code>.
+ */
+ 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<String, Object> 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 = (v1 instanceof Number || isNumber(v1.toString()))
+ && (v2 instanceof Number || isNumber(v2.toString()));
+ if (numeric) {
+ return eval(toBigDecimal(variables, v1), toBigDecimal(variables, v2));
+ } else {
+ return eval(v1.toString(), 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<String, Object> 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<String> {
+
+ /**
+ * Actual position in expression string.
+ */
+ private int pos = 0;
+
+ /**
+ * The original input expression.
+ */
+ private String input;
+ /**
+ * The previous token or <code>null</code> 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. <code>"2.4*sin(3)/(2-4)"</code> or
+ * <code>"sin(y)>0 & max(z, 3)>3"</code>
+ */
+ 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")
+ public boolean compare(Comparable v1, Comparable v2) {
+ return v1.compareTo(v2) > 0;
+ }
+ });
+
+ addOperator(new Comparator(">=", 10) {
+ @Override @SuppressWarnings("unchecked")
+ public boolean compare(Comparable v1, Comparable v2) {
+ return v1.compareTo(v2) >= 0;
+ }
+ });
+
+ addOperator(new Comparator("<", 10) {
+ @Override @SuppressWarnings("unchecked")
+ public boolean compare(Comparable v1, Comparable v2) {
+ return v1.compareTo(v2) < 0;
+ }
+ });
+
+ addOperator(new Comparator("<=", 10) {
+ @Override @SuppressWarnings("unchecked")
+ public boolean compare(Comparable v1, Comparable v2) {
+ return v1.compareTo(v2) <= 0;
+ }
+ });
+
+ addOperator(new Comparator("==", 7) {
+ @Override @SuppressWarnings("unchecked")
+ public boolean compare(Comparable v1, Comparable v2) {
+ return v1.compareTo(v2) == 0;
+ }
+ });
+
+ addOperator(new Comparator("!=", 7) {
+ @Override @SuppressWarnings("unchecked")
+ public boolean compare(Comparable v1, Comparable v2) {
+ return v1.compareTo(v2) != 0;
+ }
+ });
+
+ addFunction(new Function("NOT", 1) {
+ @Override
+ public BigDecimal eval(List<BigDecimal> 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<BigDecimal> 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<BigDecimal> parameters) {
+ double d = Math.random();
+ return new BigDecimal(d, mc);
+ }
+ });
+ addFunction(new Function("SIN", 1) {
+ @Override
+ public BigDecimal eval(List<BigDecimal> 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<BigDecimal> 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<BigDecimal> 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<BigDecimal> parameters) {
+ double d = Math.sinh(parameters.get(0).doubleValue());
+ return new BigDecimal(d, mc);
+ }
+ });
+ addFunction(new Function("COSH", 1) {
+ @Override
+ public BigDecimal eval(List<BigDecimal> parameters) {
+ double d = Math.cosh(parameters.get(0).doubleValue());
+ return new BigDecimal(d, mc);
+ }
+ });
+ addFunction(new Function("TANH", 1) {
+ @Override
+ public BigDecimal eval(List<BigDecimal> parameters) {
+ double d = Math.tanh(parameters.get(0).doubleValue());
+ return new BigDecimal(d, mc);
+ }
+ });
+ addFunction(new Function("RAD", 1) {
+ @Override
+ public BigDecimal eval(List<BigDecimal> parameters) {
+ double d = Math.toRadians(parameters.get(0).doubleValue());
+ return new BigDecimal(d, mc);
+ }
+ });
+ addFunction(new Function("DEG", 1) {
+ @Override
+ public BigDecimal eval(List<BigDecimal> parameters) {
+ double d = Math.toDegrees(parameters.get(0).doubleValue());
+ return new BigDecimal(d, mc);
+ }
+ });
+ addFunction(new Function("MAX", 2) {
+ @Override
+ public BigDecimal eval(List<BigDecimal> 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<BigDecimal> 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<BigDecimal> parameters) {
+ return parameters.get(0).abs(mc);
+ }
+ });
+ addFunction(new Function("LOG", 1) {
+ @Override
+ public BigDecimal eval(List<BigDecimal> parameters) {
+ double d = Math.log(parameters.get(0).doubleValue());
+ return new BigDecimal(d, mc);
+ }
+ });
+ addFunction(new Function("ROUND", 2) {
+ @Override
+ public BigDecimal eval(List<BigDecimal> 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<BigDecimal> parameters) {
+ BigDecimal toRound = parameters.get(0);
+ return toRound.setScale(0, RoundingMode.FLOOR);
+ }
+ });
+ addFunction(new Function("CEILING", 1) {
+ @Override
+ public BigDecimal eval(List<BigDecimal> parameters) {
+ BigDecimal toRound = parameters.get(0);
+ return toRound.setScale(0, RoundingMode.CEILING);
+ }
+ });
+ addFunction(new Function("SQRT", 1) {
+ @Override
+ public BigDecimal eval(List<BigDecimal> 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 <code>true</code>, if the input string is a number.
+ */
+ private boolean isNumber(String st) {
+ if (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;
+ }
+
+ /**
+ * Implementation of the <i>Shunting Yard</i> 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<Token> shuntingYard(String expression) {
+ List<Token> outputQueue = new ArrayList<Token>();
+ Stack<Token> stack = new Stack<Token>();
+
+ 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<String, Object>());
+ }
+
+ /**
+ * Evaluates the expression.
+ *
+ * @return The result of the expression.
+ */
+ public Object eval(Map<String, Object> variables) {
+
+ Stack<Object> stack = new Stack<Object>();
+
+ 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<Object> p = new ArrayList<Object>(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<String, Object> 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 <code>null</code> 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 <code>null</code> 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<String> 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<Token> 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();
+ }
+
+}
\ No newline at end of file
diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Parser.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Parser.java
index 271bb8a..23c2f65 100644
--- a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Parser.java
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Parser.java
@@ -97,6 +97,7 @@
case EXECUTION:
case ARRAY:
case ASSIGN:
+ case EXPR:
break;
default:
diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Tokenizer.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Tokenizer.java
index b1c529e..8b5d91f 100644
--- a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Tokenizer.java
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Tokenizer.java
@@ -46,7 +46,7 @@
{
public enum Type
{
- ASSIGN('='), PIPE('|'), SEMICOLON(';'), NEWLINE, ARRAY, CLOSURE, EXECUTION, WORD, EOT;
+ ASSIGN('='), PIPE('|'), SEMICOLON(';'), NEWLINE, ARRAY, CLOSURE, EXPR, EXECUTION, WORD, EOT;
private char c;
@@ -265,25 +265,43 @@
private CharSequence group()
{
final char push = ch;
+ final char push2;
final char pop;
+ final char pop2;
switch (ch)
{
case '{':
type = Type.CLOSURE;
+ push2 = 0;
pop = '}';
+ pop2 = 0;
break;
case '(':
- type = Type.EXECUTION;
- pop = ')';
+ if (peek() == '(') {
+ getch();
+ push2 = '(';
+ type = Type.EXPR;
+ pop = ')';
+ pop2 = ')';
+ } else {
+ type = Type.EXECUTION;
+ push2 = 0;
+ pop = ')';
+ pop2 = 0;
+ }
break;
case '[':
type = Type.ARRAY;
+ push2 = 0;
pop = ']';
+ pop2 = 0;
break;
default:
assert false;
+ push2 = 0;
pop = 0;
+ pop2 = 0;
}
short sLine = line;
@@ -327,10 +345,17 @@
break;
default:
- if (push == ch)
+ if (push == ch) {
depth++;
- else if (pop == ch && --depth == 0)
- return text.subSequence(start, index - 1);
+ }
+ else if (pop == ch && --depth == 0) {
+ if (pop2 == 0)
+ return text.subSequence(start, index - 1);
+ else if (pop2 == peek()) {
+ getch();
+ return text.subSequence(start, index - 2);
+ }
+ }
}
}
@@ -589,11 +614,20 @@
if (getch() != '{')
{
if ('(' == ch)
- { // support $(...) FELIX-2433
+ {
short sLine = line;
short sCol = column;
- val = evaluate.eval(new Token(Type.EXECUTION, group(), sLine, sCol));
- getch();
+ if ('(' == peek())
+ {
+ val = evaluate.eval(new Token(Type.EXPR, group(), sLine, sCol));
+ getch();
+ }
+ else
+ {
+ // support $(...) FELIX-2433
+ val = evaluate.eval(new Token(Type.EXECUTION, group(), sLine, sCol));
+ getch();
+ }
}
else
{
diff --git a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/Context.java b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/Context.java
index adbb66f..b96a313 100644
--- a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/Context.java
+++ b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/Context.java
@@ -68,4 +68,9 @@
session.put(name, value);
}
+ public Object get(String name)
+ {
+ return session.get(name);
+ }
+
}
diff --git a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestParser.java b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestParser.java
index 13c52f1..937ef80 100644
--- a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestParser.java
+++ b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestParser.java
@@ -46,7 +46,7 @@
assertEquals("a", c.execute("echo a | capture"));
assertEquals("a", c.execute("(echo a) | capture"));
- assertEquals("a", c.execute("((echo a)) | capture"));
+ assertEquals("a", c.execute("( (echo a) ) | capture"));
}
public void testUnknownCommand() throws Exception
diff --git a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestParser3.java b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestParser3.java
new file mode 100644
index 0000000..7e65c09
--- /dev/null
+++ b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestParser3.java
@@ -0,0 +1,61 @@
+/*
+ * 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.felix.gogo.runtime;
+
+import junit.framework.TestCase;
+
+import java.io.EOFException;
+
+/*
+ * Test features of the new parser/tokenizer, many of which are not supported
+ * by the original parser.
+ */
+public class TestParser3 extends TestCase
+{
+ public void testArithmetic() throws Exception
+ {
+ Context c = new Context();
+ c.addCommand("echo", this);
+
+ assertEquals("10", c.execute("echo $((2*(3+2)))"));
+ assertEquals(3l, c.execute("((1+2))"));
+
+ c.set("a", 2l);
+ assertEquals(3l, c.execute("((a+=1))"));
+ assertEquals(3l, c.get("a"));
+ }
+
+ public CharSequence echo(Object args[])
+ {
+ if (args == null)
+ {
+ return "null args!";
+ }
+
+ StringBuilder sb = new StringBuilder();
+ for (Object arg : args)
+ {
+ if (sb.length() > 0)
+ sb.append(' ');
+ sb.append(String.valueOf(arg));
+ }
+ return sb.toString();
+ }
+
+}
diff --git a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/expr/ExpressionTest.java b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/expr/ExpressionTest.java
new file mode 100644
index 0000000..bb16cd9
--- /dev/null
+++ b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/expr/ExpressionTest.java
@@ -0,0 +1,58 @@
+/*
+ * 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.felix.gogo.runtime.expr;
+
+import junit.framework.TestCase;
+import org.apache.felix.gogo.runtime.Expression;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class ExpressionTest extends TestCase {
+
+ public void testExpr() {
+
+ Map<String, Object> variables = new HashMap<String, Object>();
+ variables.put("a", 2d);
+ variables.put("b", 5l);
+ variables.put("c", 1l);
+ variables.put("d", 2l);
+ variables.put("s", " foo ");
+ variables.put("t", "bar");
+
+ assertEquals(4l, new Expression("c+=1, d+=2").eval(variables));
+
+ assertEquals(" foo ", new Expression("\" foo \"").eval());
+ assertEquals(" foo bar", new Expression("s + t").eval(variables));
+ assertEquals(1l, new Expression("s < t").eval(variables));
+ assertEquals(1l, new Expression("s > t || t == \"bar\"").eval(variables));
+
+ assertEquals(3l, new Expression("a += 1").eval(variables));
+ assertEquals(3l, variables.get("a"));
+
+ assertEquals(30l, new Expression("10 + 20 | 30").eval());
+
+ assertEquals(8l, new Expression("a + b").eval(variables));
+ assertEquals(3l, new Expression("if(a < b, a, b)").eval(variables));
+
+ assertEquals(16l, new Expression("2 + 2 << 2").eval());
+ assertEquals(8l, new Expression("2 | 2 << 2").eval());
+ }
+
+}