Added ability for doublemodel to evaluate math expressions using exp4j, fixed typeover issue and made the doublemodel more robust to incorrect input.

This commit is contained in:
Richard Graham 2012-05-17 06:08:05 +00:00
parent ba4ceccf26
commit 5320377719
23 changed files with 1621 additions and 17 deletions

View File

@ -1,6 +1,8 @@
package net.sf.openrocket.gui;
import javax.swing.JSpinner;
import javax.swing.text.DefaultFormatter;
import javax.swing.text.DefaultFormatterFactory;
/**
* Editable editor for a JSpinner. Simply uses JSpinner.DefaultEditor, which has been made
@ -16,6 +18,10 @@ public class SpinnerEditor extends JSpinner.DefaultEditor {
super(spinner);
//super(spinner,"0.0##");
getTextField().setEditable(true);
DefaultFormatterFactory dff = (DefaultFormatterFactory) getTextField().getFormatterFactory();
DefaultFormatter formatter = (DefaultFormatter) dff.getDefaultFormatter();
formatter.setOverwriteMode(false);
}
}

View File

@ -23,13 +23,16 @@ import net.sf.openrocket.unit.Unit;
import net.sf.openrocket.unit.UnitGroup;
import net.sf.openrocket.util.BugException;
import net.sf.openrocket.util.ChangeSource;
import net.sf.openrocket.util.FractionUtil;
import net.sf.openrocket.util.Invalidatable;
import net.sf.openrocket.util.Invalidator;
import net.sf.openrocket.util.MathUtil;
import net.sf.openrocket.util.MemoryManagement;
import net.sf.openrocket.util.Reflection;
import net.sf.openrocket.util.StateChangeListener;
import net.sf.openrocket.util.exp4j.Calculable;
import net.sf.openrocket.util.exp4j.ExpressionBuilder;
import net.sf.openrocket.util.exp4j.UnknownFunctionException;
import net.sf.openrocket.util.exp4j.UnparsableExpressionException;
/**
@ -55,8 +58,11 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat
//////////// JSpinner Model ////////////
/**
* Model suitable for JSpinner using JSpinner.NumberEditor. It extends SpinnerNumberModel
* Model suitable for JSpinner.
* Note: Previously used using JSpinner.NumberEditor and extended SpinnerNumberModel
* to be compatible with the NumberEditor, but only has the necessary methods defined.
* This is still the design, but now extends AbstractSpinnerModel to allow other characters
* to be entered so that fractional units and expressions can be used.
*/
public class ValueSpinnerModel extends AbstractSpinnerModel implements Invalidatable {
@ -73,26 +79,46 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat
" value=" + value + ", currently firing events");
return;
}
Number num = 0;
Number num = Double.NaN;
// Set num if possible
if ( value instanceof Number ) {
num = (Number)value;
} else if ( value instanceof String ) {
}
else if ( value instanceof String ) {
try {
String newValString = (String)value;
num = FractionUtil.parseFraction(newValString);
}
catch ( java.lang.NumberFormatException nfex ) {
num = 0.0d;
ExpressionBuilder builder=new ExpressionBuilder(newValString);
Calculable calc=builder.build();
num = calc.calculate();
}
catch ( java.lang.NumberFormatException e ) {
} catch (UnknownFunctionException e) {
} catch (UnparsableExpressionException e) {
} catch (java.util.EmptyStackException e) {
}
}
double newValue = num.doubleValue();
double converted = currentUnit.fromUnit(newValue);
log.user("SpinnerModel setValue called for " + DoubleModel.this.toString() + " newValue=" + newValue +
" converted=" + converted);
DoubleModel.this.setValue(converted);
// Update the doublemodel with the new number or return to the last number if not possible
if ( ((Double)num).isNaN() ) {
DoubleModel.this.setValue( lastValue );
log.user("SpinnerModel could not set value for " + DoubleModel.this.toString() + ". Could not convert " + value.toString());
}
else {
double newValue = num.doubleValue();
double converted = currentUnit.fromUnit(newValue);
log.user("SpinnerModel setValue called for " + DoubleModel.this.toString() + " newValue=" + newValue +
" converted=" + converted);
DoubleModel.this.setValue(converted);
}
// Force a refresh if text doesn't match up exactly with the stored value
if ( ! ((Double)lastValue).toString().equals( this.getValue().toString() ) ) {
DoubleModel.this.fireStateChanged();
log.debug("SpinnerModel "+DoubleModel.this.toString()+" refresh forced because string did not match actual value.");
}
}
@Override

View File

