Expression.java
/*
* Copyright 2014 Frank Asseg
*
* Licensed 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 net.objecthunter.exp4j;
import java.io.Serial;
import java.io.Serializable;
import net.objecthunter.exp4j.function.Function;
import net.objecthunter.exp4j.function.Functions;
import net.objecthunter.exp4j.operator.Operator;
import net.objecthunter.exp4j.tokenizer.FunctionToken;
import net.objecthunter.exp4j.tokenizer.NumberToken;
import net.objecthunter.exp4j.tokenizer.OperatorToken;
import net.objecthunter.exp4j.tokenizer.Token;
import net.objecthunter.exp4j.tokenizer.VariableToken;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import static net.objecthunter.exp4j.tokenizer.TokenType.*;
import static net.objecthunter.exp4j.utils.Text.l10n;
/**
* This class represents a valid mathematical expression.
*
* @author Frank Asseg
* @see ExpressionBuilder#build()
*/
public final class Expression implements Serializable {
@Serial
private static final long serialVersionUID = -2510794384846712749L;
private final Token[] tokens;
private final String[] userFunctionNames;
private final Map<String, VariableToken> variables = new TreeMap<>();
private final boolean cacheResult;
private Double result;
/**
* Creates a new expression that is a copy of the existing one.
*
* @return copy of this {@code Expression}
*/
public Expression copy() {
Expression exp = new Expression(
Arrays.copyOf(tokens, tokens.length),
Arrays.copyOf(userFunctionNames, userFunctionNames.length)
);
exp.variables.clear();
//Since I don't honor the immutable token philosophy I need to copy
//variable tokens... Still... I regret nothing!
for (int i = 0; i < exp.tokens.length; i++) {
if (exp.tokens[i].getType() == VARIABLE) {
final VariableToken v = ((VariableToken)exp.tokens[i]);
if (!exp.variables.containsKey(v.getName())) {
exp.variables.put(v.getName(), v.copy());
}
exp.tokens[i] = exp.variables.get(v.getName());
}
}
return exp;
}
Expression(final Token[] tokens, String[] userFunctionNames) {
this.tokens = tokens;
this.userFunctionNames = userFunctionNames;
populateVariablesMap();
cacheResult = checkNonDeterministic(tokens, userFunctionNames.length);
}
/**
* Tells if the result is being cached.
*
* @return {@code true} if the result will be cached and {@code false}
* otherwise
*/
boolean isCachingResult() {
return cacheResult;
}
private boolean checkNonDeterministic(Token[] tokens, int userFuncs) {
if (userFuncs == 0) {
return true;
}
boolean status = false;
for (Token t : tokens) {
status |= (t.getType() == FUNCTION &&
!((FunctionToken)t).getFunction().isDeterministic());
}
return !status;
}
private void populateVariablesMap() {
for (final Token t: tokens) {
if (t.getType() == VARIABLE) {
variables.put(((VariableToken)t).getName(), (VariableToken)t);
}
}
}
/**
* Sets the value of a variable, the variable to set must exist at build time and can't be the
* name of a function.
* All variables must be set before calling {@link Expression#evaluate()}
*
* @param name variable name as passed to {@link ExpressionBuilder}
* @param value value of the variable
* @return {@code this}
* @throws IllegalArgumentException if the variable name is a function name or if the variable
* doesn't exist at build time.
* @see ExpressionBuilder#build()
* @see Expression#containsVariable(String)
* @see Expression#getVariableNames()
*/
public Expression setVariable(final String name, final double value) {
checkVariableName(name);
variables.get(name).setValue(value);
result = null;
return this;
}
private boolean hasUserFunction(String name) {
boolean contains = false;
for (String s : userFunctionNames) {
contains |= Objects.equals(s, name);
}
return contains;
}
private void checkVariableName(String name) {
if (hasUserFunction(name) || Functions.getBuiltinFunction(name) != null) {
throw new IllegalArgumentException(l10n(
"The variable name '%s' is invalid. Since "
+ "there exists a function with the same name", name
));
}
if (!variables.containsKey(name)) {
throw new IllegalArgumentException(l10n("Variable '%s' doesn't exist.", name));
}
}
/**
* Sets the value of a set of variables, the variables to set must exist at build time and can't
* be the name of a function.
* All variables must be set before calling {@link Expression#evaluate()}
*
* @param variables a {@code Map<String,Double>} containing all the (name, value) pairs.
* @return {@code this}
* @throws IllegalArgumentException if the variable name is a function name or if the variable
* doesn't exist at build time.
* @see ExpressionBuilder#build()
* @see Expression#containsVariable(String)
* @see Expression#getVariableNames()
* @see Expression#setVariable(String, double)
*/
public Expression setVariables(Map<String, Double> variables) {
for (Map.Entry<String, Double> v : variables.entrySet()) {
setVariable(v.getKey(), v.getValue());
}
return this;
}
/**
* Retrieves a {@link Set} containing all the variable names
*
* @return variable names
*/
public Set<String> getVariableNames() {
return variables.keySet();
}
/**
* Tells if a variable exists in the expression
*
* @param name variable name
* @return {@code true} if the variable exists and {@code false} otherwise
*/
public boolean containsVariable(String name) {
return variables.containsKey(name);
}
/**
* Validates an expression.<br>
* Building an expression is not the only metric of <i>correctness</i>, this method will
* generate a {@link ValidationResult} telling if a variables are set, if the number of
* operands is correct, and if all functions have the right number of parameters.<br><br>
* <i><b>Note:</b></i> future version will most likely fail on build, and not at this stage
* (at least that's my plan).
* @param checkVariablesSet {@code true} to check if all variables are set and {@code false}
* otherwise
* @return {@link ValidationResult}
*/
public ValidationResult validate(boolean checkVariablesSet) {
final List<String> errors = new ArrayList<>(10);
checkVariablesSet(checkVariablesSet, errors);
/* Check if the number of operands, functions and operators match.
The idea is to increment a counter for operands and decrease it for operators.
When a function occurs the number of available arguments has to be greater
than or equals to the function's expected number of arguments.
The count has to be larger than 1 at all times and exactly 1 after all tokens
have been processed */
int count = 0;
for (Token tok : tokens) {
switch (tok.getType()) {
case NUMBER, VARIABLE -> count++;
case FUNCTION -> {
final Function func = ((FunctionToken) tok).getFunction();
final int argsNum = func.getNumArguments();
count = validateFunction(argsNum, count, errors, func);
}
case OPERATOR -> {
final Operator op = ((OperatorToken) tok).getOperator();
if (op.getNumOperands() == 2) {
count--;
}
}
default -> {
//Do nothing
}
}
if (count < 1) {
errors.add(l10n("Too many operators"));
return new ValidationResult(errors);
}
}
if (count > 1) {
errors.add(l10n("Too many operands"));
}
return errors.isEmpty() ? ValidationResult.SUCCESS : new ValidationResult(errors);
}
private int validateFunction(int argsNum, int count, List<String> errors, Function func) {
if (argsNum > count) {
errors.add(l10n("Not enough arguments for '%s'", func.getName()));
}
int res = count;
if (argsNum > 1) {
res -= argsNum - 1;
} else if (argsNum == 0) {
// see https://github.com/fasseg/exp4j/issues/59
res++;
}
return res;
}
/**
* Alias for {@code Expression#validate(true)}
*
* @return {@link ValidationResult}
* @see Expression#validate(boolean)
*/
public ValidationResult validate() {
return validate(true);
}
/**
* Simple wrapper for {@link ExecutorService#submit(java.util.concurrent.Callable)}.<br><br>
* Expressions are <b>NOT</b> thread safe (and most likely will never be).
* @param executor {@link ExecutorService} to use
* @return {@link Future} task that will eventually have the result of evaluate()
* @see Expression#evaluate()
*/
public Future<Double> evaluateAsync(ExecutorService executor) {
return executor.submit(this::evaluate);
}
/**
* Evaluates the expression with the given values, this method will fail if
* {@link Expression#validate()} returns a {@link ValidationResult}
* different that {@link ValidationResult#SUCCESS}.<br><br>
* <i><b>Note:</b></i> future version will most likely fail on build, and
* not at this stage, this method will only fail if variables aren't set.
*
* @return result of the evaluation
* @throws IllegalArgumentException if the expression isn't valid
* @see Expression#validate()
*/
public double evaluate() {
if (cacheResult && result != null) {
return result;
}
final ArrayStack output = new ArrayStack();
for (Token t : tokens) {
if (null != t.getType()) {
switch (t.getType()) {
case NUMBER -> output.push(((NumberToken) t).getValue());
case VARIABLE -> {
final VariableToken vt = (VariableToken) t;
if (!vt.isValueSet()) {
throw new IllegalArgumentException(l10n(
"No value has been set for variable '%s'", vt.getName()
));
}
output.push(vt.getValue());
}
case OPERATOR -> {
final Operator op = ((OperatorToken) t).getOperator();
if (output.size() < op.getNumOperands()) {
throw new IllegalArgumentException(l10n(
"Invalid number of operands available for '%s' operator",
op.getSymbol()
));
}
if (op.getNumOperands() == 2) {
/* pop the operands and push the result of the operation */
final double rightArg = output.pop();
final double leftArg = output.pop();
output.push(op.apply(leftArg, rightArg));
} else if (op.getNumOperands() == 1) {
/* pop the operand and push the result of the operation */
final double arg = output.pop();
output.push(op.apply(arg));
}
}
case FUNCTION -> {
final Function func = ((FunctionToken) t).getFunction();
final int numArguments = func.getNumArguments();
if (output.size() < numArguments) {
throw new IllegalArgumentException(l10n(
"Invalid number of arguments available for '%s' function",
func.getName()
));
}
/* collect the arguments from the stack */
final double[] args = new double[numArguments];
for (int j = numArguments - 1; j >= 0; j--) {
args[j] = output.pop();
}
output.push(func.apply(args));
}
default -> {
}
}
}
}
if (output.size() > 1) {
throw new IllegalArgumentException(l10n(
"Invalid number of items on the output queue. "
+ "Might be caused by an invalid number of arguments for a function."
));
}
return result = output.pop();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder(tokens.length * 15);
for (Token token : tokens) {
sb.append(token).append(' ');
}
return sb.substring(0, sb.length() - 1);
}
/**
* Retrieves the internal representation of the expression.<br>
* This method is mostly useless for most users.
*
* @return RPN of the expression
*/
public String toTokenString() {
StringBuilder sb = new StringBuilder(tokens.length * 35);
for (Token token : tokens) {
sb.append(token.getType()).append('[').append(token).append("] ");
}
return sb.substring(0, sb.length() - 1);
}
private void checkVariablesSet(boolean checkVariablesSet, List<String> errors) {
if (!checkVariablesSet) {
return;
}
/* check that all vars have a value set */
for (VariableToken vt : variables.values()) {
if (!vt.isValueSet()) {
errors.add(l10n("The variable '%s' has not been set", vt.getName()));
}
}
}
}