Fixed negative number bug in exp4j and custom expressions.
[debian/openrocket] / core / src / net / sf / openrocket / simulation / customexpression / CustomExpression.java
1 package net.sf.openrocket.simulation.customexpression;
2
3 import java.util.List;
4 import java.util.regex.*;
5
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;
19
20 /**
21  * Represents a single custom expression
22  * @author Richard Graham
23  *
24  */
25 public class CustomExpression implements Cloneable{
26         
27         private static final LogHelper log = Application.getLogger();
28         
29         private OpenRocketDocument doc;
30         private String name, symbol, unit;
31
32         protected String expression;
33         private ExpressionBuilder builder;
34         private List<CustomExpression> subExpressions = new ArrayList<CustomExpression>();
35         
36         public CustomExpression(OpenRocketDocument doc){
37                 setName("");
38                 setSymbol("");
39                 setUnit("");
40                 setExpression("");
41                 this.doc = doc;
42         }
43         
44         public CustomExpression(OpenRocketDocument doc, 
45                                                         String name, 
46                                                         String symbol, 
47                                                         String unit, 
48                                                         String expression) {
49                 this.doc = doc;
50                 
51                 setName(name);
52                 setSymbol(symbol);
53                 setUnit(unit);
54                 setExpression(expression);
55         }
56         
57         /*
58          * Sets the long name of this expression, e.g. 'Kinetic energy'  
59          */
60         public void setName(String name){
61                 this.name = name;
62         }
63         
64         /*
65          * Sets the string for the units of the result of this expression.
66          */
67         public void setUnit(String unit){
68                 this.unit = unit;
69         }
70         
71         /*
72          * Sets the symbol string. This is the short, locale independent symbol for this whole expression
73          */
74         public void setSymbol(String symbol){
75                 this.symbol = symbol;
76         }
77         
78         /*
79          * Sets the actual expression string for this expression
80          */
81         public void setExpression(String expression){
82                 
83                 // This is the expression as supplied
84                 this.expression = expression;
85                 
86                 // Replace any indexed variables
87                 expression = subTimeIndexes(expression);
88                 expression = subTimeRanges(expression);
89                 
90                 builder = new ExpressionBuilder(expression);
91                 for (String n : getAllSymbols()){
92                         builder.withVariable(new Variable(n));
93                 }
94                 for (CustomExpression exp : this.subExpressions){
95                         builder.withVariable(new Variable(exp.hash()));
96                 }
97         
98                 builder.withCustomFunctions(Functions.getInstance().getAllFunction());
99                 log.info("Built expression "+expression);
100         }
101         
102         /*
103          * Replaces expressions of the form:
104          *   a[x:y]  with a hash and creates an associated RangeExpression from x to y
105          */
106         private String subTimeRanges(String str){
107                 
108                 Pattern p = Pattern.compile(variableRegex()+"\\[[^\\]]*:.*?\\]");
109                 Matcher m = p.matcher(str);
110                 
111                 // for each match, make a new custom expression (in subExpressions) with a hashed name
112                 // and replace the expression and variable in the original expression string with [hash].
113                 while (m.find()){
114                         String match = m.group();
115                         
116                         int start = match.indexOf("[");
117                         int end = match.indexOf("]");
118                         int colon = match.indexOf(":");
119                         
120                         String startTime = match.substring(start+1, colon);
121                         String endTime = match.substring(colon+1, end);
122                         String variableType = match.substring(0, start);
123                         
124                         RangeExpression exp = new RangeExpression(doc, startTime, endTime, variableType);
125                         subExpressions.add( exp );
126                         str = str.replace(match, exp.hash());
127                 }
128                 return str;
129         }
130         
131         /*
132          * Replaces expressions of the form
133          *   a[x]    with a hash and creates an associated IndexExpression with x
134          */
135         private String subTimeIndexes(String str){
136                 
137                 // find any matches of the time-indexed variable notation, e.g. m[1.2] for mass at 1.2 sec
138                 Pattern p = Pattern.compile(variableRegex()+"\\[[^:]*?\\]");
139                 Matcher m = p.matcher(str);
140                                 
141                 // for each match, make a new custom expression (in subExpressions) with a hashed name
142                 // and replace the expression and variable in the original expression string with [hash].
143                 while (m.find()){
144                         String match = m.group();
145                         // just the index part (in the square brackets) :
146                         String indexText = match.substring(match.indexOf("[")+1, match.length()-1);
147                         // just the flight data type
148                         String typeText = match.substring(0, match.indexOf("[")); 
149                                                 
150                         // Do the replacement and add a corresponding new IndexExpression to the list
151                         IndexExpression exp = new IndexExpression(doc, indexText, typeText);
152                         subExpressions.add( exp );
153                         str = str.replace(match, exp.hash());
154                 }
155                 return str;
156         }
157         
158         /*
159          * Returns a string of the form (t|a| ... ) with all variable symbols available
160          * This is useful for regex evaluation
161          */
162         protected String variableRegex(){
163                 String regex = "(";
164                 for (String s : getAllSymbols()){
165                         regex = regex + s + "|";
166                 }
167                 regex = regex.substring(0, regex.length()-1) + ")";
168                 return regex;
169         }
170         
171         // get a list of all the names of all the available variables
172         protected ArrayList<String> getAllNames(){
173                 ArrayList<String> names = new ArrayList<String>();
174                 for (FlightDataType type : FlightDataType.ALL_TYPES)
175                         names.add(type.getName());
176
177                 if (doc != null){
178                         List<CustomExpression> expressions = doc.getCustomExpressions();
179                         for (CustomExpression exp : expressions ){
180                                 if (exp != this)
181                                         names.add(exp.getName());
182                         }
183                 }
184                 return names;
185         }
186         
187         // get a list of all the symbols of the available variables ignoring this one
188         protected ArrayList<String> getAllSymbols(){
189                 ArrayList<String> symbols = new ArrayList<String>();
190                 for (FlightDataType type : FlightDataType.ALL_TYPES)
191                         symbols.add(type.getSymbol());
192                 
193                 if (doc != null){
194                         for (CustomExpression exp : doc.getCustomExpressions() ){
195                                 if (exp != this)
196                                         symbols.add(exp.getSymbol());
197                         }
198                 }
199                 return symbols;
200         }
201         
202         public boolean checkSymbol(){
203                 if ("".equals(symbol.trim()))
204                         return false;
205                 
206                 // No bad characters
207                 for (char c : "0123456789.,()[]{}<>:#@%^&*$ ".toCharArray())
208                         if (symbol.indexOf(c) != -1 )
209                                 return false;
210                 
211                 // No operators (ignoring brackets)
212                 for (String s : Functions.AVAILABLE_OPERATORS.keySet()){
213                         if (symbol.equals(s.trim().replaceAll("\\(|\\)|\\]|\\[|:", "")))
214                                 return false;
215                 }
216                 
217                 // No already defined symbols
218                 ArrayList<String> symbols = getAllSymbols().clone();
219                 if (symbols.contains(symbol.trim())){
220                         int index = symbols.indexOf(symbol.trim());
221                         log.user("Symbol "+symbol+" already exists, found "+symbols.get(index));
222                         return false;
223                 }
224                 
225                 return true;
226         }
227         
228         public boolean checkName(){
229                 if ("".equals(name.trim()))
230                         return false;
231                 
232                 // No characters that could mess things up saving etc
233                 for (char c : ",()[]{}<>#$".toCharArray())
234                         if (name.indexOf(c) != -1 )
235                                 return false;
236                 
237                 ArrayList<String> names = getAllNames().clone();
238                 if (names.contains(name.trim())){
239                         int index = names.indexOf(name.trim());
240                         log.user("Name "+name+" already exists, found "+names.get(index));
241                         return false;
242                 }
243                 
244                 return true;
245         }
246         
247         // Currently no restrictions on unit
248         public boolean checkUnit(){
249                 return true;
250         }
251         
252         public boolean checkAll(){
253                 return checkUnit() && checkSymbol() && checkName() && checkExpression();
254         }
255         
256         public String getName(){
257                 return name;
258         }
259         
260         public String getSymbol(){
261                 return symbol;
262         }
263         
264         public String getUnit(){
265                 return unit;
266         }
267         
268         public String getExpressionString(){
269                 return expression;
270         }
271         
272         /**
273          * Performs a basic check to see if the current expression string is valid
274          * This includes checking for bad characters and balanced brackets and test
275          * building the expression.
276          */
277         public boolean checkExpression(){
278                 if ("".equals(expression.trim())){
279                         return false;
280                 }
281                 
282                 int round = 0, square = 0; // count of bracket openings
283                 for (char c : expression.toCharArray()){
284                         switch (c) {
285                                 case '(' : round++; break;
286                                 case ')' : round--; break;
287                                 case '[' : square++; break;
288                                 case ']' : square--; break;
289                                 case ':' : 
290                                         if (square <= 0){
291                                                 log.user(": found outside range expression");
292                                                 return false;
293                                         }
294                                         else break;
295                                 case '#' : return false;
296                                 case '$' : return false;
297                                 case '=' : return false;
298                         }
299                 }
300                 if (round != 0 || square != 0) {
301                         log.user("Expression has unballanced brackets");
302                         return false;
303                 }
304                 
305                 
306                 //// Define the available variables as empty
307                 // The built in data types
308                 for (FlightDataType type : FlightDataType.ALL_TYPES){
309                         builder.withVariable(new Variable(type.getSymbol()));
310                 }
311                 
312                 for (String symb : getAllSymbols()){
313                         builder.withVariable(new Variable(symb));
314                 }
315                 
316                 // Try to build
317                 try {
318                         builder.build();
319                 } catch (Exception e) {
320                         log.user("Custom expression invalid : " + e.toString());
321                         return false;
322                 }
323                 
324                 
325                 // Otherwise, all OK
326                 return true;
327         }
328         
329         public Double evaluateDouble(SimulationStatus status){
330                 return evaluate(status).getDoubleValue();
331         }
332         
333         /*
334          * Builds the expression, done automatically during evaluation. Logs any errors. Returns null in case of error.
335          */
336         protected Calculable buildExpression(){
337                 return buildExpression(builder);
338         }
339         
340         /*
341          * Builds a specified expression, log any errors and returns null in case of error.
342          */
343         protected Calculable buildExpression(ExpressionBuilder b){
344                 Calculable calc;
345                 try {
346                         calc = b.build();
347                 } catch (UnknownFunctionException e1) {
348                         log.user("Unknown function. Could not build custom expression "+name);
349                         return null;
350                 } catch (UnparsableExpressionException e1) {
351                         log.user("Unparsable expression. Could not build custom expression "+name+". "+e1.getMessage());
352                         return null;
353                 }
354                 
355                 return calc;
356         }
357         
358         /*
359          * Evaluate the expression using the last variable values from the simulation status.
360          * Returns NaN on any error.
361          */
362         public Variable evaluate(SimulationStatus status){
363                 
364                 Calculable calc = buildExpression(builder);
365                 if (calc == null){
366                         return new Variable("Unknown");
367                 }
368                 
369                 // Evaluate any sub expressions and set associated variables in the calculable
370                 for (CustomExpression expr : this.subExpressions){
371                         calc.setVariable( expr.evaluate(status) );
372                 }
373                         
374                 // Set all the built-in variables. Strictly we surely won't need all of them
375                 // Going through and checking them to include only the ones used *might* give a speedup
376                 for (FlightDataType type : status.getFlightData().getTypes()){
377                         double value = status.getFlightData().getLast(type); 
378                         calc.setVariable( new Variable(type.getSymbol(), value ) );
379                 }
380                 
381                 double result = Double.NaN;
382                 try{
383                         result = calc.calculate().getDoubleValue();
384                 }
385                 catch (java.util.EmptyStackException e){
386                         log.user("Unable to calculate expression "+this.expression+" due to empty stack exception");
387                 }
388                         
389                 return new Variable(name, result);
390         }
391         
392         /*
393          * Returns the new flight data type corresponding to this calculated data
394          * If the unit matches a SI unit string then the datatype will have the corresponding unitgroup.
395          * Otherwise, a fixed unit group will be created
396          */
397         public FlightDataType getType(){
398                 
399                 UnitGroup ug = UnitGroup.SIUNITS.get(unit); 
400                 if ( ug == null ){
401                         ug = new FixedUnitGroup(unit);
402                 }
403                 
404                 FlightDataType type = FlightDataType.getType(name, symbol, ug);
405                 
406                 //log.debug(this.getClass().getSimpleName()+" returned type "+type.getName()+" (" + type.getSymbol() + ")" );           
407                 
408                 return type;
409         }
410         
411         /*
412          * Add this expression to the document if valid and not in document already
413          */
414         public void addToDocument(){
415                 // Abort if exact expression already in
416                 List<CustomExpression> expressions = doc.getCustomExpressions();
417                 if ( !expressions.isEmpty() ) {
418                         // check if expression already exists
419                         if ( expressions.contains(this) ){
420                                 log.user("Expression already in document. This unit : "+this.getUnit()+", existing unit : "+expressions.get(0).getUnit());
421                                 return;
422                         }
423                 }
424                         
425                 if (this.checkAll()){
426                         log.user("Custom expression added to rocket document");
427                         doc.addCustomExpression( this );
428                 }
429         }
430         
431         /*
432          * Removes this expression from the document, replacing it with a given new expression
433          */
434         public void overwrite(CustomExpression newExpression){
435                 if (!doc.getCustomExpressions().contains(this)) 
436                         return;
437                 else {
438                         int index = doc.getCustomExpressions().indexOf(this);
439                         doc.getCustomExpressions().set(index, newExpression);
440                 }
441         }
442         
443         @Override
444         public String toString(){
445                 return "Custom expression : "+this.name.toString()+ " " + this.expression.toString();
446         }
447         
448         @Override
449         /*
450          * Clone method makes a deep copy of everything except the reference to the document.
451          * If you want to apply this to another simulation, set simulation manually after cloning.
452          * @see java.lang.Object#clone()
453          */
454         public Object clone() {
455               try {
456                   return super.clone();
457               }
458               catch( CloneNotSupportedException e )
459               {
460                   return new CustomExpression(  doc  , 
461                                 new String(this.getName()), 
462                                 new String(this.getSymbol()),
463                                 new String(this.getUnit()),
464                                 new String(this.getExpressionString()));
465               }
466         } 
467         
468         /*
469          * Returns a simple all upper case string hash code with a proceeding $ mark.
470          * Used for temporary substitution when evaluating index and range expressions.
471          */
472         public String hash(){
473                 Integer hashint = new Integer(this.getExpressionString().hashCode());
474                 String hash = "$";
475                 for (char c : hashint.toString().toCharArray()){
476                         char newc = (char) (c + 17);
477                         hash = hash + newc;
478                 }
479                 return hash;
480         }
481         
482         @Override
483         public boolean equals(Object obj){
484                 CustomExpression other = (CustomExpression) obj;
485                 
486                 return ( this.getName().equals( other.getName() ) && 
487                                  this.getSymbol().equals( other.getSymbol() ) &&
488                                  this.getExpressionString().equals( other.getExpressionString() ) &&
489                                  this.getUnit().equals( other.getUnit() )
490                                 );
491         }
492
493         @Override
494         public int hashCode() {
495                 return hash().hashCode();
496         }
497 }