@ -43,7 +43,8 @@ public class AboutDialog extends JDialog {
"<b>OpenRocket utilizes the following libraries:</b><br><br>" +
"MiG Layout (http://www.miglayout.com/)<br>" +
"JFreeChart (http://www.jfree.org/jfreechart/)<br>" +
"iText (http://www.itextpdf.com/)";
"iText (http://www.itextpdf.com/)<br>" +
"exp4j (http://projects.congrace.de/exp4j/index.html)";
public AboutDialog(JFrame parent) {

View File

@ -170,7 +170,7 @@ public class FractionalUnit extends Unit {
} else if (intPart == 0.0 ){
return intFormat.format(sign*frac) + "/" + intFormat.format(fracBase);
} else {
return intFormat.format(sign*intPart) + " " + intFormat.format(frac) + "/" + intFormat.format(fracBase);
return intFormat.format(sign*intPart) + " + " + intFormat.format(frac) + "/" + intFormat.format(fracBase);
}
}

View File

@ -0,0 +1,85 @@
/*
Copyright 2011 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.sf.openrocket.util.exp4j;
import java.text.NumberFormat;
import java.util.List;
/**
* Abstract base class for mathematical expressions
*
* @author fas@congrace.de
*/
abstract class AbstractExpression {
private final String expression;
private final Token[] tokens;
private final String[] variableNames;
private final NumberFormat numberFormat = NumberFormat.getInstance();
/**
* Construct a new {@link AbstractExpression}
*
* @param expression
* the mathematical expression to be used
* @param tokens
* the {@link Token}s in the expression
* @param variableNames
* an array of variable names which are used in the expression
*/
AbstractExpression(String expression, Token[] tokens, String[] variableNames) {
this.expression = expression;
this.tokens = tokens;
this.variableNames = variableNames;
}
/**
* get the mathematical expression {@link String}
*
* @return the expression
*/
public String getExpression() {
return expression;
}
/**
* get the used {@link NumberFormat}
*
* @return the used {@link NumberFormat}
*/
public NumberFormat getNumberFormat() {
return numberFormat;
}
/**
* get the {@link Token}s
*
* @return the array of {@link Token}s
*/
Token[] getTokens() {
return tokens;
}
/**
* get the variable names
*
* @return the {@link List} of variable names
*/
String[] getVariableNames() {
return variableNames;
}
}

View File

@ -0,0 +1,33 @@
package net.sf.openrocket.util.exp4j;
/**
* This is the basic result class of the exp4j {@link ExpressionBuilder}
*
* @author ruckus
*
*/
public interface Calculable {
/**
* calculate the result of the expression
*
* @return the result of the calculation
*/
public double calculate();
/**
* return the expression in reverse polish postfix notation
*
* @return the expression used to construct this {@link Calculable}
*/
public String getExpression();
/**
* set a variable value for the calculation
*
* @param name
* the variable name
* @param value
* the value of the variable
*/
public void setVariable(String name, double value);
}

View File

@ -0,0 +1,30 @@
/*
Copyright 2011 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.sf.openrocket.util.exp4j;
import java.util.Map;
import java.util.Stack;
abstract class CalculationToken extends Token {
CalculationToken(String value) {
super(value);
}
abstract void mutateStackForCalculation(Stack<Double> stack, Map<String, Double> variableValues);
}

View File

@ -0,0 +1,58 @@
/*
Copyright 2011 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.sf.openrocket.util.exp4j;
/**
* Simple commandline interpreter for mathematical expressions the interpreter
* takes a mathematical expressions as a {@link String} argument, evaluates it
* and prints out the result.
*
*
* <pre>
* java de.congrace.exp4j.CommandlineInterpreter "2 * log(2.2223) - ((2-3.221) * 14.232^2)"
* > 248.91042049521056
* </pre>
*
* @author fas@congrace.de
*
*/
public class CommandlineInterpreter {
private static void calculateExpression(String string) {
try {
final PostfixExpression pe = PostfixExpression.fromInfix(string);
System.out.println(pe.calculate());
} catch (UnparsableExpressionException e) {
e.printStackTrace();
} catch (UnknownFunctionException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
if (args.length != 1) {
printUsage();
} else {
calculateExpression(args[0]);
}
}
private static void printUsage() {
final StringBuilder usage = new StringBuilder();
usage.append("Commandline Expression Parser\n\n").append("Example: ").append("\n").append("java -jar exp4j.jar \"2.12 * log(23) * (12 - 4)\"\n\n")
.append("written by fas@congrace.de");
System.err.println(usage.toString());
}
}

View File

@ -0,0 +1,84 @@
package net.sf.openrocket.util.exp4j;
import java.util.Map;
import java.util.Stack;
import net.sf.openrocket.util.exp4j.FunctionToken.Function;
/**
* this classed is used to create custom functions for exp4j<br/>
* <br/>
* <b>Example</b><br/>
* <code><pre>{@code
* CustomFunction fooFunc = new CustomFunction("foo") {
* public double applyFunction(double value) {
* return value*Math.E;
* }
* };
* double varX=12d;
* Calculable calc = new ExpressionBuilder("foo(x)").withCustomFunction(fooFunc).withVariable("x",varX).build();
* assertTrue(calc.calculate() == Math.E * varX);
* }</pre></code>
*
* @author ruckus
*
*/
public abstract class CustomFunction extends CalculationToken {
private int argc=1;
/**
* create a new single value input CustomFunction with a set name
*
* @param value
* the name of the function (e.g. foo)
*/
protected CustomFunction(String value) throws InvalidCustomFunctionException{
super(value);
for (Function f:Function.values()) {
if (value.equalsIgnoreCase(f.toString())){
throw new InvalidCustomFunctionException(value + " is already reserved as a function name");
}
}
}
/**
* create a new single value input CustomFunction with a set name
*
* @param value
* the name of the function (e.g. foo)
*/
protected CustomFunction(String value,int argumentCount) throws InvalidCustomFunctionException{
super(value);
this.argc=argumentCount;
for (Function f:Function.values()) {
if (value.equalsIgnoreCase(f.toString())){
throw new InvalidCustomFunctionException(value + " is already reserved as a function name");
}
}
}
/**
* apply the function to a value
*
* @param values
* the values to which the function should be applied.
* @return the function value
*/
public abstract double applyFunction(double[] values);
@Override
void mutateStackForCalculation(Stack<Double> stack, Map<String, Double> variableValues) {
double[] args=new double[argc];
for (int i=0;i<argc;i++) {
args[i]=stack.pop();
}
stack.push(this.applyFunction(args));
}
@Override
void mutateStackForInfixTranslation(Stack<Token> operatorStack, StringBuilder output) {
operatorStack.push(this);
}
public int getArgumentCount() {
return argc;
}
}

View File

@ -0,0 +1,131 @@
package net.sf.openrocket.util.exp4j;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
/**
* This is Builder implementation for the exp4j API used to create a Calculable
* instance for the user
*
* @author ruckus
*
*/
public class ExpressionBuilder {
private final Map<String, Double> variables = new LinkedHashMap<String, Double>();
private final Set<CustomFunction> customFunctions = new HashSet<CustomFunction>();
private String expression;
/**
* Create a new ExpressionBuilder
*
* @param expression
* the expression to evaluate
*/
public ExpressionBuilder(String expression) {
this.expression = expression;
}
/**
* build a new {@link Calculable} from the expression using the supplied
* variables
*
* @return the {@link Calculable} which can be used to evaluate the
* expression
* @throws UnknownFunctionException
* when an unrecognized function name is used in the expression
* @throws UnparsableExpressionException
* if the expression could not be parsed
*/
public Calculable build() throws UnknownFunctionException, UnparsableExpressionException {
if (expression.indexOf('=') == -1 && !variables.isEmpty()) {
// User supplied an expression without leading "f(...)="
// so we just append the user function to a proper "f()="
// for PostfixExpression.fromInfix()
StringBuilder function = new StringBuilder("f(");
for (String var : variables.keySet()) {
function.append(var).append(',');
}
expression = function.deleteCharAt(function.length() - 1).toString() + ")=" + expression;
}
// create the PostfixExpression and return it as a Calculable
PostfixExpression delegate = PostfixExpression.fromInfix(expression, customFunctions);
for (String var : variables.keySet()) {
if (variables.get(var) != null) {
delegate.setVariable(var, variables.get(var));
}
for (CustomFunction custom:customFunctions){
if (custom.getValue().equals(var)){
throw new UnparsableExpressionException("variable '" + var + "' cannot have the same name as a custom function " + custom.getValue());
}
}
}
return delegate;
}
/**
* add a custom function instance for the evaluator to recognize
*
* @param function
* the {@link CustomFunction} to add
* @return the {@link ExpressionBuilder} instance
*/
public ExpressionBuilder withCustomFunction(CustomFunction function) {
customFunctions.add(function);
return this;
}
public ExpressionBuilder withCustomFunctions(Collection<CustomFunction> functions) {
customFunctions.addAll(functions);
return this;
}
/**
* set the value for a variable
*
* @param variableName
* the variable name e.g. "x"
* @param value
* the value e.g. 2.32d
* @return the {@link ExpressionBuilder} instance
*/
public ExpressionBuilder withVariable(String variableName, double value) {
variables.put(variableName, value);
return this;
}
/**
* set the variables names used in the expression without setting their
* values
*
* @param variableNames
* vararg {@link String} of the variable names used in the
* expression
* @return the ExpressionBuilder instance
*/
public ExpressionBuilder withVariableNames(String... variableNames) {
for (String variable : variableNames) {
variables.put(variable, null);
}
return this;
}
/**
* set the values for variables
*
* @param variableMap
* a map of variable names to variable values
* @return the {@link ExpressionBuilder} instance
*/
public ExpressionBuilder withVariables(Map<String, Double> variableMap) {
for (Entry<String, Double> v : variableMap.entrySet()) {
variables.put(v.getKey(), v.getValue());
}
return this;
}
}

View File

@ -0,0 +1,17 @@
package net.sf.openrocket.util.exp4j;
import java.util.Stack;
public class FunctionSeparatorToken extends Token{
public FunctionSeparatorToken() {
super(",");
}
@Override
void mutateStackForInfixTranslation(Stack<Token> operatorStack, StringBuilder output) {
Token token;
while (!((token=operatorStack.peek()) instanceof ParenthesisToken) && !token.getValue().equals("(")){
output.append(operatorStack.pop().getValue()).append(" ");
}
}
}

View File

@ -0,0 +1,127 @@
/*
Copyright 2011 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.sf.openrocket.util.exp4j;
import java.util.Map;
import java.util.Stack;
/**
* A {@link Token} for functions
*
* @author fas@congrace.de
*
*/
class FunctionToken extends CalculationToken {
/**
* the functionNames that can be used in an expression
*
* @author ruckus
*
*/
enum Function {
ABS, ACOS, ASIN, ATAN, CBRT, CEIL, COS, COSH, EXP, EXPM1, FLOOR, LOG, SIN, SINH, SQRT, TAN, TANH
}
private Function function;
/**
* construct a new {@link FunctionToken}
*
* @param value
* the name of the function
* @throws UnknownFunctionException
* if an unknown function name is encountered
*/
FunctionToken(String value) throws UnknownFunctionException {
super(value);
try {
function = Function.valueOf(value.toUpperCase());
} catch (IllegalArgumentException e) {
throw new UnknownFunctionException(value);
}
if (function == null) {
throw new UnknownFunctionException(value);
}
}
/**
* apply a function to a value x
*
* @param x
* the value the function should be applied to
* @return the result of the function
*/
double applyFunction(double x) {
switch (function) {
case ABS:
return Math.abs(x);
case ACOS:
return Math.acos(x);
case ASIN:
return Math.asin(x);
case ATAN:
return Math.atan(x);
case CBRT:
return Math.cbrt(x);
case CEIL:
return Math.ceil(x);
case COS:
return Math.cos(x);
case COSH:
return Math.cosh(x);
case EXP:
return Math.exp(x);
case EXPM1:
return Math.expm1(x);
case FLOOR:
return Math.floor(x);
case LOG:
return Math.log(x);
case SIN:
return Math.sin(x);
case SINH:
return Math.sinh(x);
case SQRT:
return Math.sqrt(x);
case TAN:
return Math.tan(x);
case TANH:
return Math.tanh(x);
default:
return Double.NaN; // should not happen ;)
}
}
/**
* get the {@link Function}
*
* @return the correspoding {@link Function}
*/
Function getFunction() {
return function;
}
@Override
void mutateStackForCalculation(Stack<Double> stack, Map<String, Double> variableValues) {
stack.push(this.applyFunction(stack.pop()));
}
@Override
void mutateStackForInfixTranslation(Stack<Token> operatorStack, StringBuilder output) {
operatorStack.push(this);
}
}

View File

@ -0,0 +1,109 @@
/*
Copyright 2011 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.sf.openrocket.util.exp4j;
import java.util.Set;
import java.util.Stack;
/**
* Translate a mathematical expression in human readable infix notation to a
* Reverse Polish Notation (postfix) expression for easier parsing. by
* implementing the shunting yard algorithm by dijkstra
*
* @author fas@congrace.de
*/
class InfixTranslator {
private static String substituteUnaryOperators(String expr) {
final StringBuilder exprBuilder = new StringBuilder(expr.length());
final char[] data = expr.toCharArray();
char lastChar = ' ';
for (int i = 0; i < expr.length(); i++) {
if (exprBuilder.length() > 0) {
lastChar = exprBuilder.charAt(exprBuilder.length() - 1);
}
final char c = data[i];
switch (c) {
case '+':
if (i > 0 && lastChar != '(' && !(OperatorToken.isOperator(lastChar))) {
exprBuilder.append(c);
}
break;
case '-':
if (i > 0 && lastChar != '(' && !(OperatorToken.isOperator(lastChar))) {
exprBuilder.append(c);
} else {
exprBuilder.append('#');
}
break;
default:
if (!Character.isWhitespace(c)) {
exprBuilder.append(c);
}
}
}
return exprBuilder.toString();
}
/**
* Delegation method for simple expression without variables or custom
* functions
*
* @param infixExpression
* the infix expression to be translated
* @return translated RNP postfix expression
* @throws UnparsableExpressionException
* when the expression is invalid
* @throws UnknownFunctionException
* when an unknown function has been used in the input.
*/
static String toPostfixExpression(String infixExpression) throws UnparsableExpressionException, UnknownFunctionException {
return toPostfixExpression(infixExpression, null, null);
}
/**
* implement the shunting yard algorithm
*
* @param infixExpression
* the human readable expression which should be translated to
* RPN
* @param variableNames
* the variable names used in the expression
* @param customFunctions
* the CustomFunction implementations used
* @return the expression in postfix format
* @throws UnparsableExpressionException
* if the expression could not be translated to RPN
* @throws UnknownFunctionException
* if an unknown function was encountered
*/
static String toPostfixExpression(String infixExpression, String[] variableNames, Set<CustomFunction> customFunctions)
throws UnparsableExpressionException, UnknownFunctionException {
infixExpression = substituteUnaryOperators(infixExpression);
final Token[] tokens = new Tokenizer(variableNames, customFunctions).tokenize(infixExpression);
final StringBuilder output = new StringBuilder(tokens.length);
final Stack<Token> operatorStack = new Stack<Token>();
for (final Token token : tokens) {
token.mutateStackForInfixTranslation(operatorStack, output);
}
// all tokens read, put the rest of the operations on the output;
while (operatorStack.size() > 0) {
output.append(operatorStack.pop().getValue()).append(" ");
}
return output.toString().trim();
}
}

View File

@ -0,0 +1,9 @@
package net.sf.openrocket.util.exp4j;
public class InvalidCustomFunctionException extends Exception{
private static final long serialVersionUID = 1L;
public InvalidCustomFunctionException(String message) {
super(message);
}
}

View File

@ -0,0 +1,66 @@
/*
Copyright 2011 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.sf.openrocket.util.exp4j;
import java.util.Map;
import java.util.Stack;
/**
* A {@link Token} for Numbers
*
* @author fas@congrace.de
*
*/
class NumberToken extends CalculationToken {
private final double doubleValue;
/**
* construct a new {@link NumberToken}
*
* @param value
* the value of the number as a {@link String}
*/
NumberToken(String value) {
super(value);
this.doubleValue = Double.parseDouble(value);
}
@Override
public boolean equals(Object obj) {
if (obj instanceof NumberToken) {
final NumberToken t = (NumberToken) obj;
return t.getValue().equals(this.getValue());
}
return false;
}
@Override
public int hashCode() {
return getValue().hashCode();
}
@Override
void mutateStackForCalculation(Stack<Double> stack, Map<String, Double> variableValues) {
stack.push(this.doubleValue);
}
@Override
void mutateStackForInfixTranslation(Stack<Token> operatorStack, StringBuilder output) {
output.append(this.getValue()).append(' ');
}
}

View File

@ -0,0 +1,208 @@
/*
Copyright 2011 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.sf.openrocket.util.exp4j;
import java.util.Map;
import java.util.Stack;
/**
* {@link Token} for Operations like +,-,*,/,% and ^
*
* @author fas@congrace.de
*/
class OperatorToken extends CalculationToken {
/**
* the valid {@link Operation}s for the {@link OperatorToken}
*
* @author fas@congrace.de
*/
enum Operation {
ADDITION(1, true), SUBTRACTION(1, true), MULTIPLICATION(2, true), DIVISION(2, true), MODULO(2, true), EXPONENTIATION(3, false), UNARY_MINUS(4, false), UNARY_PLUS(
4, false);
private final int precedence;
private final boolean leftAssociative;
private Operation(int precedence, boolean leftAssociative) {
this.precedence = precedence;
this.leftAssociative = leftAssociative;
}
}
/**
* return a corresponding {@link Operation} for a symbol
*
* @param c
* the symbol of the operation
* @return the corresponding {@link Operation}
*/
static Operation getOperation(char c) {
switch (c) {
case '+':
return Operation.ADDITION;
case '-':
return Operation.SUBTRACTION;
case '*':
return Operation.MULTIPLICATION;
case '/':
return Operation.DIVISION;
case '^':
return Operation.EXPONENTIATION;
case '#':
return Operation.UNARY_MINUS;
case '%':
return Operation.MODULO;
default:
return null;
}
}
static boolean isOperator(char c) {
return getOperation(c) != null;
}
private final Operation operation;
/**
* construct a new {@link OperatorToken}
*
* @param value
* the symbol (e.g.: '+')
* @param operation
* the {@link Operation} of this {@link Token}
*/
OperatorToken(String value, Operation operation) {
super(value);
this.operation = operation;
}
/**
* apply the {@link Operation}
*
* @param values
* the doubles to operate on
* @return the result of the {@link Operation}
*/
double applyOperation(double... values) {
switch (operation) {
case ADDITION:
return values[0] + values[1];
case SUBTRACTION:
return values[0] - values[1];
case MULTIPLICATION:
return values[0] * values[1];
case EXPONENTIATION:
return Math.pow(values[0], values[1]);
case DIVISION:
return values[0] / values[1];
case UNARY_MINUS:
return -values[0];
case UNARY_PLUS:
return values[0];
case MODULO:
return values[0] % values[1];
default:
return 0;
}
}
@Override
public boolean equals(Object obj) {
if (obj instanceof OperatorToken) {
final OperatorToken t = (OperatorToken) obj;
return t.getValue().equals(this.getValue());
}
return false;
}
int getOperandCount() {
switch (operation) {
case ADDITION:
case SUBTRACTION:
case MULTIPLICATION:
case DIVISION:
case EXPONENTIATION:
case MODULO:
return 2;
case UNARY_MINUS:
case UNARY_PLUS:
return 1;
default:
return 0;
}
}
/**
* get the {@link Operation} of this {@link Token}
*
* @return the {@link Operation}
*/
Operation getOperation() {
return operation;
}
int getPrecedence() {
return operation.precedence;
}
@Override
public int hashCode() {
return getValue().hashCode();
}
/**
* check if the operation is left associative
*
* @return true if left associative, otherwise false
*/
boolean isLeftAssociative() {
return operation.leftAssociative;
}
@Override
void mutateStackForCalculation(Stack<Double> stack, Map<String, Double> variableValues) {
if (this.getOperandCount() == 2) {
final double n2 = stack.pop();
final double n1 = stack.pop();
stack.push(this.applyOperation(n1, n2));
} else if (this.getOperandCount() == 1) {
final double n1 = stack.pop();
stack.push(this.applyOperation(n1));
}
}
@Override
void mutateStackForInfixTranslation(Stack<Token> operatorStack, StringBuilder output) {
Token before;
while (!operatorStack.isEmpty() && (before = operatorStack.peek()) != null && (before instanceof OperatorToken || before instanceof FunctionToken)) {
if (before instanceof FunctionToken) {
operatorStack.pop();
output.append(before.getValue()).append(" ");
} else {
final OperatorToken stackOperator = (OperatorToken) before;
if (this.isLeftAssociative() && this.getPrecedence() <= stackOperator.getPrecedence()) {
output.append(operatorStack.pop().getValue()).append(" ");
} else if (!this.isLeftAssociative() && this.getPrecedence() < stackOperator.getPrecedence()) {
output.append(operatorStack.pop().getValue()).append(" ");
} else {
break;
}
}
}
operatorStack.push(this);
}
}

View File

@ -0,0 +1,69 @@
/*
Copyright 2011 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.sf.openrocket.util.exp4j;
import java.util.Stack;
/**
* Token for parenthesis
*
* @author fas@congrace.de
*/
class ParenthesisToken extends Token {
ParenthesisToken(String value) {
super(value);
}
@Override
public boolean equals(Object obj) {
if (obj instanceof ParenthesisToken) {
final ParenthesisToken t = (ParenthesisToken) obj;
return t.getValue().equals(this.getValue());
}
return false;
}
@Override
public int hashCode() {
return getValue().hashCode();
}
/**
* check the direction of the parenthesis
*
* @return true if it's a left parenthesis (open) false if it is a right
* parenthesis (closed)
*/
boolean isOpen() {
return getValue().equals("(") || getValue().equals("[") || getValue().equals("{");
}
@Override
void mutateStackForInfixTranslation(Stack<Token> operatorStack, StringBuilder output) {
if (this.isOpen()) {
operatorStack.push(this);
} else {
Token next;
while ((next = operatorStack.peek()) instanceof OperatorToken || next instanceof FunctionToken || next instanceof CustomFunction
|| (next instanceof ParenthesisToken && !((ParenthesisToken) next).isOpen())) {
output.append(operatorStack.pop().getValue()).append(" ");
}
operatorStack.pop();
}
}
}

View File

@ -0,0 +1,144 @@
/*
Copyright 2011 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.sf.openrocket.util.exp4j;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
/**
* Class for calculating values from a RPN postfix expression.<br/>
* The default way to create a new instance of {@link PostfixExpression} is by
* using the static factory method fromInfix()
*
* @author fas@congrace.de
*/
public final class PostfixExpression extends AbstractExpression implements Calculable {
/**
* Factory method for creating {@link PostfixExpression}s from human
* readable infix expressions
*
* @param expression
* the infix expression to be used
* @return an equivalent {@link PostfixExpression}
* @throws UnparsableExpressionException
* if the expression was invalid
* @throws UnknownFunctionException
* if an unknown function has been used
* @deprecated please use {@link ExpressionBuilder} API
*/
@Deprecated
public static PostfixExpression fromInfix(String expression) throws UnparsableExpressionException, UnknownFunctionException {
return fromInfix(expression, null);
}
/**
* Factory method for creating {@link PostfixExpression}s from human
* readable infix expressions
*
* @param expression
* the infix expression to be used
* @param customFunctions
* the CustomFunction implementations used
* @return an equivalent {@link PostfixExpression}
* @throws UnparsableExpressionException
* if the expression was invalid
* @throws UnknownFunctionException
* if an unknown function has been used
* @deprecated please use {@link ExpressionBuilder}
*/
@Deprecated
public static PostfixExpression fromInfix(String expression, Set<CustomFunction> customFunctions) throws UnparsableExpressionException,
UnknownFunctionException {
String[] variables = null;
int posStart, posEnd;
if ((posStart = expression.indexOf('=')) > 0) {
String functionDef = expression.substring(0, posStart);
expression = expression.substring(posStart + 1);
if ((posStart = functionDef.indexOf('(')) > 0 && (posEnd = functionDef.indexOf(')')) > 0) {
variables = functionDef.substring(posStart + 1, posEnd).split(",");
}
}
return new PostfixExpression(InfixTranslator.toPostfixExpression(expression, variables, customFunctions), variables, customFunctions);
}
private final Map<String, Double> variableValues = new HashMap<String, Double>();
/**
* Construct a new simple {@link PostfixExpression}
*
* @param expression
* the postfix expression to be calculated
* @param variableNames
* the variable names in the expression
* @param customFunctions
* the CustomFunction implementations used
* @throws UnparsableExpressionException
* when expression is invalid
* @throws UnknownFunctionException
* when an unknown function has been used
*/
private PostfixExpression(String expression, String[] variableNames, Set<CustomFunction> customFunctions) throws UnparsableExpressionException,
UnknownFunctionException {
super(expression, new Tokenizer(variableNames, customFunctions).tokenize(expression), variableNames);
}
/**
* delegate the calculation of a simple expression without variables
*
* @return the result
*/
public double calculate() {
return calculate(null);
}
/**
* calculate the result of the expression and substitute the variables by
* their values beforehand
*
* @param values
* the variable values to be substituted
* @return the result of the calculation
* @throws IllegalArgumentException
* if the variables are invalid
*/
public double calculate(double... values) throws IllegalArgumentException {
if (getVariableNames() == null && values != null) {
throw new IllegalArgumentException("there are no variables to set values");
} else if (getVariableNames() != null && values == null && variableValues.isEmpty()) {
throw new IllegalAccessError("variable values have to be set");
} else if (values != null && values.length != getVariableNames().length) {
throw new IllegalArgumentException("The are an unequal number of variables and arguments");
}
int i = 0;
if (getVariableNames() != null && values != null) {
for (double val : values) {
variableValues.put(getVariableNames()[i++], val);
}
}
final Stack<Double> stack = new Stack<Double>();
for (final Token t : getTokens()) {
((CalculationToken) t).mutateStackForCalculation(stack, variableValues);
}
return stack.pop();
}
public void setVariable(String name, double value) {
variableValues.put(name, value);
}
}

View File

@ -0,0 +1,50 @@
/*
Copyright 2011 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.sf.openrocket.util.exp4j;
import java.util.Stack;
/**
* Superclass for tokenized Strings
*
* @author fas@congrace.de
*/
abstract class Token {
private final String value;
/**
* construct a new {@link Token}
*
* @param value
* the value of the {@link Token}
*/
Token(String value) {
super();
this.value = value;
}
/**
* get the value (String representation) of the token
*
* @return the value
*/
String getValue() {
return value;
}
abstract void mutateStackForInfixTranslation(Stack<Token> operatorStack, StringBuilder output);
}

View File

@ -0,0 +1,219 @@
/*
Copyright 2011 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.sf.openrocket.util.exp4j;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import net.sf.openrocket.util.exp4j.FunctionToken.Function;
/**
* Class for tokenizing mathematical expressions by breaking an expression up
* into multiple different {@link Token}s
*
* @author fas@congrace.de
*/
class Tokenizer {
private String[] variableNames;
private final Set<String> functionNames = new HashSet<String>();
private final Set<CustomFunction> customFunctions;
{
functionNames.add("abs");
functionNames.add("acos");
functionNames.add("asin");
functionNames.add("atan");
functionNames.add("cbrt");
functionNames.add("ceil");
functionNames.add("cos");
functionNames.add("cosh");
functionNames.add("exp");
functionNames.add("expm1");
functionNames.add("floor");
functionNames.add("log");
functionNames.add("sin");
functionNames.add("sinh");
functionNames.add("sqrt");
functionNames.add("tan");
functionNames.add("tanh");
}
Tokenizer() {
super();
customFunctions = null;
}
/**
* construct a new Tokenizer that recognizes variable names
*
* @param variableNames
* the variable names in the expression
* @throws IllegalArgumentException
* if a variable has the name as a function
* @param customFunctions
* the CustomFunction implementations used if the variableNames
* are not valid
*/
Tokenizer(String[] variableNames, Set<CustomFunction> customFunctions) throws IllegalArgumentException {
super();
this.variableNames = variableNames;
if (variableNames != null) {
for (String varName : variableNames) {
if (functionNames.contains(varName.toLowerCase())) {
throw new IllegalArgumentException("Variable '" + varName + "' can not have the same name as a function");
}
}
}
this.customFunctions = customFunctions;
}
private Token getCustomFunctionToken(String name) throws UnknownFunctionException {
for (CustomFunction func : customFunctions) {
if (func.getValue().equals(name)) {
return func;
}
}
throw new UnknownFunctionException(name);
}
private boolean isCustomFunction(String name) {
if (customFunctions == null) {
return false;
}
for (CustomFunction func : customFunctions) {
if (func.getValue().equals(name)) {
return true;
}
}
return false;
}
/**
* check if a char is part of a number
*
* @param c
* the char to be checked
* @return true if the char is part of a number
*/
private boolean isDigit(char c) {
return Character.isDigit(c) || c == '.';
}
private boolean isFunction(String name) {
for (Function fn : Function.values()) {
if (fn.name().equals(name.toUpperCase())) {
return true;
}
}
return false;
}
/**
* check if a String is a variable name
*
* @param name
* the variable name which is checked to be valid the char to be
* checked
* @return true if the char is a variable name (e.g. x)
*/
private boolean isVariable(String name) {
if (variableNames != null) {
for (String var : variableNames) {
if (name.equals(var)) {
return true;
}
}
}
return false;
}
/**
* tokenize an infix expression by breaking it up into different
* {@link Token} that can represent operations,functions,numbers,
* parenthesis or variables
*
* @param infix
* the infix expression to be tokenized
* @return the {@link Token}s representing the expression
* @throws UnparsableExpressionException
* when the expression is invalid
* @throws UnknownFunctionException
* when an unknown function name has been used.
*/
Token[] tokenize(String infix) throws UnparsableExpressionException, UnknownFunctionException {
final List<Token> tokens = new ArrayList<Token>();
final char[] chars = infix.toCharArray();
// iterate over the chars and fork on different types of input
Token lastToken;
for (int i = 0; i < chars.length; i++) {
char c = chars[i];
if (c == ' ')
continue;
if (isDigit(c)) {
final StringBuilder valueBuilder = new StringBuilder(1);
// handle the numbers of the expression
valueBuilder.append(c);
int numberLen = 1;
while (chars.length > i + numberLen && isDigit(chars[i + numberLen])) {
valueBuilder.append(chars[i + numberLen]);
numberLen++;
}
i += numberLen - 1;
lastToken = new NumberToken(valueBuilder.toString());
} else if (Character.isLetter(c) || c == '_') {
// can be a variable or function
final StringBuilder nameBuilder = new StringBuilder();
nameBuilder.append(c);
int offset = 1;
while (chars.length > i + offset && (Character.isLetter(chars[i + offset]) || Character.isDigit(chars[i + offset]) || chars[i + offset] == '_')) {
nameBuilder.append(chars[i + offset++]);
}
String name = nameBuilder.toString();
if (this.isVariable(name)) {
// a variable
i += offset - 1;
lastToken = new VariableToken(name);
} else if (this.isFunction(name)) {
// might be a function
i += offset - 1;
lastToken = new FunctionToken(name);
} else if (this.isCustomFunction(name)) {
// a custom function
i += offset - 1;
lastToken = getCustomFunctionToken(name);
} else {
// an unknown symbol was encountered
throw new UnparsableExpressionException(c, i);
}
}else if (c == ',') {
// a function separator, hopefully
lastToken=new FunctionSeparatorToken();
} else if (OperatorToken.isOperator(c)) {
lastToken = new OperatorToken(String.valueOf(c), OperatorToken.getOperation(c));
} else if (c == '(' || c == ')' || c == '[' || c == ']' || c == '{' || c == '}') {
lastToken = new ParenthesisToken(String.valueOf(c));
} else {
// an unknown symbol was encountered
throw new UnparsableExpressionException(c, i);
}
tokens.add(lastToken);
}
return tokens.toArray(new Token[tokens.size()]);
}
}

View File

@ -0,0 +1,37 @@
/*
Copyright 2011 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.sf.openrocket.util.exp4j;
/**
* Exception for handling unknown Functions.
*
* @see FunctionToken
* @author fas@congrace.de
*/
public class UnknownFunctionException extends Exception {
private static final long serialVersionUID = 1L;
/**
* construct a new {@link UnknownFunctionException}
*
* @param functionName
* the function name which could not be found
*/
public UnknownFunctionException(String functionName) {
super("Unknown function: " + functionName);
}
}

View File

@ -0,0 +1,47 @@
/*
Copyright 2011 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.sf.openrocket.util.exp4j;
/**
* Exception for invalid expressions
*
* @author fas@congrace.de
*/
public class UnparsableExpressionException extends Exception {
private static final long serialVersionUID = 1L;
/**
* construct a new {@link UnparsableExpressionException}
*
* @param c
* the character which could not be parsed
* @param pos
* the position of the character in the expression
*/
public UnparsableExpressionException(char c, int pos) {
super("Unable to parse character at position " + pos + ": '" + String.valueOf(c) + "'");
}
/**
* construct a new {@link UnparsableExpressionException}
*
* @param msg
* the error message
*/
public UnparsableExpressionException(String msg) {
super(msg);
}
}

View File

@ -0,0 +1,48 @@
/*
Copyright 2011 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.sf.openrocket.util.exp4j;
import java.util.Map;
import java.util.Stack;
/**
* A {@link Token} for representing variables
*
* @author fas
*/
class VariableToken extends CalculationToken {
/**
* construct a new {@link VariableToken}
*
* @param value
* the value of the token
*/
VariableToken(String value) {
super(value);
}
@Override
void mutateStackForCalculation(Stack<Double> stack, Map<String, Double> variableValues) {
double value = variableValues.get(this.getValue());
stack.push(value);
}
@Override
void mutateStackForInfixTranslation(Stack<Token> operatorStack, StringBuilder output) {
output.append(this.getValue()).append(" ");
}
}