From: richardgraham Date: Thu, 17 May 2012 06:08:05 +0000 (+0000) Subject: Added ability for doublemodel to evaluate math expressions using exp4j, fixed typeove... X-Git-Tag: upstream/12.09^2~257 X-Git-Url: https://git.gag.com/?a=commitdiff_plain;h=fdd4269bf96572bedfaceaad21a4594808469f47;p=debian%2Fopenrocket Added ability for doublemodel to evaluate math expressions using exp4j, fixed typeover issue and made the doublemodel more robust to incorrect input. git-svn-id: https://openrocket.svn.sourceforge.net/svnroot/openrocket/trunk@691 180e2498-e6e9-4542-8430-84ac67f01cd8 --- diff --git a/core/src/net/sf/openrocket/gui/SpinnerEditor.java b/core/src/net/sf/openrocket/gui/SpinnerEditor.java index 6d2ca1b8..be367f03 100644 --- a/core/src/net/sf/openrocket/gui/SpinnerEditor.java +++ b/core/src/net/sf/openrocket/gui/SpinnerEditor.java @@ -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); } } diff --git a/core/src/net/sf/openrocket/gui/adaptors/DoubleModel.java b/core/src/net/sf/openrocket/gui/adaptors/DoubleModel.java index 30da01f9..b79903cd 100644 --- a/core/src/net/sf/openrocket/gui/adaptors/DoubleModel.java +++ b/core/src/net/sf/openrocket/gui/adaptors/DoubleModel.java @@ -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 diff --git a/core/src/net/sf/openrocket/gui/dialogs/AboutDialog.java b/core/src/net/sf/openrocket/gui/dialogs/AboutDialog.java index 1bad92f0..c3f9295a 100644 --- a/core/src/net/sf/openrocket/gui/dialogs/AboutDialog.java +++ b/core/src/net/sf/openrocket/gui/dialogs/AboutDialog.java @@ -43,7 +43,8 @@ public class AboutDialog extends JDialog { "OpenRocket utilizes the following libraries:

