Skip to content

Commit

Permalink
Added the optimization feature based on fedrico's ideas and documented
Browse files Browse the repository at this point in the history
the new feature

as per discussion on #26
  • Loading branch information
fasseg committed Mar 2, 2015
1 parent 212d13b commit f18928d
Show file tree
Hide file tree
Showing 4 changed files with 248 additions and 5 deletions.
36 changes: 35 additions & 1 deletion src/main/java/net/objecthunter/exp4j/ExpressionBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@

import net.objecthunter.exp4j.function.Function;
import net.objecthunter.exp4j.operator.Operator;
import net.objecthunter.exp4j.optimizer.Optimizer;
import net.objecthunter.exp4j.shuntingyard.ShuntingYard;
import net.objecthunter.exp4j.tokenizer.Token;

/**
* Factory class for {@link Expression} instances. This class is the main API entrypoint. Users should create new
Expand All @@ -36,6 +38,8 @@ public class ExpressionBuilder {

private final Set<String> variableNames;

private boolean optimize;

/**
* Create a new ExpressionBuilder instance and initialize it with a given expression string.
* @param expression the expression to be parsed
Expand Down Expand Up @@ -143,6 +147,30 @@ public ExpressionBuilder operator(List<Operator> operators) {
return this;
}

/**
* Set if the expression should be optimized before evaluation. Operations on non variables can be optimized away
* before evaluation takes place, since e.g. {@code sin(2+2)} can be simplified to {@code sin(4)}.
*
* <br><br><strong>WARNING</strong>: Optimizing an expression increases the time complexity of the algorithm. This is *only* beneficial if
* the expression contains operations on plain numbers.
*
* <br><br>
* <pre>
* {@code Expression e = new ExpressionBuilder("log(2*3+4^2)")
* .optimize(true) // internally this turns the expression into log(22)
* .build();
* }
* </pre>
*
*
* @param enabled set to true if optimization should take place, false otherwise
* @return an {@link Expression} instance which can be used to evaluate the result of the expression
*/
public ExpressionBuilder optimize(boolean enabled) {
this.optimize = enabled;
return this;
}

/**
* Build the {@link Expression} instance using the custom operators and functions set.
* @return an {@link Expression} instance which can be used to evaluate the result of the expression
Expand All @@ -151,7 +179,13 @@ public Expression build() {
if (expression.length() == 0) {
throw new IllegalArgumentException("The expression can not be empty");
}
return new Expression(ShuntingYard.convertToRPN(this.expression, this.userFunctions, this.userOperators, this.variableNames),
/* Tokenize the expression and have the tokens ordered for RPN operations by the shunting yard */
Token[] tokens = ShuntingYard.convertToRPN(this.expression, this.userFunctions, this.userOperators,
this.variableNames);
if (optimize) {
tokens = new Optimizer(true).optimize(tokens);
}
return new Expression(tokens,
this.userFunctions.keySet());
}

Expand Down
100 changes: 100 additions & 0 deletions src/main/java/net/objecthunter/exp4j/optimizer/Optimizer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Copyright 2015 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.optimizer;


import net.objecthunter.exp4j.operator.Operator;
import net.objecthunter.exp4j.tokenizer.NumberToken;
import net.objecthunter.exp4j.tokenizer.OperatorToken;
import net.objecthunter.exp4j.tokenizer.Token;

import java.util.Arrays;
import java.util.Stack;

/**
* Optimize an expression for faster evaluation.
* Be aware that optimizing an expression means sweeping over the input an additional time and therefore increasing the
* time complexity.
*
* (1) Optimize simple operations:
* This optimization checks for the occurence of simple operation like addition or multiplication with non variables
* and substitutes the two NumberToken and a OperatorToken by a single NumberToken
*
* This optimization was suggested and first implemented by Federico Vera
*/
public class Optimizer {
private final boolean optimizeSimpleOperations;

public Optimizer(boolean optimizeSimpleOperations) {
this.optimizeSimpleOperations = optimizeSimpleOperations;
}

/**
* Optimize a set of tokens
* @param tokens the set of tokens to optimize
* @return the optimized set
*/
public static Token[] optimize(Token[] tokens) {
final Token[] output = new Token[tokens.length];
int idx = 0;

for (Token t : tokens) {
switch (t.getType()) {
case Token.TOKEN_OPERATOR:
final OperatorToken opToken = (OperatorToken) t;
final Operator op = opToken.getOperator();
if (output.length < op.getNumOperands()) {
throw new IllegalArgumentException("Invalid number of operands available");
}
if (op.getNumOperands() == 2) {
final Token rightOperand = output[--idx];
final Token leftOperand = output[--idx];
if (rightOperand.getType() == Token.TOKEN_NUMBER && leftOperand.getType() == Token.TOKEN_NUMBER) {
/* just replace the three tokens with a single NumberToken */
double leftVal = ((NumberToken) leftOperand).getValue();
double rightVal = ((NumberToken) rightOperand).getValue();
output[idx++] = new NumberToken(op.apply(leftVal, rightVal));
} else {
/* reset the index to the position after the operands */
idx += 2;
/* add the original operator token to the output */
output[idx++] = t;
}
} else if (op.getNumOperands() == 1) {
final Token operand = output[--idx];
if (operand.getType() == Token.TOKEN_NUMBER) {
/* just replace the two tokens with a single NumberToken */
double val = ((NumberToken) operand).getValue();
output[idx++] = new NumberToken(op.apply(val));
} else {
/* reset the index to the position after the operands */
idx += 1;
/* add the original operator token to the output */
output[idx++] = t;
}
} else {
/* can't optimize, the number of operands seems funky, since it's not 2 or 1, so just add the
token w/o optimization*/
output[idx++] = t;
}
break;
default:
output[idx++]=t;
}
}
return Arrays.copyOf(output, idx);
}
}
26 changes: 23 additions & 3 deletions src/site/apt/index.apt.vm
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,24 @@ Expression e = new ExpressionBuilder("0$").operator(reciprocal).build();
e.evaluate(); // <- this call will throw an ArithmeticException
+--

