1 package net.sf.openrocket.simulation.customexpression;
4 import java.util.regex.*;
6 import net.sf.openrocket.document.OpenRocketDocument;
7 import net.sf.openrocket.logging.LogHelper;
8 import net.sf.openrocket.simulation.FlightDataType;
9 import net.sf.openrocket.simulation.SimulationStatus;
10 import net.sf.openrocket.startup.Application;
11 import net.sf.openrocket.unit.FixedUnitGroup;
12 import net.sf.openrocket.unit.UnitGroup;
13 import net.sf.openrocket.util.ArrayList;
14 import de.congrace.exp4j.Calculable;
15 import de.congrace.exp4j.ExpressionBuilder;
16 import de.congrace.exp4j.UnknownFunctionException;
17 import de.congrace.exp4j.UnparsableExpressionException;
18 import de.congrace.exp4j.Variable;
21 * Represents a single custom expression
22 * @author Richard Graham
25 public class CustomExpression implements Cloneable{
27 private static final LogHelper log = Application.getLogger();
29 private OpenRocketDocument doc;
30 private String name, symbol, unit;
32 protected String expression;
33 private ExpressionBuilder builder;
34 private List<CustomExpression> subExpressions = new ArrayList<CustomExpression>();
36 public CustomExpression(OpenRocketDocument doc){
45 public CustomExpression(OpenRocketDocument doc,
55 setExpression(expression);
59 * Sets the long name of this expression, e.g. 'Kinetic energy'
61 public void setName(String name){
66 * Sets the string for the units of the result of this expression.
68 public void setUnit(String unit){
73 * Sets the symbol string. This is the short, locale independent symbol for this whole expression
75 public void setSymbol(String symbol){
80 * Sets the actual expression string for this expression
82 public void setExpression(String expression){
84 // This is the expression as supplied
85 this.expression = expression;
87 // Replace any indexed variables
88 expression = subTimeIndexes(expression);
89 expression = subTimeRanges(expression);
91 builder = new ExpressionBuilder(expression);
92 for (String n : getAllSymbols()){
93 builder.withVariable(new Variable(n));
95 for (CustomExpression exp : this.subExpressions){
96 builder.withVariable(new Variable(exp.hash()));
99 builder.withCustomFunctions(Functions.getInstance().getAllFunction());
100 log.info("Built expression "+expression);
104 * Replaces expressions of the form:
105 * a[x:y] with a hash and creates an associated RangeExpression from x to y
107 private String subTimeRanges(String str){
109 Pattern p = Pattern.compile(variableRegex()+"\\[[^\\]]*:.*?\\]");
110 Matcher m = p.matcher(str);
112 // for each match, make a new custom expression (in subExpressions) with a hashed name
113 // and replace the expression and variable in the original expression string with [hash].
115 String match = m.group();
117 int start = match.indexOf("[");
118 int end = match.indexOf("]");
119 int colon = match.indexOf(":");
121 String startTime = match.substring(start+1, colon);
122 String endTime = match.substring(colon+1, end);
123 String variableType = match.substring(0, start);
125 RangeExpression exp = new RangeExpression(doc, startTime, endTime, variableType);
126 subExpressions.add( exp );
127 str = str.replace(match, exp.hash());
133 * Replaces expressions of the form
134 * a[x] with a hash and creates an associated IndexExpression with x
136 private String subTimeIndexes(String str){
138 // find any matches of the time-indexed variable notation, e.g. m[1.2] for mass at 1.2 sec
139 Pattern p = Pattern.compile(variableRegex()+"\\[[^:]*?\\]");
140 Matcher m = p.matcher(str);
142 // for each match, make a new custom expression (in subExpressions) with a hashed name
143 // and replace the expression and variable in the original expression string with [hash].
145 String match = m.group();
146 // just the index part (in the square brackets) :
147 String indexText = match.substring(match.indexOf("[")+1, match.length()-1);
148 // just the flight data type
149 String typeText = match.substring(0, match.indexOf("["));
151 // Do the replacement and add a corresponding new IndexExpression to the list
152 IndexExpression exp = new IndexExpression(doc, indexText, typeText);
153 subExpressions.add( exp );
154 str = str.replace(match, exp.hash());
160 * Returns a string of the form (t|a| ... ) with all variable symbols available
161 * This is useful for regex evaluation
163 protected String variableRegex(){
165 for (String s : getAllSymbols()){
166 regex = regex + s + "|";
168 regex = regex.substring(0, regex.length()-1) + ")";
172 // get a list of all the names of all the available variables
173 protected ArrayList<String> getAllNames(){
174 ArrayList<String> names = new ArrayList<String>();
176 for (FlightDataType type : FlightDataType.ALL_TYPES)
177 names.add(type.getName());
180 List<CustomExpression> expressions = doc.getCustomExpressions();
181 for (CustomExpression exp : expressions ){
183 names.add(exp.getName());
187 for (FlightDataType type : doc.getFlightDataTypes()){
188 String symb = type.getName();
189 if (name == null) continue;
191 if (!name.equals( this.getName() )){
198 // get a list of all the symbols of the available variables ignoring this one
199 protected ArrayList<String> getAllSymbols(){
200 ArrayList<String> symbols = new ArrayList<String>();
202 for (FlightDataType type : FlightDataType.ALL_TYPES)
203 symbols.add(type.getSymbol());
206 for (CustomExpression exp : doc.getCustomExpressions() ){
208 symbols.add(exp.getSymbol());
212 for (FlightDataType type : doc.getFlightDataTypes()){
213 String symb = type.getSymbol();
214 if (!symb.equals( this.getSymbol() )){
222 public boolean checkSymbol(){
223 if ("".equals(symbol.trim()))
227 for (char c : "0123456789.,()[]{}<>:#@%^&*$ ".toCharArray())
228 if (symbol.indexOf(c) != -1 )
231 // No operators (ignoring brackets)
232 for (String s : Functions.AVAILABLE_OPERATORS.keySet()){
233 if (symbol.equals(s.trim().replaceAll("\\(|\\)|\\]|\\[|:", "")))
237 // No already defined symbols
238 ArrayList<String> symbols = getAllSymbols().clone();
239 if (symbols.contains(symbol.trim())){
240 int index = symbols.indexOf(symbol.trim());
241 log.user("Symbol "+symbol+" already exists, found "+symbols.get(index));
248 public boolean checkName(){
249 if ("".equals(name.trim()))
252 // No characters that could mess things up saving etc
253 for (char c : ",()[]{}<>#$".toCharArray())
254 if (name.indexOf(c) != -1 )
257 ArrayList<String> names = getAllNames().clone();
258 if (names.contains(name.trim())){
259 int index = names.indexOf(name.trim());
260 log.user("Name "+name+" already exists, found "+names.get(index));
267 // Currently no restrictions on unit
268 public boolean checkUnit(){
272 public boolean checkAll(){
273 return checkUnit() && checkSymbol() && checkName() && checkExpression();
276 public String getName(){
280 public String getSymbol(){
284 public String getUnit(){
288 public String getExpressionString(){
293 * Performs a basic check to see if the current expression string is valid
294 * This includes checking for bad characters and balanced brackets and test
295 * building the expression.
297 public boolean checkExpression(){
298 if ("".equals(expression.trim())){
302 int round = 0, square = 0; // count of bracket openings
303 for (char c : expression.toCharArray()){
305 case '(' : round++; break;
306 case ')' : round--; break;
307 case '[' : square++; break;
308 case ']' : square--; break;
311 log.user(": found outside range expression");
315 case '#' : return false;
316 case '$' : return false;
317 case '=' : return false;
320 if (round != 0 || square != 0) {
321 log.user("Expression has unballanced brackets");
326 //// Define the available variables as empty
327 // The built in data types
329 for (FlightDataType type : FlightDataType.ALL_TYPES){
330 builder.withVariable(new Variable(type.getSymbol()));
333 for (String symb : getAllSymbols()){
334 builder.withVariable(new Variable(symb));
337 for (FlightDataType type : doc.getFlightDataTypes()){
338 builder.withVariable(new Variable(type.getSymbol()));
344 } catch (Exception e) {
345 log.user("Custom expression " + this.toString() + " invalid : " + e.toString());
354 public Double evaluateDouble(SimulationStatus status){
355 double result = evaluate(status).getDoubleValue();
356 if (result == Double.NEGATIVE_INFINITY || result == Double.POSITIVE_INFINITY) result = Double.NaN;
361 * Builds the expression, done automatically during evaluation. Logs any errors. Returns null in case of error.
363 protected Calculable buildExpression(){
364 return buildExpression(builder);
368 * Builds a specified expression, log any errors and returns null in case of error.
370 protected Calculable buildExpression(ExpressionBuilder b){
371 Calculable calc = null;
374 } catch (UnknownFunctionException e1) {
375 log.user("Unknown function. Could not build custom expression "+this.toString());
377 } catch (UnparsableExpressionException e1) {
378 log.user("Unparsable expression. Could not build custom expression "+this.toString()+". "+e1.getMessage());
386 * Evaluate the expression using the last variable values from the simulation status.
387 * Returns NaN on any error.
389 public Variable evaluate(SimulationStatus status){
391 Calculable calc = buildExpression(builder);
393 return new Variable("Unknown");
396 // Evaluate any sub expressions and set associated variables in the calculable
397 for (CustomExpression expr : this.subExpressions){
398 calc.setVariable( expr.evaluate(status) );
401 // Set all the built-in variables. Strictly we surely won't need all of them
402 // Going through and checking them to include only the ones used *might* give a speedup
403 for (FlightDataType type : status.getFlightData().getTypes()){
404 double value = status.getFlightData().getLast(type);
405 calc.setVariable( new Variable(type.getSymbol(), value ) );
408 double result = Double.NaN;
410 result = calc.calculate().getDoubleValue();
412 catch (java.util.EmptyStackException e){
413 log.user("Unable to calculate expression "+this.expression+" due to empty stack exception");
416 return new Variable(name, result);
420 * Returns the new flight data type corresponding to this calculated data
421 * If the unit matches a SI unit string then the datatype will have the corresponding unitgroup.
422 * Otherwise, a fixed unit group will be created
424 public FlightDataType getType(){
427 UnitGroup ug = UnitGroup.SIUNITS.get(unit);
429 log.debug("SI unit not found for "+unit+" in expression "+toString()+". Making a new fixed unit.");
430 ug = new FixedUnitGroup(unit);
432 //UnitGroup ug = new FixedUnitGroup(unit);
434 FlightDataType type = FlightDataType.getType(name, symbol, ug);
436 //log.debug(this.getClass().getSimpleName()+" returned type "+type.getName()+" (" + type.getSymbol() + ")" );
442 * Add this expression to the document if valid and not in document already
444 public void addToDocument(){
445 // Abort if exact expression already in
446 List<CustomExpression> expressions = doc.getCustomExpressions();
447 if ( !expressions.isEmpty() ) {
448 // check if expression already exists
449 if ( expressions.contains(this) ){
450 log.user("Expression already in document. This unit : "+this.getUnit()+", existing unit : "+expressions.get(0).getUnit());
455 if (this.checkAll()){
456 log.user("Custom expression added to rocket document");
457 doc.addCustomExpression( this );
462 * Removes this expression from the document, replacing it with a given new expression
464 public void overwrite(CustomExpression newExpression){
465 if (!doc.getCustomExpressions().contains(this))
468 int index = doc.getCustomExpressions().indexOf(this);
469 doc.getCustomExpressions().set(index, newExpression);
474 public String toString(){
475 return "[Expression name="+this.name.toString()+ " expression=" + this.expression+" unit="+this.unit+"]";
480 * Clone method makes a deep copy of everything except the reference to the document.
481 * If you want to apply this to another simulation, set simulation manually after cloning.
482 * @see java.lang.Object#clone()
484 public Object clone() {
486 return super.clone();
488 catch( CloneNotSupportedException e )
490 return new CustomExpression( doc ,
491 new String(this.getName()),
492 new String(this.getSymbol()),
493 new String(this.getUnit()),
494 new String(this.getExpressionString()));
499 * Returns a simple all upper case string hash code with a proceeding $ mark.
500 * Used for temporary substitution when evaluating index and range expressions.
502 public String hash(){
503 Integer hashint = new Integer(this.getExpressionString().hashCode() + symbol.hashCode());
505 for (char c : hashint.toString().toCharArray()){
506 if (c == '-') c = '0';
507 char newc = (char) (c + 17);
514 public boolean equals(Object obj){
515 CustomExpression other = (CustomExpression) obj;
517 return ( this.getName().equals( other.getName() ) &&
518 this.getSymbol().equals( other.getSymbol() ) &&
519 this.getExpressionString().equals( other.getExpressionString() ) &&
520 this.getUnit().equals( other.getUnit() )
525 public int hashCode() {
526 return hash().hashCode();