
com.fathzer.soft.javaluator.AbstractEvaluator Maven / Gradle / Ivy
package com.fathzer.soft.javaluator;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Deque;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/** An abstract evaluator, able to evaluate infix expressions.
*
Some standard evaluators are included in the library, you can define your own by subclassing this class.
*
This class is thread safe.
* @param The type of values handled by the evaluator
* @author Jean-Marc Astesana
* @see License information
*/
public abstract class AbstractEvaluator {
private final Tokenizer tokenizer;
private final Map functions;
private final Map> operators;
private final Map constants;
private final String functionArgumentSeparator;
private final Map functionBrackets;
private final Map expressionBrackets;
/** Constructor.
* @param parameters The evaluator parameters.
*
Please note that there's no side effect between the evaluator and the parameters.
* So, changes made to the parameters after the call to this constructor are ignored by the instance.
*/
protected AbstractEvaluator(Parameters parameters) {
//TODO if constants, operators, functions are duplicated => error
final ArrayList tokenDelimitersBuilder = new ArrayList();
this.functions = new HashMap();
this.operators = new HashMap>();
this.constants = new HashMap();
this.functionBrackets = new HashMap();
for (final BracketPair pair : parameters.getFunctionBrackets()) {
functionBrackets.put(pair.getOpen(), pair);
functionBrackets.put(pair.getClose(), pair);
tokenDelimitersBuilder.add(pair.getOpen());
tokenDelimitersBuilder.add(pair.getClose());
}
this.expressionBrackets = new HashMap();
for (final BracketPair pair : parameters.getExpressionBrackets()) {
expressionBrackets.put(pair.getOpen(), pair);
expressionBrackets.put(pair.getClose(), pair);
tokenDelimitersBuilder.add(pair.getOpen());
tokenDelimitersBuilder.add(pair.getClose());
}
if (operators!=null) {
for (Operator ope : parameters.getOperators()) {
tokenDelimitersBuilder.add(ope.getSymbol());
List known = this.operators.get(ope.getSymbol());
if (known==null) {
known = new ArrayList();
this.operators.put(ope.getSymbol(), known);
}
known.add(ope);
if (known.size()>1) {
validateHomonyms(known);
}
}
}
boolean needFunctionSeparator = false;
if (parameters.getFunctions()!=null) {
for (Function function : parameters.getFunctions()) {
this.functions.put(parameters.getTranslation(function.getName()), function);
if (function.getMaximumArgumentCount()>1) {
needFunctionSeparator = true;
}
}
}
if (parameters.getConstants()!=null) {
for (Constant constant : parameters.getConstants()) {
this.constants.put(parameters.getTranslation(constant.getName()), constant);
}
}
functionArgumentSeparator = parameters.getFunctionArgumentSeparator();
if (needFunctionSeparator) {
tokenDelimitersBuilder.add(functionArgumentSeparator);
}
tokenizer = new Tokenizer(tokenDelimitersBuilder);
}
/** Validates that homonym operators are valid.
*
Homonym operators are operators with the same name (like the unary - and the binary - operators)
*
This method is called when homonyms are passed to the constructor.
*
This default implementation only allows the case where there's two operators, one binary and one unary.
* Subclasses can override this method in order to accept others configurations.
* @param operators The operators to validate.
* @throws IllegalArgumentException if the homonyms are not compatibles.
* @see #guessOperator(Token, List)
*/
protected void validateHomonyms(List operators) {
if (operators.size()>2) {
throw new IllegalArgumentException();
}
}
/** When a token can be more than one operator (homonym operators), this method guesses the right operator.
*
A very common case is the - sign in arithmetic computation which can be an unary or a binary operator, depending
* on what was the previous token.
*
Warning: maybe the arguments of this function are not enough to deal with all the cases.
* So, this part of the evaluation is in alpha state (method may change in the future).
* @param previous The last parsed tokens (the previous token in the infix expression we are evaluating).
* @param candidates The candidate tokens.
* @return A token
* @see #validateHomonyms(List)
*/
protected Operator guessOperator(Token previous, List candidates) {
final int argCount = ((previous!=null) && (previous.isCloseBracket() || previous.isLiteral())) ? 2 : 1;
for (Operator operator : candidates) {
if (operator.getOperandCount()==argCount) {
return operator;
}
}
return null;
}
@SuppressWarnings("unchecked")
private void output(Deque values, Token token, Object evaluationContext) {
if (token.isLiteral()) { // If the token is a literal, a constant, or a variable name
String literal = token.getLiteral();
Constant ct = this.constants.get(literal);
T value = ct==null?null:evaluate(ct, evaluationContext);
if (value==null && evaluationContext!=null && (evaluationContext instanceof AbstractVariableSet)) {
value = ((AbstractVariableSet)evaluationContext).get(literal);
}
values.push(value!=null ? value : toValue(literal, evaluationContext));
} else if (token.isOperator()) {
Operator operator = token.getOperator();
values.push(evaluate(operator, getArguments(values, operator.getOperandCount()), evaluationContext));
} else {
throw new IllegalArgumentException();
}
}
/** Evaluates a constant.
*
Subclasses that support constants must override this method.
* The default implementation throws a RuntimeException meaning that implementor forget to implement this method
* while creating a subclass that accepts constants.
* @param constant The constant
* @param evaluationContext The context of the evaluation
* @return The constant's value
*/
protected T evaluate(Constant constant, Object evaluationContext) {
throw new RuntimeException("evaluate(Constant) is not implemented for "+constant.getName());
}
/** Evaluates an operation.
*
Subclasses that support operators must override this method.
* The default implementation throws a RuntimeException meaning that implementor forget to implement this method
* while creating a subclass that accepts operators.
* @param operator The operator
* @param operands The operands
* @param evaluationContext The context of the evaluation
* @return The result of the operation
*/
protected T evaluate(Operator operator, Iterator operands, Object evaluationContext) {
throw new RuntimeException("evaluate(Operator, Iterator) is not implemented for "+operator.getSymbol());
}
/** Evaluates a function.
*
Subclasses that support functions must override this method.
* The default implementation throws a RuntimeException meaning that implementor forget to implement this method
* while creating a subclass that accepts functions.
* @param function The function
* @param arguments The function's arguments
* @param evaluationContext The context of the evaluation
* @return The result of the function
*/
protected T evaluate(Function function, Iterator arguments, Object evaluationContext) {
throw new RuntimeException("evaluate(Function, Iterator) is not implemented for "+function.getName());
}
private void doFunction(Deque values, Function function, int argCount, Object evaluationContext) {
if (function.getMinimumArgumentCount()>argCount || function.getMaximumArgumentCount() getArguments(Deque values, int nb) {
// Be aware that arguments are in reverse order on the values stack.
// Don't forget to reorder them in the original order (the one they appear in the evaluated formula)
if (values.size() result = new LinkedList();
for (int i = 0; i This context is an object that can contain useful dynamic data, for example the values of the variables
* used in the expression (Use an AbstractVariableSet to do that).
The context is not limited to variable values but
* can be used for any dynamic information. A good example is the BooleanSetEvaluator one.
* @return the result of the evaluation.
* @throws IllegalArgumentException if the expression is not correct.
* @see AbstractVariableSet
*/
public T evaluate(String expression, Object evaluationContext) {
final Deque values = new ArrayDeque(); // values stack
final Deque stack = new ArrayDeque(); // operator stack
final Deque previousValuesSize = functions.isEmpty()?null:new ArrayDeque();
final Iterator tokens = tokenize(expression);
Token previous = null;
while (tokens.hasNext()) {
// read one token from the input stream
String strToken = tokens.next();
final Token token = toToken(previous, strToken);
if (token.isOpenBracket()) {
// If the token is a left parenthesis, then push it onto the stack.
stack.push(token);
if (previous!=null && previous.isFunction()) {
if (!functionBrackets.containsKey(token.getBrackets().getOpen())) {
throw new IllegalArgumentException("Invalid bracket after function: "+strToken);
}
} else {
if (!expressionBrackets.containsKey(token.getBrackets().getOpen())) {
throw new IllegalArgumentException("Invalid bracket in expression: "+strToken);
}
}
} else if (token.isCloseBracket()) {
if (previous==null) {
throw new IllegalArgumentException("expression can't start with a close bracket");
}
if (previous.isFunctionArgumentSeparator()) {
throw new IllegalArgumentException("argument is missing");
}
BracketPair brackets = token.getBrackets();
// If the token is a right parenthesis:
boolean openBracketFound = false;
// Until the token at the top of the stack is a left parenthesis,
// pop operators off the stack onto the output queue
while (!stack.isEmpty()) {
Token sc = stack.pop();
if (sc.isOpenBracket()) {
if (sc.getBrackets().equals(brackets)) {
openBracketFound = true;
break;
} else {
throw new IllegalArgumentException("Invalid parenthesis match "+sc.getBrackets().getOpen()+brackets.getClose());
}
} else {
output(values, sc, evaluationContext);
}
}
if (!openBracketFound) {
// If the stack runs out without finding a left parenthesis, then
// there are mismatched parentheses.
throw new IllegalArgumentException("Parentheses mismatched");
}
if (!stack.isEmpty() && stack.peek().isFunction()) {
// If the token at the top of the stack is a function token, pop it
// onto the output queue.
int argCount = values.size()-previousValuesSize.pop();
doFunction(values, (Function)stack.pop().getFunction(), argCount, evaluationContext);
}
} else if (token.isFunctionArgumentSeparator()) {
if (previous==null) {
throw new IllegalArgumentException("expression can't start with a function argument separator");
}
// Verify that there was an argument before this separator
if (previous.isOpenBracket() || previous.isFunctionArgumentSeparator()) {
// The cases were operator miss an operand are detected elsewhere.
throw new IllegalArgumentException("argument is missing");
}
// If the token is a function argument separator
boolean pe = false;
while (!stack.isEmpty()) {
if (stack.peek().isOpenBracket()) {
pe = true;
break;
} else {
// Until the token at the top of the stack is a left parenthesis,
// pop operators off the stack onto the output queue.
output(values, stack.pop(), evaluationContext);
}
}
if (!pe) {
// If no left parentheses are encountered, either the separator was misplaced
// or parentheses were mismatched.
throw new IllegalArgumentException("Separator or parentheses mismatched");
} else {
// Verify we are in function scope
Token openBracket = stack.pop();
Token scopeToken = stack.peek();
stack.push(openBracket);
if (!scopeToken.isFunction()) {
throw new IllegalArgumentException("Argument separator used outside of function scope");
}
}
} else if (token.isFunction()) {
// If the token is a function token, then push it onto the stack.
stack.push(token);
previousValuesSize.push(values.size());
} else if (token.isOperator()) {
// If the token is an operator, op1, then:
while (!stack.isEmpty()) {
Token sc = stack.peek();
// While there is an operator token, o2, at the top of the stack
// op1 is left-associative and its precedence is less than or equal
// to that of op2,
// or op1 has precedence less than that of op2,
// Let + and ^ be right associative.
// Correct transformation from 1^2+3 is 12^3+
// The differing operator priority decides pop / push
// If 2 operators have equal priority then associativity decides.
if (sc.isOperator()
&& ((token.getAssociativity().equals(Operator.Associativity.LEFT) && (token.getPrecedence() <= sc.getPrecedence())) ||
(token.getPrecedence() < sc.getPrecedence()))) {
// Pop o2 off the stack, onto the output queue;
output(values, stack.pop(), evaluationContext);
} else {
break;
}
}
// push op1 onto the stack.
stack.push(token);
} else {
// If the token is a number (identifier), a constant or a variable, then add its value to the output queue.
if ((previous!=null) && previous.isLiteral()) {
throw new IllegalArgumentException("A literal can't follow another literal");
}
output(values, token, evaluationContext);
}
previous = token;
}
// When there are no more tokens to read:
// While there are still operator tokens in the stack:
while (!stack.isEmpty()) {
Token sc = stack.pop();
if (sc.isOpenBracket() || sc.isCloseBracket()) {
throw new IllegalArgumentException("Parentheses mismatched");
}
output(values, sc, evaluationContext);
}
if (values.size()!=1) {
throw new IllegalArgumentException();
}
return values.pop();
}
private Token toToken(Token previous, String token) {
if (token.equals(functionArgumentSeparator)) {
return Token.FUNCTION_ARG_SEPARATOR;
} else if (functions.containsKey(token)) {
return Token.buildFunction(functions.get(token));
} else if (operators.containsKey(token)) {
List list = operators.get(token);
return (list.size()==1) ? Token.buildOperator(list.get(0)) : Token.buildOperator(guessOperator(previous, list));
} else {
final BracketPair brackets = getBracketPair(token);
if (brackets!=null) {
if (brackets.getOpen().equals(token)) {
return Token.buildOpenToken(brackets);
} else {
return Token.buildCloseToken(brackets);
}
} else {
return Token.buildLiteral(token);
}
}
}
private BracketPair getBracketPair(String token) {
BracketPair result = expressionBrackets.get(token);
return result==null ? functionBrackets.get(token) : result;
}
/** Gets the operators supported by this evaluator.
* @return a collection of operators.
*/
public Collection getOperators() {
ArrayList result = new ArrayList();
Collection> values = this.operators.values();
for (List list : values) {
result.addAll(list);
}
return result;
}
/** Gets the functions supported by this evaluator.
* @return a collection of functions.
*/
public Collection getFunctions() {
return this.functions.values();
}
/** Gets the constants supported by this evaluator.
* @return a collection of constants.
*/
public Collection getConstants() {
return this.constants.values();
}
/** Converts the evaluated expression into tokens.
*
Example: The result for the expression "-1+min(10,3)" is an iterator on "-", "1", "+", "min", "(", "10", ",", "3", ")".
*
By default, the operators symbols, the brackets and the function argument separator are used as delimiter in the string.
* @param expression The expression that is evaluated
* @return A string iterator.
*/
protected Iterator tokenize(String expression) {
return tokenizer.tokenize(expression);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy