View Javadoc
1   package org.djutils.eval;
2   
3   import java.lang.reflect.Field;
4   import java.util.ArrayList;
5   import java.util.Collection;
6   import java.util.HashMap;
7   import java.util.List;
8   import java.util.Map;
9   
10  import org.djunits.unit.AbsoluteLinearUnit;
11  import org.djunits.unit.DimensionlessUnit;
12  import org.djunits.unit.TimeUnit;
13  import org.djunits.unit.Unit;
14  import org.djunits.unit.si.SIDimensions;
15  import org.djunits.value.vdouble.scalar.Dimensionless;
16  import org.djunits.value.vdouble.scalar.SIScalar;
17  import org.djunits.value.vdouble.scalar.Time;
18  import org.djunits.value.vdouble.scalar.base.Constants;
19  import org.djunits.value.vdouble.scalar.base.DoubleScalar;
20  import org.djunits.value.vdouble.scalar.base.DoubleScalarAbs;
21  import org.djunits.value.vdouble.scalar.base.DoubleScalarRel;
22  import org.djutils.exceptions.Throw;
23  import org.djutils.metadata.MetaData;
24  import org.djutils.metadata.ObjectDescriptor;
25  
26  /**
27   * Eval - evaluate mathematical expression. Derived from software developed between 2002-2016 by Peter Knoppers.
28   * <p>
29   * CONSIDER: implement string datatype.
30   * <p>
31   * The precedence of binary operators follows the list of the
32   * <a href="https://www.cs.bilkent.edu.tr/~guvenir/courses/CS101/op_precedence.html">Java Operator Precedence Table</a>,
33   * skipping bitwise and other operators that make no sense for this evaluator and adding the exponentiation (^) operator.
34   * </p>
35   * @author <a href="https://www.tudelft.nl/pknoppers">Peter Knoppers</a>
36   */
37  public class Eval
38  {
39      /** The expression evaluation stack. */
40      private List<Object> stack = new ArrayList<>();
41  
42      /** The expression. */
43      private String expression;
44  
45      /** Position of next token in the expression. */
46      private int position = 0;
47  
48      /** Access to the variable pool. */
49      private RetrieveValue retrieveValue = null;
50  
51      /** Binding strength of ternary conditional. */
52      private static final int BIND_CONDITIONAL_EXPRESSION = 1;
53  
54      /** Binding strength of boolean OR operation. */
55      private static final int BIND_OR = 2;
56  
57      /** Binding strength of boolean AND operation. */
58      private static final int BIND_AND = 3;
59  
60      /** Binding strength of == and != operators. */
61      private static final int BIND_EQUAL = 4;
62  
63      /** Binding strength of <, <=, >, >= relational operators. */
64      private static final int BIND_RELATIONAL = 5;
65  
66      /** Binding strength of addition and subtraction operators. */
67      private static final int BIND_ADD = 6;
68  
69      /** Binding strength of multiplication and division operators. */
70      private static final int BIND_MUL = 7;
71  
72      /** Binding strength of the power operation. */
73      private static final int BIND_POW = 8;
74  
75      /** Binding strength of unary minus and logical negation. */
76      private static final int BIND_UMINUS = 9;
77  
78      /** Object descriptor array for all zero argument functions. */
79      private static final ObjectDescriptor[] noArguments = new ObjectDescriptor[] {};
80  
81      /** The built-in functions. */
82      // @formatter:off
83      private final Function[] builtinFunctions = new Function[] {
84          new F0("AVOGADRO", Constants.AVOGADRO, new MetaData("Avogadro constant", "Avogadro constant in 1/mol", noArguments)),
85          new F0("BOLTZMANN", Constants.BOLTZMANN, new MetaData("Boltzmann constant",
86                  "The exact value of the Boltzmann constant in Joule per Kelvin", noArguments)),
87          new F0("CESIUM133_FREQUENCY", Constants.CESIUM133_FREQUENCY, new MetaData("Cesium 133 frequency", 
88                  "The exact value of the Cesium 133 ground state hyperfine structure transition frequency", noArguments)),
89          new F0("CURRENTTIME", Time.ZERO.getClass(),
90                  new MetaData("The current time in seconds since 1970 UTC", 
91                          "The current time in seconds since 1970 UTC to the nearest ms as reported by the operating system", noArguments), 
92                  (f) -> new Time(System.currentTimeMillis() / 1000d, TimeUnit.BASE_SECOND)),
93          new F0("E", Constants.E, new MetaData("Euler\'s constant e", "Euler\'s constant e; the base of the natural logarithm")),
94          new F0("ELECTRONCHARGE", Constants.ELECTRONCHARGE, new MetaData("Electrical charge of one electron", 
95                  "The exact electrical charge of one electron", noArguments)),
96          new F0("ELECTRONMASS", Constants.ELECTRONMASS, new MetaData("Mass of an electron", 
97                  "Mass of an electron, the value of this physical constant has an uncertainty of 2.8e-40 kg", noArguments)),
98          new F0("G", Constants.G, new MetaData("Gravitational constant", 
99                  "Gravitational constant, a.k.a. Newtonian constant of gravitation. This is the 2018 best known approximation, which has an "
100                 + "uncertainty 1.5e-15 m^3/kgs^2", noArguments)),
101         new F0("LIGHTSPEED", Constants.LIGHTSPEED, new MetaData("Speed of light in vacuum", "The exact speed of light in vacuum", noArguments)),
102         new F0("LUMINOUS_EFFICACY_540THZ", Constants.LUMINOUS_EFFICACY_540THZ, new MetaData(
103                 "The luminous efficacy Kcd of monochromatic radiation of frequency 540×10^12 Hz (540 THz)",
104                 "The exact luminous efficacy Kcd of monochromatic radiation of frequency 540×10^12 Hz (540 THz)", noArguments)),
105         new F0("NEUTRONMASS", Constants.NEUTRONMASS, new MetaData("Mass of a neutron", 
106                 "Mass of a neutron. The value of this physical constant has an uncertainty of 9.5e-37 kg.", noArguments)),
107         new F0("PI", Constants.PI, new MetaData("Ratio of a half circumference of a circle and its radius", 
108                 "Ratio of a half circumference of a circle and its radius", noArguments)),
109         new F0("PHI", Constants.PHI, new MetaData("Golden ratio", "Golden ratio", noArguments)),
110         new F0("PLANCK", Constants.PLANCK, new MetaData("Planck constant; ratio of a photon's energy and its frequency",
111                 "Planck constant; the exact ratio of a photon's energy and its frequency", noArguments)),
112         new F0("PLANCKREDUCED", Constants.PLANCKREDUCED, new MetaData("Reduced Planck constant", 
113                 "Reduced Planck constant, a.k.a. angular Planck constant; Planck constant divided by 2 pi" ,noArguments)),
114         new F0("PROTONCHARGE", Constants.PROTONCHARGE, new MetaData("ElectricalCharge of one proton", "ElectricalCharge of one proton", noArguments)),
115         new F0("PROTONMASS", Constants.PROTONMASS, new MetaData("Mass of a proton", 
116                 "Mass of a proton. The value of this physical constant has an uncertainty of 5.1e-37", noArguments)),
117         new F0("TAU", Constants.TAU, new MetaData("Ratio of circumference of circle and its radius", 
118                 "Ratio of circumference of circle and its radius", noArguments)),
119         new F0("VACUUMIMPEDANCE", Constants.VACUUMIMPEDANCE, new MetaData("Impedance of vacuum", "Impedance of vacuum", noArguments)),
120         new F0("VACUUMPERMEABILITY", Constants.VACUUMPERMEABILITY, new MetaData("Permeability of vacuum",
121                 "Permeability of vacuum. The uncertainty of this value is 1.9e-16N/A^2", noArguments)),
122         new F0("VACUUMPERMITTIVITY", Constants.VACUUMPERMITTIVITY, new MetaData("Permittivity of vacuum.",
123                 "Permittivity of vacuum. The uncertainty of this value is 1.3e-21 F/m.", noArguments)),
124         new F0("TRUE", Boolean.TRUE, new MetaData("The logical value TRUE", "The logical value TRUE", noArguments)),
125         new F0("FALSE", Boolean.FALSE, new MetaData("The logical value FALSE", "The logical value FALSE", noArguments)),
126         new F1("acos", Dimensionless.class, new MetaData("acos", "returns the angle of which the cosine equals the value of the argument",
127                 new ObjectDescriptor("angle", "angle", Dimensionless.class)), (i, a) -> checkDimensionless(i, a).acos()),
128         new F1("asin", Dimensionless.class, new MetaData("asin", "returns the angle of which the sine equals the value of the argument",
129                 new ObjectDescriptor("angle", "angle", Dimensionless.class)), (i, a) -> checkDimensionless(i, a).asin()),
130         new F1("atan", Dimensionless.class, new MetaData("atan", "returns the angle of which the tangent equals the value of the argument",
131                 new ObjectDescriptor("angle", "angle", Dimensionless.class)), (i, a) -> checkDimensionless(i, a).atan()),
132         new F1("cbrt", Dimensionless.class, new MetaData("cbrt", "returns the cubic root of the value of the argument",
133                 new ObjectDescriptor("value", "value", Dimensionless.class)), (i, a) -> checkDimensionless(i, a).cbrt()),
134         new F1("cos", Dimensionless.class, new MetaData("cos", "returns the cosine of the value of the argument",
135                 new ObjectDescriptor("value", "value", Dimensionless.class)), (i, a) -> checkDimensionless(i, a).cos()),
136         new F1("cosh", Dimensionless.class, new MetaData("cosh", "returns the hyperbolic cosine of the value of the argument",
137                 new ObjectDescriptor("value", "value", Dimensionless.class)), (i, a) -> checkDimensionless(i, a).cosh()),
138         new F1("exp", Dimensionless.class, new MetaData("exp", "returns e to the power of the argument",
139                 new ObjectDescriptor("value", "value", Dimensionless.class)), (i, a) -> checkDimensionless(i, a).exp()),
140         new F1("expm1", Dimensionless.class, new MetaData("expm1", "returns e to the power of the argument minus 1",
141                 new ObjectDescriptor("value", "value", Dimensionless.class)), (i, a) -> checkDimensionless(i, a).expm1()),
142         new F1("log", Dimensionless.class, new MetaData("log", "returns natural logarithm (logarithm base e) of the argument",
143                 new ObjectDescriptor("value", "value", Dimensionless.class)), (i, a) -> checkDimensionless(i, a).log()),
144         new F1("log10", Dimensionless.class, new MetaData("log10", "returns logarithm base 10 of the argument",
145                 new ObjectDescriptor("value", "value", Dimensionless.class)), (i, a) -> checkDimensionless(i, a).log10()),
146         new F1("log1p", Dimensionless.class, new MetaData("log1p", "returns natural logarithm (logarithm base e) of the argument plus 1",
147                 new ObjectDescriptor("value", "value", Dimensionless.class)), (i, a) -> checkDimensionless(i, a).log1p()),
148         new F1("signum", Dimensionless.class, new MetaData("signum", "returns sign of the argument (1 if positive, -1 if negative, 0 if zero)",
149                 new ObjectDescriptor("value", "value", Dimensionless.class)), (i, a) -> checkDimensionless(i, a).signum()),
150         new F1("sin", Dimensionless.class, new MetaData("cos", "returns the sine of the value of the argument",
151                 new ObjectDescriptor("value", "value", Dimensionless.class)), (i, a) -> checkDimensionless(i, a).sin()),
152         new F1("sinh", Dimensionless.class, new MetaData("cosh", "returns the hyperbolic sine of the value of the argument",
153                 new ObjectDescriptor("value", "value", Dimensionless.class)), (i, a) -> checkDimensionless(i, a).sinh()),
154         new F1("sqrt", Dimensionless.class, new MetaData("cos", "returns the square root of the value of the argument",
155                 new ObjectDescriptor("value", "value", Dimensionless.class)), (i, a) -> checkDimensionless(i, a).sqrt()),
156         new F1("tan", Dimensionless.class, new MetaData("cos", "returns the tangent of the value of the argument",
157                 new ObjectDescriptor("value", "value", Dimensionless.class)), (i, a) -> checkDimensionless(i, a).tan()),
158         new F1("tanh", Dimensionless.class, new MetaData("cosh", "returns the hyperbolic tangent of the value of the argument",
159                 new ObjectDescriptor("value", "value", Dimensionless.class)), (i, a) -> checkDimensionless(i, a).tanh()),
160         new F2("pow", new MetaData("pow", "raises the first argument to the power of the second argument",                
161                 new ObjectDescriptor("base", "base", Dimensionless.class), 
162                 new ObjectDescriptor("exponent", "exponent", Dimensionless.class)),
163                 (i, b, p)-> performPower(b, p) ),
164         new F2("atan2", new MetaData("atan2", 
165                 "atan2 function (needs two DoubleScalarRel parameters that have the same SI dimensions)", 
166                 new ObjectDescriptor("y", "y", DoubleScalarRel.class),
167                 new ObjectDescriptor("x", "x", DoubleScalarRel.class)), (i, y, x) -> performAtan2(y, x)),
168         };
169     
170     // @formatter:on
171     /** Map of all the built-in functions. */
172     private Map<String, Function> functionData;
173 
174     {
175         this.functionData = new HashMap<>();
176         for (Function function : this.builtinFunctions)
177         {
178             this.functionData.put(function.getId(), function);
179         }
180     }
181 
182     /** User defined functions. */
183     private Map<String, Function> userDefinedFunctions = null;
184 
185     /** User supplied unit parser. */
186     private UnitParser userSuppliedUnitParser = null;
187 
188     /** Map from DoubleScalar sub classes to Quantities */
189     private Map<Class<?>, SIDimensions> siDimensionsMap = new HashMap<>();
190 
191     /**
192      * Construct a new evaluator with no RetrieveValue object and no added/overridden function and no added/overridden units.
193      */
194     public Eval()
195     {
196         // Nothing to do here
197     }
198 
199     /**
200      * Set or replace the RetrieveValue object of this evaluator.
201      * @param retrieveValue the new RetrieveValue object (may be null to only delete the currently active RetrieveValue object).
202      * @return this (for easy method chaining)
203      */
204     public Eval setRetrieveValue(final RetrieveValue retrieveValue)
205     {
206         this.retrieveValue = retrieveValue;
207         return this;
208     }
209 
210     /**
211      * Install a unit parser (or replace or remove a previously installed unit parser). A user supplied unit parser takes
212      * precedence over the built-in unit parser (that can only handle SI strings; see SIDimensions.of).
213      * @param unitParser the new unit parser or null to remove a previously installed unit parser
214      * @return this (for easy method chainging)
215      */
216     public Eval setUnitParser(final UnitParser unitParser)
217     {
218         this.userSuppliedUnitParser = unitParser;
219         return this;
220     }
221 
222     /**
223      * Evaluate a mathematical expression
224      * @param expression the expression
225      * @return The value of the evaluated expression
226      * @throws RuntimeException when the expression cannot be evaluated
227      */
228     public Object evaluate(final String expression) throws RuntimeException
229     {
230         return evaluateExpression(expression);
231     }
232 
233     /**
234      * Evaluate a mathematical expression, check that the result is a logical value and return that value as a Boolean
235      * @param expression the expression
236      * @return the result of the expression
237      * @throws RuntimeException when the expression could not be evaluated, or the result is not a logical value
238      */
239     public Boolean evaluateAsBoolean(final String expression) throws RuntimeException
240     {
241         Object result = evaluateExpression(expression);
242         if (!(result instanceof Boolean))
243         {
244             throwException("Result " + result + " can not be cast to a Boolean");
245         }
246         return ((Boolean) result);
247     }
248 
249     /**
250      * Evaluate a mathematical expression, check that the result is a floating point value and return that value as a double. If
251      * the result is strongly typed (some DJUNITS quantity), the SI value is returned.
252      * @param expression the expression
253      * @return the result of the expression
254      * @throws RuntimeException when the expression could not be evaluated, or the result is not a double value
255      */
256     public double evaluateAsDouble(final String expression) throws RuntimeException
257     {
258         Object result = evaluateExpression(expression);
259         if (!(result instanceof DoubleScalar<?, ?>))
260         {
261             throwException("Result " + result + " can not be cast to a double");
262         }
263         return ((DoubleScalar<?, ?>) result).si;
264     }
265 
266     /**
267      * Create and return a collection of all built in functions.
268      * @return all built in functions
269      */
270     public Collection<Function> builtInFunctions()
271     {
272         return this.functionData.values();
273     }
274 
275     /**
276      * Install a map of user-defined functions. If a built-in function has the same name as a user-defined function; the
277      * user-defined function takes precedence.
278      * @param userDefinedFunctionMap map that maps the name of the function to a Function object
279      * @return this (for easy method chaining)
280      */
281     public Eval setUserDefinedFunctions(final Map<String, Function> userDefinedFunctionMap)
282     {
283         this.userDefinedFunctions = userDefinedFunctionMap;
284         return this;
285     }
286 
287     /**
288      * Evaluate one expression.
289      * @param expression the expression to evaluate
290      * @return the result of the evaluation (DoubleScalar or Boolean)
291      * @throws RuntimeException when the expression could not be evaluated, or the result is not a logical value
292      */
293     public Object evaluateExpression(final String expression) throws RuntimeException
294     {
295         Throw.whenNull(expression, "expression may not be null");
296         Throw.when(expression.length() == 0, IllegalArgumentException.class, "Expression may not be the empty string");
297         String savedExpression = this.expression;
298         int savedPosition = this.position;
299         List<Object> savedStack = new ArrayList<>(this.stack);
300         try
301         {
302             this.expression = expression;
303             this.position = 0;
304             this.stack.clear();
305             eatSpace();
306             evalLhs(0);
307             if (this.position < this.expression.length())
308             {
309                 this.throwException("Trailing garbage: \"" + this.expression.substring(this.position) + "\"");
310             }
311             if (this.stack.size() > 1)
312             {
313                 this.throwException("Unfinished operations");
314             }
315             if (this.stack.size() <= 0)
316             {
317                 this.throwException("No result after evaluation");
318             }
319             return pop();
320         }
321         finally
322         {
323             this.expression = savedExpression;
324             this.position = savedPosition;
325             this.stack = savedStack;
326         }
327     }
328 
329     /**
330      * Increment position up to the next non-space character.
331      */
332     private void eatSpace()
333     {
334         while (this.position < this.expression.length() && Character.isWhitespace(this.expression.charAt(this.position)))
335         {
336             this.position++;
337         }
338     }
339 
340     /**
341      * Evaluate the left-hand-side of a binary operation.
342      * @param bindingStrength the binding strength of a pending binary operation (0 if no binary operation is pending)
343      * @throws RuntimeException on error
344      */
345     private void evalLhs(final int bindingStrength) throws RuntimeException
346     {
347         eatSpace();
348         if (this.position >= this.expression.length())
349         {
350             throwException("Missing operand");
351         }
352         char token = this.expression.charAt(this.position);
353         switch (token)
354         {
355             case '-': // Unary minus
356             {
357                 this.position++;
358                 evalLhs(BIND_UMINUS);
359                 Object value = pop();
360                 if (value instanceof DoubleScalar<?, ?>)
361                 {
362                     push(((DoubleScalar<?, ?>) value).neg());
363                     break;
364                 }
365                 throwException("Cannot apply unary minus on " + value);
366             }
367 
368             case '!':
369                 if (this.position >= this.expression.length() - 1 || '=' != this.expression.charAt(this.position + 1))
370                 {
371                     // unary logical negation
372                     this.position++;
373                     evalLhs(BIND_UMINUS);
374                     Object value = pop();
375                     if (value instanceof Boolean)
376                     {
377                         push(!((Boolean) value));
378                         break;
379                     }
380                     throwException("Cannot apply unary not operator on " + value);
381                 }
382                 break; // We should not get a "!=" operation here, but if we do, it results in an error later on
383 
384             case '(': // parenthesized expression
385                 this.position++;
386                 evalLhs(0);
387                 if (this.position >= this.expression.length() || ')' != this.expression.charAt(this.position))
388                 {
389                     throwException("Missing closing parenthesis");
390                 }
391                 this.position++; // step over the closing parenthesis
392                 break;
393 
394             default: // parse one operand
395             {
396                 if (Character.isLetter(token) || '#' == token || '@' == token || '_' == token)
397                 {
398                     push(handleFunctionOrVariableOrNamedConstant());
399                 }
400                 else if (Character.isDigit(token) || '.' == token)
401                 {
402                     push(handleNumber());
403                 }
404             }
405         }
406         evalRhs(bindingStrength);
407     }
408 
409     /**
410      * Evaluate the right hand sid of a mathematical expression. Stop at a closing parenthesis, or a binary operator that does
411      * not have a higher binding strength than the argument.
412      * @param bindingStrength if the next token is a binary operator with a lower or equal binding strength; return without
413      *            consuming that token.
414      * @throws RuntimeException when the expression is mathematically unsound
415      */
416     private void evalRhs(final int bindingStrength) throws RuntimeException
417     {
418         eatSpace();
419         while (this.position < this.expression.length())
420         {
421             char token = this.expression.charAt(this.position);
422             switch (token)
423             {
424                 case ')':
425                     return;
426 
427                 case '^': // power operator
428                     if (this.stack.isEmpty())
429                     {
430                         throwException("Missing left operand");
431                     }
432                     if (bindingStrength > BIND_POW) // Not >=; this ensures a^b^c is evaluated as a^(b^c); not (a^b)^c
433                     { // See https://en.wikipedia.org/wiki/Order_of_operations#Serial_exponentiation
434                         return; // Cannot happen
435                     }
436                     this.position++;
437                     evalLhs(BIND_POW);
438                     power();
439                     break;
440 
441                 case '*':
442                     if (this.stack.isEmpty())
443                     {
444                         throwException("Missing left operand");
445                     }
446                     if (bindingStrength >= BIND_MUL)
447                     {
448                         return;
449                     }
450                     this.position++;
451                     evalLhs(BIND_MUL);
452                     multiply();
453                     break;
454 
455                 case '/':
456                     if (this.stack.isEmpty())
457                     {
458                         throwException("Missing left operand");
459                     }
460                     if (bindingStrength >= BIND_MUL)
461                     {
462                         return;
463                     }
464                     this.position++;
465                     evalLhs(BIND_MUL);
466                     divide();
467                     break;
468 
469                 case '+':
470                     if (this.stack.isEmpty())
471                     {
472                         throwException("Missing left operand");
473                     }
474                     if (bindingStrength >= BIND_ADD)
475                     {
476                         return;
477                     }
478                     this.position++;
479                     evalLhs(BIND_ADD);
480                     add();
481                     break;
482 
483                 case '-':
484                     if (this.stack.isEmpty())
485                     {
486                         throwException("Missing left operand"); // Cannot happen; that minus would be considered unary
487                     }
488                     if (bindingStrength >= BIND_ADD)
489                     {
490                         return;
491                     }
492                     this.position++;
493                     evalLhs(BIND_ADD);
494                     subtract();
495                     break;
496 
497                 case '&': // boolean and with something
498                     if (this.position >= this.expression.length() - 1 || '&' != this.expression.charAt(this.position + 1))
499                     {
500                         throwException("Single \'&\' is not a valid operator");
501                     }
502                     if (this.stack.isEmpty())
503                     {
504                         throwException("Missing left operand");
505                     }
506                     if (bindingStrength >= BIND_AND)
507                     {
508                         return;
509                     }
510                     this.position += 2;
511                     evalLhs(BIND_AND);
512                     and();
513                     break;
514 
515                 case '|': // boolean or with something
516                     if (this.position >= this.expression.length() - 1 || '|' != this.expression.charAt(this.position + 1))
517                     {
518                         throwException("Single \'|\' is not a valid operator");
519                     }
520                     if (this.stack.isEmpty())
521                     {
522                         throwException("Missing left operand");
523                     }
524                     if (bindingStrength >= BIND_OR)
525                     {
526                         return;
527                     }
528                     this.position += 2;
529                     evalLhs(BIND_OR);
530                     or();
531                     break;
532 
533                 case '<':
534                     if (this.stack.isEmpty())
535                     {
536                         throwException("Missing left operand");
537                     }
538                     this.position++;
539                     if (this.position < this.expression.length() && '=' == this.expression.charAt(this.position))
540                     {
541                         this.position++;
542                         evalLhs(BIND_RELATIONAL);
543                         compareDoubleScalars((a, b) -> (a <= b));
544                     }
545                     else
546                     {
547                         evalLhs(BIND_RELATIONAL);
548                         compareDoubleScalars((a, b) -> (a < b));
549                     }
550                     break;
551 
552                 case '>':
553                     if (this.stack.isEmpty())
554                     {
555                         throwException("Missing left operand");
556                     }
557                     this.position++;
558                     if (this.position < this.expression.length() && '=' == this.expression.charAt(this.position))
559                     {
560                         this.position++;
561                         evalLhs(BIND_RELATIONAL);
562                         compareDoubleScalars((a, b) -> (a >= b));
563                     }
564                     else
565                     {
566                         evalLhs(BIND_RELATIONAL);
567                         compareDoubleScalars((a, b) -> (a > b));
568                     }
569                     break;
570 
571                 case '=':
572                 {
573                     if (this.position >= this.expression.length() - 1 || '=' != this.expression.charAt(this.position + 1))
574                     {
575                         throwException("Single \'=\' is not a valid operator");
576                     }
577                     if (this.stack.isEmpty())
578                     {
579                         throwException("Missing left operand");
580                     }
581                     this.position += 2;
582                     evalLhs(BIND_EQUAL);
583                     Object right = pop();
584                     Object left = pop();
585                     push(left.equals(right)); // This also works for Boolean operands.
586                     break;
587                 }
588 
589                 case '!':
590                 {
591                     if (this.position >= this.expression.length() - 1 || '=' != this.expression.charAt(this.position + 1))
592                     {
593                         throwException("Single \'!\' is not a valid operator");
594                     }
595                     if (this.stack.isEmpty())
596                     {
597                         throwException("Missing left operand");
598                     }
599                     this.position += 2;
600                     evalLhs(BIND_EQUAL);
601                     Object right = pop();
602                     Object left = pop();
603                     push(!left.equals(right)); // This also works for Boolean operands and mixed-type operands.
604                     break;
605                 }
606 
607                 case '?': // Conditional expression
608                 {
609                     if (bindingStrength >= BIND_CONDITIONAL_EXPRESSION)
610                     {
611                         return;
612                     }
613                     this.position++;
614                     // This is special as we really do not want to evaluate or cause side effects on the non-taken part
615                     Object choice = pop();
616                     if (!(choice instanceof Boolean))
617                     {
618                         throwException("Condition does not evaluate to a logical value");
619                     }
620                     if (!((Boolean) choice))
621                     {
622                         // Skip the "then" part
623                         skip(true);
624                     }
625                     else
626                     {
627                         evalLhs(0); // should consume everything up to the ':'
628                     }
629                     if (this.position >= this.expression.length() || ':' != this.expression.charAt(this.position))
630                     {
631                         throwException("Missing \':\' of conditional expression");
632                     }
633                     this.position++; // skip the ':'
634                     if ((Boolean) choice)
635                     {
636                         // Skip the "else" part
637                         skip(false);
638                     }
639                     else
640                     {
641                         evalLhs(BIND_CONDITIONAL_EXPRESSION);
642                     }
643                     break;
644                 }
645 
646                 case ':':
647                     return;
648 
649                 case ',':
650                     return;
651 
652                 default:
653                     throwException("Operator expected (got \"" + token + "\")");
654 
655             }
656         }
657     }
658 
659     /**
660      * Interface for comparator for two double values.
661      */
662     interface CompareValues
663     {
664         /**
665          * Compare two double values.
666          * @param argument1 the left argument of the comparator
667          * @param argument2 the right argument of the comparator
668          * @return the result type of the function
669          */
670         Object execute(double argument1, double argument2);
671 
672     }
673 
674     /**
675      * Pop two operands from the stack and compare them using the provided comparator lambda expression
676      * @param comparator a function that compares two DoubleScalar values.
677      */
678     private void compareDoubleScalars(final CompareValues comparator)
679     {
680         Object right = pop();
681         Object left = pop();
682         if ((left instanceof DoubleScalar) && (right instanceof DoubleScalar)
683                 && getDimensions((DoubleScalar<?, ?>) left).equals(getDimensions((DoubleScalar<?, ?>) right)))
684         {
685             push(comparator.execute(((DoubleScalar<?, ?>) left).si, ((DoubleScalar<?, ?>) right).si));
686             return;
687         }
688         throwException("Cannot compare " + left + " to " + right);
689     }
690 
691     /**
692      * Throw an exception because the expression cannot be evaluated.
693      * @param description description of the problem
694      * @throws RuntimeException always thrown
695      */
696     private void throwException(final String description) throws RuntimeException
697     {
698         throw new RuntimeException(description + " at position " + this.position);
699     }
700 
701     /**
702      * Skip the "then" or the "else" part of a conditional expression.
703      * @param thenPart if true; skip the then part (i.e. skip until a ':'); if false; skip the else part (i.e. until end of
704      *            expression of a binary operator)
705      */
706     private void skip(final boolean thenPart)
707     {
708         while (this.position < this.expression.length())
709         {
710             switch (this.expression.charAt(this.position))
711             {
712                 case '(': // Eat up everything until the matching closing parenthesis
713                 {
714                     int level = 1;
715                     this.position++;
716                     while (this.position < this.expression.length())
717                     {
718                         char c = this.expression.charAt(this.position);
719                         if ('(' == c)
720                         {
721                             level++;
722                         }
723                         else if (')' == c && --level == 0)
724                         {
725                             break;
726                         }
727                         this.position++;
728                     }
729                     if (level > 0)
730                     {
731                         throwException("Missing closing parenthesis");
732                     }
733                     this.position++; // skip the closing parenthesis
734                     break;
735                 }
736 
737                 case '?': // Nested conditional expression
738                     this.position++;
739                     skip(true);
740                     if (this.position >= this.expression.length() || ':' != this.expression.charAt(this.position))
741                     {
742                         throwException("Conditional expression missing \':\'");
743                     }
744                     this.position++;
745                     skip(false);
746                     break;
747 
748                 case ':':
749                     return;
750 
751                 case '^':
752                 case '*':
753                 case '/':
754                 case '+':
755                 case '-':
756                 case '<':
757                 case '=':
758                 case '>':
759                 case '!':
760                     if (!thenPart)
761                     {
762                         return; // Every operator has lower precedence than the else part
763                     }
764                     this.position++;
765                     break;
766 
767                 default:
768                     this.position++;
769                     break;
770             }
771         }
772         if (thenPart)
773         {
774             throwException("Nonterminated conditional expression");
775         }
776         // Else end of expression is end of else part
777     }
778 
779     /**
780      * Perform the boolean AND operation on the two top-most elements on the stack and push the result back onto the stack.
781      */
782     private void and()
783     {
784         Object right = pop();
785         Object left = pop();
786         if ((left instanceof Boolean) && (right instanceof Boolean))
787         {
788             push(((Boolean) left) && ((Boolean) right));
789             return;
790         }
791         throwException("Cannot compute logical AND of " + left + " and " + right);
792     }
793 
794     /**
795      * Perform the boolean OR operation on the two top-most elements on the stack and push the result back onto the stack.
796      */
797     private void or()
798     {
799         Object right = pop();
800         Object left = pop();
801         if ((left instanceof Boolean) && (right instanceof Boolean))
802         {
803             push(((Boolean) left) || ((Boolean) right));
804             return;
805         }
806         throwException("Cannot compute logical AND of " + left + " and " + right);
807     }
808 
809     /**
810      * Perform the power operation on the two top-most elements on stack and push the result back onto the stack.
811      */
812     private void power()
813     {
814         Object exponent = pop();
815         Object base = pop();
816         push(performPower(base, exponent));
817     }
818 
819     /**
820      * Perform the power operation on the two arguments and return the result
821      * @param base the base operand of the power operation
822      * @param exponent the exponent of the power operation
823      * @return the result of the power operation
824      */
825     private Object performPower(final Object base, final Object exponent)
826     {
827         if ((base instanceof DoubleScalarRel) && (exponent instanceof DoubleScalarRel)
828                 && getDimensions((DoubleScalarRel<?, ?>) base).equals(getDimensions(DimensionlessUnit.SI))
829                 && getDimensions((DoubleScalarRel<?, ?>) exponent).equals(getDimensions(DimensionlessUnit.SI)))
830         {
831             var result = Dimensionless.ofSI(Math.pow(((DoubleScalarRel<?, ?>) base).si, ((DoubleScalarRel<?, ?>) exponent).si));
832             // System.out.println(base + " ^ " + exponent + " = " + result);
833             return result;
834         }
835         throwException("Cannot raise " + base + " to power " + exponent);
836         return null; // Not reached
837     }
838 
839     /**
840      * Perform the atan2 function on the two arguments and return the result
841      * @param y should be some kind of DoubleScalarRel
842      * @param x should be some kind of DoubleScalarRel with the same SiDimensions as y
843      * @return in fact a DoubleScalarRel with a quantity matching Dimensionless
844      */
845     private Object performAtan2(final Object y, final Object x)
846     {
847         if ((y instanceof DoubleScalarRel) && (x instanceof DoubleScalarRel)
848                 && getDimensions((DoubleScalarRel<?, ?>) y).equals(getDimensions((DoubleScalarRel<?, ?>) x)))
849         {
850             var result = Dimensionless.ofSI(Math.atan2(((DoubleScalarRel<?, ?>) y).si, ((DoubleScalarRel<?, ?>) x).si));
851             // System.out.println(base + " ^ " + exponent + " = " + result);
852             return result;
853         }
854         throwException("Cannot compute atan2 of " + y + ", " + x + ")");
855         return null; // Not reached
856 
857     }
858 
859     /**
860      * Multiply the two top-most elements on the stack and push the result back onto the stack.
861      */
862     private void multiply()
863     {
864         Object right = pop();
865         Object left = pop();
866         if ((right instanceof DoubleScalarRel) && (left instanceof DoubleScalarRel))
867         {
868             push(((DoubleScalarRel<?, ?>) left).times((DoubleScalarRel<?, ?>) right));
869             return;
870         }
871         throwException("Cannot multiply with " + right + " as right hand operand");
872     }
873 
874     /**
875      * Divide the two top-most elements on the stack and push the result back onto the stack.
876      */
877     private void divide()
878     {
879         Object right = pop();
880         Object left = pop();
881         if ((left instanceof DoubleScalarRel) && (right instanceof DoubleScalarRel))
882         {
883             if (0.0 == ((DoubleScalarRel<?, ?>) right).si)
884             {
885                 throwException("Division by 0");
886             }
887             push(((DoubleScalarRel<?, ?>) left).divide((DoubleScalarRel<?, ?>) right));
888             return;
889         }
890         throwException("Cannot divide " + left + " by " + right);
891     }
892 
893     /**
894      * Add the two top-most elements on the stack and push the result back onto the stack.
895      */
896     @SuppressWarnings({"rawtypes", "unchecked"})
897     private void add()
898     {
899         Object right = pop();
900         Object left = pop();
901         if (!(left instanceof DoubleScalar))
902         {
903             throwException("Left operand of addition must be a scalar (got \"" + left + "\")");
904         }
905         if (!(right instanceof DoubleScalar))
906         {
907             throwException("Right operand of addition must be a scalar (got \"" + right + "\")");
908         }
909         // Both operands are DoubleScalar
910         if (!((DoubleScalar<?, ?>) left).getDisplayUnit()
911             .getQuantity()
912             .getSiDimensions()
913             .equals(getDimensions((DoubleScalar<?, ?>) right)))
914         {
915             // System.out.println("left: " + getDimensions((DoubleScalar<?, ?>) left));
916             // System.out.println("right: " + getDimensions((DoubleScalar<?, ?>) right));
917             throwException("Cannot add " + left + " to " + right + " because the types are incompatible");
918         }
919         // Operands are of compatible unit
920         if ((left instanceof DoubleScalarRel) && (right instanceof DoubleScalarRel))
921         {
922             // Rel + Rel -> Rel
923             var dsl = (DoubleScalarRel) left;
924             var dsr = (DoubleScalarRel) right;
925             var sum = dsl.plus(dsr);
926             // System.out.println(left + " + " + right + " = " + sum);
927             // Set display unit???
928             push(sum);
929             return;
930         }
931         if (right instanceof DoubleScalarAbs)
932         {
933             throwException("Cannot add an absolute value to some other value");
934         }
935         // Abs + Rel -> Abs
936         var dsl = (DoubleScalarAbs) left;
937         var dsr = (DoubleScalarRel) right;
938         var sum = dsl.instantiateAbs(dsl.si + dsr.si, (AbsoluteLinearUnit) dsl.getDisplayUnit().getStandardUnit());
939         // System.out.println(left + " + " + right + " = " + sum);
940         // sum.setDisplayUnit(ds.getDisplayUnit());
941         push(sum);
942     }
943 
944     /**
945      * Subtract the two top-most elements on the stack and push the result back onto the stack.
946      */
947     @SuppressWarnings({"rawtypes", "unchecked"})
948     private void subtract()
949     {
950         Object right = pop();
951         Object left = pop();
952         if (!(left instanceof DoubleScalar))
953         {
954             throwException("Left operand of subtraction must be a scalar (got \"" + left + "\")");
955         }
956         if (!(right instanceof DoubleScalar))
957         {
958             throwException("Right operand of subtraction must be a scalar (got \"" + right + "\")");
959         }
960         // Now we know that we're dealing with DoubleScalar objects
961         if (!getDimensions((DoubleScalar<?, ?>) left).equals(getDimensions((DoubleScalar<?, ?>) right)))
962         {
963             throwException("Cannot subtract " + right + " from " + left + " because the types are incompatible");
964         }
965         if ((left instanceof DoubleScalarAbs) && (right instanceof DoubleScalarAbs))
966         {
967             // Abs - Abs -> Rel
968             var dsl = (DoubleScalarAbs) left;
969             var dsr = (DoubleScalarAbs) right;
970             var difference = dsl.minus(dsr);
971             // System.out.println(left + " - " + right + " = " + difference);
972             push(difference);
973             return;
974         }
975         if ((left instanceof DoubleScalarAbs) && (right instanceof DoubleScalarRel))
976         {
977             // Abs - Rel -> Abs
978             var dsl = (DoubleScalarAbs) left;
979             var dsr = (DoubleScalarRel) right;
980             var difference = dsl.instantiateAbs(dsl.si - dsr.si, (AbsoluteLinearUnit) dsl.getDisplayUnit().getStandardUnit());
981             // System.out.println(left + " - " + right + " = " + difference);
982             push(difference);
983             return;
984         }
985         if ((left instanceof DoubleScalarRel) && (right instanceof DoubleScalarAbs))
986         {
987             // Rel - Abs -> error
988             throwException("Cannot subtract " + right + " from " + left + " because the right operand is absolute");
989         }
990         // Rel - Rel -> Rel
991         var dsl = (DoubleScalarRel) left;
992         var dsr = (DoubleScalarRel) right;
993         var difference = dsl.minus(dsr);
994         // System.out.println(left + " - " + right + " = " + difference);
995         push(difference);
996     }
997 
998     /**
999      * Parse a number and convert it to a SIScalar. If it is followed by an SI unit string inside square brackets, parse it into
1000      * the correct type.
1001      * @return the value of the parsed number or value
1002      */
1003     private DoubleScalar<?, ?> handleNumber()
1004     {
1005         // Parse a number value
1006         int startPosition = this.position;
1007         boolean seenExp = false;
1008         boolean seenExpSign = false;
1009         boolean seenExpDigit = false;
1010         boolean seenRadix = false;
1011         while (this.position < this.expression.length())
1012         {
1013             char c = this.expression.charAt(this.position);
1014             // Any leading plus and minus signs at the start of this value are consumed by evalLhs
1015             // A subsequent plus and minus sign indicates an addition or subtraction
1016             if (seenExp && (!seenExpDigit) && ('-' == c || '+' == c))
1017             {
1018                 if (seenExpSign)
1019                 {
1020                     throwException("Too many signs in exponent");
1021                 }
1022                 seenExpSign = true;
1023             }
1024             else if (seenExp && seenExpSign && (!Character.isDigit(c)))
1025             {
1026                 break;
1027             }
1028             else if ('e' == c || 'E' == c)
1029             {
1030                 if (seenExp)
1031                 {
1032                     throwException("Too many 'e' or 'E' indicators");
1033                 }
1034                 seenExp = true;
1035             }
1036             else if ('.' == c)
1037             {
1038                 if (seenRadix)
1039                 {
1040                     throwException("Too many '.'");
1041                 }
1042                 if (seenExp)
1043                 {
1044                     throwException("A \'.\' is not allowed after \'e\' or \'E\'");
1045                 }
1046                 seenRadix = true;
1047             }
1048             else if (Character.isDigit(c))
1049             {
1050                 if (seenExp)
1051                 {
1052                     seenExpDigit = true;
1053                 }
1054             }
1055             else
1056             {
1057                 break;
1058             }
1059             this.position++;
1060         }
1061         eatSpace();
1062         if (seenExp && !seenExpDigit)
1063         {
1064             throwException("Exponent value missing");
1065         }
1066         String number = this.expression.substring(startPosition, this.position);
1067         if (this.position < this.expression.length() && '[' == this.expression.charAt(this.position))
1068         {
1069             this.position++;
1070             startPosition = this.position;
1071             while (this.position < this.expression.length())
1072             {
1073                 char c = this.expression.charAt(this.position);
1074                 if (']' == c)
1075                 {
1076                     String unit = this.expression.substring(startPosition, this.position++);
1077                     DoubleScalar<?, ?> result = null;
1078                     if (null != this.userSuppliedUnitParser)
1079                     {
1080                         result = this.userSuppliedUnitParser.parseUnit(Double.parseDouble(number), unit);
1081                     }
1082                     if (null == result)
1083                     {
1084                         result = SIScalar.valueOf(number + " " + unit);
1085                     }
1086                     return result;
1087                 }
1088                 if ((!Character.isLetterOrDigit(c)) && '-' != c && '^' != c && '/' != c && '.' != c)
1089                 {
1090                     throwException("Bad symbol in SI unit string");
1091                 }
1092                 this.position++;
1093             }
1094             if (this.position >= this.expression.length())
1095             {
1096                 throwException("Missing closing bracket (\']\')");
1097             }
1098         }
1099         return SIScalar.valueOf(number); // No unit specified
1100     }
1101 
1102     /**
1103      * Process a function call, a variable interpolation, or a mathematical, or physical constant. Precondition: this.position
1104      * points to the first letter of the name of the function, variable, or constant.
1105      * @return the value of the function, variable, or mathematical or physical constant
1106      */
1107     private Object handleFunctionOrVariableOrNamedConstant()
1108     {
1109         int startPosition = this.position;
1110         while (this.position < this.expression.length())
1111         {
1112             char c = this.expression.charAt(this.position);
1113             if (Character.isLetterOrDigit(c) || '.' == c || '_' == c || '@' == c || '#' == c)
1114             {
1115                 this.position++;
1116             }
1117             else
1118             {
1119                 break;
1120             }
1121         }
1122         String name = this.expression.substring(startPosition, this.position);
1123         eatSpace();
1124         if (this.position >= this.expression.length() || this.expression.charAt(this.position) != '(')
1125         {
1126             // No opening parenthesis; name must be the name of a variable; look it up
1127             Object result = null == this.retrieveValue ? null : this.retrieveValue.lookup(name);
1128             if (null == result)
1129             {
1130                 throwException("Cannot resolve variable " + name);
1131             }
1132             // Do not check the type of the result to allow expression that return any kind of object
1133             return result;
1134         }
1135         // At an opening parenthesis. This signals the start of a function call; collect the parameters of the function on the
1136         // stack and count them
1137         this.position++;
1138         eatSpace();
1139         int argCount = 0;
1140         while (this.position < this.expression.length() && ')' != this.expression.charAt(this.position))
1141         {
1142             evalLhs(0);
1143             argCount++;
1144             eatSpace();
1145             if (this.position < this.expression.length() && ',' == this.expression.charAt(this.position))
1146             {
1147                 this.position++;
1148             }
1149         }
1150         if (this.position >= this.expression.length() || ')' != this.expression.charAt(this.position))
1151         {
1152             throwException("Missing closing parenthesis");
1153         }
1154         this.position++; // step over the closing parenthesis
1155         Function f = null;
1156         if (null != this.userDefinedFunctions)
1157         {
1158             f = this.userDefinedFunctions.get(name);
1159         }
1160         if (null == f)
1161         {
1162             f = this.functionData.get(name);
1163         }
1164         if (null == f)
1165         {
1166             throwException("Unknown function " + name);
1167         }
1168         Object[] args = new Object[argCount];
1169         for (int i = 0; i < argCount; i++)
1170         {
1171             args[argCount - i - 1] = pop();
1172         }
1173         if (f.getMetaData() != MetaData.NO_META_DATA)
1174         {
1175             // MetaData.verifyComposition does not handle this case correctly.
1176             int argsNeeded = f.getMetaData().getObjectDescriptors().length;
1177             if (argsNeeded != argCount)
1178             {
1179                 throwException(name + " needs " + argsNeeded + " parameter" + (argsNeeded == 1 ? "" : "s") + " (got " + argCount
1180                         + ")");
1181             }
1182             for (int i = 0; i < argCount; i++)
1183             {
1184                 if ((args[i] instanceof Boolean) && (!Boolean.class.isAssignableFrom(f.getMetaData().getObjectClass(i))))
1185                 {
1186                     throwException(name + " does not take " + args[i] + " as parameter " + i);
1187                 }
1188                 else if ((args[i] instanceof DoubleScalar)
1189                         && (DoubleScalar.class.isAssignableFrom(f.getMetaData().getObjectClass(i))))
1190                 {
1191                     DoubleScalar<?, ?> ds = (DoubleScalar<?, ?>) args[i];
1192                     Class<?> clazz = f.getMetaData().getObjectClass(i);
1193                     if (!clazz.equals(DoubleScalarRel.class))
1194                     {
1195                         SIDimensions siDimensions = this.siDimensionsMap.get(clazz);
1196                         if (null == siDimensions)
1197                         {
1198                             // Not in the cache
1199                             try
1200                             {
1201                                 Field field = clazz.getDeclaredField("ZERO"); // Every DoubleScalar type has this
1202                                 DoubleScalar<?, ?> zero = (DoubleScalar<?, ?>) field.get(clazz);
1203                                 siDimensions = zero.getDisplayUnit().getQuantity().getSiDimensions();
1204                                 this.siDimensionsMap.put(clazz, siDimensions); // Add this one to our map
1205                             }
1206                             catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException nsfe)
1207                             {
1208                                 throwException("ERROR: Cannot determine quantity for " + clazz.getCanonicalName());
1209                             }
1210                         }
1211                         if (!siDimensions.equals(getDimensions(ds)))
1212                         {
1213                             throwException("parameter " + i + " of " + name + " has incompatible quantity");
1214                         }
1215                     }
1216                 }
1217                 else
1218                 {
1219                     throwException("Argument " + i + " of function " + name + " is of an unhandled type (" + args[i] + ")");
1220                 }
1221             }
1222         }
1223         // All parameters are apparently compatible; invoke the function
1224         return (f.function(args));
1225     }
1226 
1227     /**
1228      * Push one object onto the evaluation stack.
1229      * @param object the object to push onto the evaluation stack
1230      */
1231     private void push(final Object object)
1232     {
1233         this.stack.add(object);
1234     }
1235 
1236     /**
1237      * Pop one object from the evaluation stack. Throw exception when stack underflows
1238      * @return the object popped from the evaluation stack
1239      * @throws RuntimeException when the stack is currently empty
1240      */
1241     private Object pop() throws RuntimeException
1242     {
1243         if (this.stack.isEmpty())
1244         {
1245             throwException("Stack empty");
1246         }
1247         return (this.stack.remove(this.stack.size() - 1));
1248     }
1249 
1250     /**
1251      * Convert an object to a Dimensionless if possible, or complain.
1252      * @param functionData meta data of the function that wants a Dimensionless
1253      * @param object object that supposedly can be converted to a Dimensionless
1254      * @return the result
1255      */
1256     private Dimensionless checkDimensionless(final Function functionData, final Object object)
1257     {
1258         if (!(object instanceof DoubleScalar))
1259         {
1260             throwException("Function " + functionData.getId() + " cannot be applied to " + object);
1261         }
1262         DoubleScalar<?, ?> ds = (DoubleScalar<?, ?>) object;
1263         if (!getDimensions(ds).equals(getDimensions(DimensionlessUnit.SI)))
1264         {
1265             throwException("Function " + functionData.getId() + " cannot be applied to " + ds);
1266         }
1267         return new Dimensionless(ds.si, DimensionlessUnit.SI);
1268     }
1269 
1270     /**
1271      * Retrieve the SIDimensions of a DoubleScalar.
1272      * @param doubleScalar the DoubleScalar
1273      * @return the SIDimensions object that describes the quantity of the DoubleScalar
1274      */
1275     private static SIDimensions getDimensions(final DoubleScalar<?, ?> doubleScalar)
1276     {
1277         return doubleScalar.getDisplayUnit().getQuantity().getSiDimensions();
1278     }
1279 
1280     /**
1281      * Retrieve the SIDimensions of a unit.
1282      * @param unit the unit
1283      * @return the SIDimensions unit
1284      */
1285     private static SIDimensions getDimensions(final Unit<?> unit)
1286     {
1287         return unit.getQuantity().getSiDimensions();
1288     }
1289 
1290 }