* Optimization

exp4j can try to optimize a given expression for faster evaluation by simplifying constant operations. An expression "2+2-sin(x)" can be simplified to "4-sin(x)". The resulting contraction of the expression is beneficial to the evaluation, but does introduce an additional sweep over the input and therefore increases space and time complexity of the algorithm.
Users are advised to only use optimization in a scenario where expressions are highly likely to contain a constant operation and repeated evaluation of the same expression takes place.

** Example 8

Optimization of an expression before evaluation

+--
Expression e = new ExpressionBuilder("2+2-sin(x)")
.optimize(true) // let's exp4j contract 2+2-sin(x) to 4-sin(x)
.variable("x")
.build();
for (int i = 0; i < 1000; i++) {
assertEquals(4d-Math.sin(i), e.setVariable("x", i).evaluate(), 0);
}
+--

* Built-in functions

Expand Down Expand Up @@ -349,7 +367,7 @@ e.evaluate(); // <- this call will throw an ArithmeticException
The validate method also accepts a boolean argument, indicating if a check for empty variables should be performed.


** Example 8
** Example 9

Validate an expression

Expand All @@ -367,7 +385,7 @@ res = e.validate();
assertTrue(res.isValid());
+--

** Example 9
** Example 10

Validate an expression before variables have been set, i.e. skip checking if all variables have been set.

Expand All @@ -387,10 +405,12 @@ assertNull(res.getErrors());

Tag Library

Thanks to Leo there is a Tag Library available at https://github.com/leogtzr/exp4j_tld. This enables users to use exp4j directly in JSP pages.
Thanks to Leo there is a Tag Library available at {{{https://github.com/leogtzr/exp4j_tld}leogtzr/exp4j_tld}}. This enables users to use exp4j directly in JSP pages.

* API changes

* Version 0.4.5: Added optimize() method to ExpressionBuilder

* Version 0.4.3: Unicode names support added

* Version 0.4.1: Future API introduced
Expand Down
91 changes: 90 additions & 1 deletion src/test/java/net/objecthunter/exp4j/ExpressionBuilderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -1627,6 +1627,17 @@ public void testDocumentationExample8() throws Exception {

}

@Test
public void testDocumentationExample9() throws Exception {
Expression e = new ExpressionBuilder("2+2-sin(x)")
.optimize(true)
.variable("x")
.build();
for (int i = 0; i < 1000; i++) {
assertEquals(4d-Math.sin(i), e.setVariable("x", i).evaluate(), 0);
}
}

@Test
public void testDocumentationExample2() throws Exception {
ExecutorService exec = Executors.newFixedThreadPool(1);
Expand Down Expand Up @@ -2529,4 +2540,82 @@ public double apply(double... args) {
.evaluate();
assertEquals(-2d, result, 0d);
}
}

@Test
public void testOptimization1() throws Exception {
Expression e = new ExpressionBuilder("2")
.optimize(true)
.build();
assertEquals(2d, e.evaluate(), 0d);
}

@Test
public void testOptimization2() throws Exception {
Expression e = new ExpressionBuilder("2+2")
.optimize(true)
.build();
assertEquals(4d, e.evaluate(), 0d);
}

@Test
public void testOptimization3() throws Exception {
Expression e = new ExpressionBuilder("(2*3)")
.optimize(true)
.build();
assertEquals(6d, e.evaluate(), 0d);
}

@Test
public void testOptimization4() throws Exception {
Expression e = new ExpressionBuilder("sin(2*3)")
.optimize(true)
.build();
assertEquals(sin(6d), e.evaluate(), 0d);
}

@Test
public void testOptimization5() throws Exception {
Operator factorial = new Operator("!", 1, true, Operator.PRECEDENCE_POWER + 1) {

@Override
public double apply(double... args) {
final int arg = (int) args[0];
if ((double) arg != args[0]) {
throw new IllegalArgumentException("Operand for factorial has to be an integer");
}
if (arg < 0) {
throw new IllegalArgumentException("The operand of the factorial can not be less than zero");
}
double result = 1;
for (int i = 1; i <= arg; i++) {
result *= i;
}
return result;
}
};

Expression e = new ExpressionBuilder("sin(2+3!)")
.operator(factorial)
.optimize(true)
.build();
assertEquals(sin(8d), e.evaluate(), 0d);
}

@Test
public void testOptimization6() throws Exception {
Expression e = new ExpressionBuilder("sin(2+3) * 2+ log(3-1) / (2+3) * 4^2")
.optimize(true)
.build();
assertEquals(sin(5d) * 2 + log(2) / 5 * 16, e.evaluate(), 0d);
}

@Test
public void testOptimization7() throws Exception {
Expression e = new ExpressionBuilder("2+2v")
.optimize(true)
.variable("v")
.build();
e.setVariable("v", 3);
assertEquals(8d, e.evaluate(), 0d);
}
}

0 comments on commit f18928d

Please sign in to comment.