" + "MiG Layout (http://www.miglayout.com/)
" + "JFreeChart (http://www.jfree.org/jfreechart/)
" + - "iText (http://www.itextpdf.com/)"; + "iText (http://www.itextpdf.com/)
" + + "exp4j (http://projects.congrace.de/exp4j/index.html)"; public AboutDialog(JFrame parent) { diff --git a/core/src/net/sf/openrocket/unit/FractionalUnit.java b/core/src/net/sf/openrocket/unit/FractionalUnit.java index 26436478..5a7a9806 100644 --- a/core/src/net/sf/openrocket/unit/FractionalUnit.java +++ b/core/src/net/sf/openrocket/unit/FractionalUnit.java @@ -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); } } diff --git a/core/src/net/sf/openrocket/util/exp4j/AbstractExpression.java b/core/src/net/sf/openrocket/util/exp4j/AbstractExpression.java new file mode 100644 index 00000000..908e5075 --- /dev/null +++ b/core/src/net/sf/openrocket/util/exp4j/AbstractExpression.java @@ -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; + } + +} diff --git a/core/src/net/sf/openrocket/util/exp4j/Calculable.java b/core/src/net/sf/openrocket/util/exp4j/Calculable.java new file mode 100644 index 00000000..3fecba51 --- /dev/null +++ b/core/src/net/sf/openrocket/util/exp4j/Calculable.java @@ -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); +} diff --git a/core/src/net/sf/openrocket/util/exp4j/CalculationToken.java b/core/src/net/sf/openrocket/util/exp4j/CalculationToken.java new file mode 100644 index 00000000..75d9dc97 --- /dev/null +++ b/core/src/net/sf/openrocket/util/exp4j/CalculationToken.java @@ -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 stack, Map variableValues); + +} diff --git a/core/src/net/sf/openrocket/util/exp4j/CommandlineInterpreter.java b/core/src/net/sf/openrocket/util/exp4j/CommandlineInterpreter.java new file mode 100644 index 00000000..443545f0 --- /dev/null +++ b/core/src/net/sf/openrocket/util/exp4j/CommandlineInterpreter.java @@ -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. + * + * + *
+ * java de.congrace.exp4j.CommandlineInterpreter "2 * log(2.2223) - ((2-3.221) * 14.232^2)"
+ * > 248.91042049521056
+ * 
+ * + * @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()); + } +} diff --git a/core/src/net/sf/openrocket/util/exp4j/CustomFunction.java b/core/src/net/sf/openrocket/util/exp4j/CustomFunction.java new file mode 100644 index 00000000..8a437af2 --- /dev/null +++ b/core/src/net/sf/openrocket/util/exp4j/CustomFunction.java @@ -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
+ *
+ * Example
+ *
{@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);
+ * }
+ * + * @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 stack, Map variableValues) { + double[] args=new double[argc]; + for (int i=0;i operatorStack, StringBuilder output) { + operatorStack.push(this); + } + public int getArgumentCount() { + return argc; + } +} diff --git a/core/src/net/sf/openrocket/util/exp4j/ExpressionBuilder.java b/core/src/net/sf/openrocket/util/exp4j/ExpressionBuilder.java new file mode 100644 index 00000000..8e4a4307 --- /dev/null +++ b/core/src/net/sf/openrocket/util/exp4j/ExpressionBuilder.java @@ -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 variables = new LinkedHashMap(); + private final Set customFunctions = new HashSet(); + + 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 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 variableMap) { + for (Entry v : variableMap.entrySet()) { + variables.put(v.getKey(), v.getValue()); + } + return this; + } +} diff --git a/core/src/net/sf/openrocket/util/exp4j/FunctionSeparatorToken.java b/core/src/net/sf/openrocket/util/exp4j/FunctionSeparatorToken.java new file mode 100644 index 00000000..0e69f8ad --- /dev/null +++ b/core/src/net/sf/openrocket/util/exp4j/FunctionSeparatorToken.java @@ -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 operatorStack, StringBuilder output) { + Token token; + while (!((token=operatorStack.peek()) instanceof ParenthesisToken) && !token.getValue().equals("(")){ + output.append(operatorStack.pop().getValue()).append(" "); + } + } + +} diff --git a/core/src/net/sf/openrocket/util/exp4j/FunctionToken.java b/core/src/net/sf/openrocket/util/exp4j/FunctionToken.java new file mode 100644 index 00000000..10c4ba47 --- /dev/null +++ b/core/src/net/sf/openrocket/util/exp4j/FunctionToken.java @@ -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 stack, Map variableValues) { + stack.push(this.applyFunction(stack.pop())); + } + + @Override + void mutateStackForInfixTranslation(Stack operatorStack, StringBuilder output) { + operatorStack.push(this); + } +} diff --git a/core/src/net/sf/openrocket/util/exp4j/InfixTranslator.java b/core/src/net/sf/openrocket/util/exp4j/InfixTranslator.java new file mode 100644 index 00000000..a1070092 --- /dev/null +++ b/core/src/net/sf/openrocket/util/exp4j/InfixTranslator.java @@ -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 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 operatorStack = new Stack(); + 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(); + } +} diff --git a/core/src/net/sf/openrocket/util/exp4j/InvalidCustomFunctionException.java b/core/src/net/sf/openrocket/util/exp4j/InvalidCustomFunctionException.java new file mode 100644 index 00000000..de2da3cc --- /dev/null +++ b/core/src/net/sf/openrocket/util/exp4j/InvalidCustomFunctionException.java @@ -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); + } +} diff --git a/core/src/net/sf/openrocket/util/exp4j/NumberToken.java b/core/src/net/sf/openrocket/util/exp4j/NumberToken.java new file mode 100644 index 00000000..97017bda --- /dev/null +++ b/core/src/net/sf/openrocket/util/exp4j/NumberToken.java @@ -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 stack, Map variableValues) { + stack.push(this.doubleValue); + } + + @Override + void mutateStackForInfixTranslation(Stack operatorStack, StringBuilder output) { + output.append(this.getValue()).append(' '); + } +} diff --git a/core/src/net/sf/openrocket/util/exp4j/OperatorToken.java b/core/src/net/sf/openrocket/util/exp4j/OperatorToken.java new file mode 100644 index 00000000..cff78846 --- /dev/null +++ b/core/src/net/sf/openrocket/util/exp4j/OperatorToken.java @@ -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 stack, Map 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 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); + } +} diff --git a/core/src/net/sf/openrocket/util/exp4j/ParenthesisToken.java b/core/src/net/sf/openrocket/util/exp4j/ParenthesisToken.java new file mode 100644 index 00000000..2a365811 --- /dev/null +++ b/core/src/net/sf/openrocket/util/exp4j/ParenthesisToken.java @@ -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 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(); + } + } +} diff --git a/core/src/net/sf/openrocket/util/exp4j/PostfixExpression.java b/core/src/net/sf/openrocket/util/exp4j/PostfixExpression.java new file mode 100644 index 00000000..36ef5799 --- /dev/null +++ b/core/src/net/sf/openrocket/util/exp4j/PostfixExpression.java @@ -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.
+ * 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 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 variableValues = new HashMap(); + + /** + * 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 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 stack = new Stack(); + for (final Token t : getTokens()) { + ((CalculationToken) t).mutateStackForCalculation(stack, variableValues); + } + return stack.pop(); + } + + public void setVariable(String name, double value) { + variableValues.put(name, value); + } +} diff --git a/core/src/net/sf/openrocket/util/exp4j/Token.java b/core/src/net/sf/openrocket/util/exp4j/Token.java new file mode 100644 index 00000000..3e0024cc --- /dev/null +++ b/core/src/net/sf/openrocket/util/exp4j/Token.java @@ -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 operatorStack, StringBuilder output); +} diff --git a/core/src/net/sf/openrocket/util/exp4j/Tokenizer.java b/core/src/net/sf/openrocket/util/exp4j/Tokenizer.java new file mode 100644 index 00000000..8290de35 --- /dev/null +++ b/core/src/net/sf/openrocket/util/exp4j/Tokenizer.java @@ -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 functionNames = new HashSet(); + private final Set 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 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 tokens = new ArrayList(); + 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()]); + } +} diff --git a/core/src/net/sf/openrocket/util/exp4j/UnknownFunctionException.java b/core/src/net/sf/openrocket/util/exp4j/UnknownFunctionException.java new file mode 100644 index 00000000..a7a6beb9 --- /dev/null +++ b/core/src/net/sf/openrocket/util/exp4j/UnknownFunctionException.java @@ -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); + } +} diff --git a/core/src/net/sf/openrocket/util/exp4j/UnparsableExpressionException.java b/core/src/net/sf/openrocket/util/exp4j/UnparsableExpressionException.java new file mode 100644 index 00000000..deddcb9b --- /dev/null +++ b/core/src/net/sf/openrocket/util/exp4j/UnparsableExpressionException.java @@ -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); + } +} diff --git a/core/src/net/sf/openrocket/util/exp4j/VariableToken.java b/core/src/net/sf/openrocket/util/exp4j/VariableToken.java new file mode 100644 index 00000000..1d6a336e --- /dev/null +++ b/core/src/net/sf/openrocket/util/exp4j/VariableToken.java @@ -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 stack, Map variableValues) { + double value = variableValues.get(this.getValue()); + stack.push(value); + } + + @Override + void mutateStackForInfixTranslation(Stack operatorStack, StringBuilder output) { + output.append(this.getValue()).append(" "); + } +}