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.DimensionlessUnit;
11  import org.djunits.unit.TimeUnit;
12  import org.djunits.unit.Unit;
13  import org.djunits.unit.si.SIDimensions;
14  import org.djunits.value.vdouble.scalar.Dimensionless;
15  import org.djunits.value.vdouble.scalar.SIScalar;
16  import org.djunits.value.vdouble.scalar.Time;
17  import org.djunits.value.vdouble.scalar.base.Constants;
18  import org.djunits.value.vdouble.scalar.base.DoubleScalar;
19  import org.djunits.value.vdouble.scalar.base.DoubleScalarAbs;
20  import org.djunits.value.vdouble.scalar.base.DoubleScalarRel;
21  import org.djutils.exceptions.Throw;
22  import org.djutils.metadata.MetaData;
23  import org.djutils.metadata.ObjectDescriptor;
24  
25  /**
26   * Eval - evaluate mathematical expression. Derived from software developed between 2002-2016 by Peter Knoppers.
27   * <p>
28   * CONSIDER: implement string datatype.
29   * <p>
30   * The precedence of binary operators follows the list of the
31   * <a href="https://www.cs.bilkent.edu.tr/~guvenir/courses/CS101/op_precedence.html">Java Operator Precedence Table</a>,
32   * skipping bitwise and other operators that make no sense for this evaluator and adding the exponentiation (^) operator.
33   * </p>
34   * @author <a href="https://www.tudelft.nl/pknoppers">Peter Knoppers</a>
35   */
36  public class Eval
37  {
38      /** The expression evaluation stack. */
39      private List<Object> stack = new ArrayList<>();
40  
41      /** The expression. */
42      private String expression;
43  
44      /** Position of next token in the expression. */
45      private int position = 0;
46  
47      /** Access to the variable pool. */
48      private RetrieveValue retrieveValue = null;
49  
50      /** Binding strength of ternary conditional. */
51      private static final int BIND_CONDITIONAL_EXPRESSION = 1;
52  
53      /** Binding strength of boolean OR operation. */
54      private static final int BIND_OR = 2;
55  
56      /** Binding strength of boolean AND operation. */
57      private static final int BIND_AND = 3;
58  
59      /** Binding strength of == and != operators. */
60      private static final int BIND_EQUAL = 4;
61  
62      /** Binding strength of <, <=, >, >= relational operators. */
63      private static final int BIND_RELATIONAL = 5;
64  
65      /** Binding strength of addition and subtraction operators. */
66      private static final int BIND_ADD = 6;
67  
68      /** Binding strength of multiplication and division operators. */
69      private static final int BIND_MUL = 7;
70  
71      /** Binding strength of the power operation. */
72      private static final int BIND_POW = 8;
73  
74      /** Binding strength of unary minus and logical negation. */
75      private static final int BIND_UMINUS = 9;
76  
77      /** Object descriptor array for all zero argument functions. */
78      private static final ObjectDescriptor[] noArguments = new ObjectDescriptor[] {};
79  
80      /** The built-in functions. */
81      // @formatter:off
82      private final Function[] builtinFunctions = new Function[] {
83          new F0("AVOGADRO", Constants.AVOGADRO, new MetaData("Avogadro constant", "Avogadro constant in 1/mol", noArguments)),
84          new F0("BOLTZMANN", Constants.BOLTZMANN, new MetaData("Boltzmann constant",
85                  "The exact value of the Boltzmann constant in Joule per Kelvin", noArguments)),
86          new F0("CESIUM133_FREQUENCY", Constants.CESIUM133_FREQUENCY, new MetaData("Cesium 133 frequency", 
87                  "The exact value of the Cesium 133 ground state hyperfine structure transition frequency", noArguments)),
88          new F0("CURRENTTIME", Time.ZERO.getClass(),
89                  new MetaData("The current time in seconds since 1970 UTC", 
90                          "The current time in seconds since 1970 UTC to the nearest ms as reported by the operating system", noArguments), 
91                  (f) -> new Time(System.currentTimeMillis() / 1000d, TimeUnit.BASE_SECOND)),
92          new F0("E", Constants.E, new MetaData("Euler\'s constant e", "Euler\'s constant e; the base of the natural logarithm")),
93          new F0("ELECTRONCHARGE", Constants.ELECTRONCHARGE, new MetaData("Electrical charge of one electron", 
94                  "The exact electrical charge of one electron", noArguments)),
95          new F0("ELECTRONMASS", Constants.ELECTRONMASS, new MetaData("Mass of an electron", 
96                  "Mass of an electron, the value of this physical constant has an uncertainty of 2.8e-40 kg", noArguments)),
97          new F0("G", Constants.G, new MetaData("Gravitational constant", 
98                  "Gravitational constant, a.k.a. Newtonian constant of gravitation. This is the 2018 best known approximation, which has an "
99                  + "uncertainty 1.5e-15 m^3/kgs^2", noArguments)),
100         new F0("LIGHTSPEED", Constants.LIGHTSPEED, new MetaData("Speed of light in vacuum", "The exact speed of light in vacuum", noArguments)),
101         new F0("LUMINOUS_EFFICACY_540THZ", Constants.LUMINOUS_EFFICACY_540THZ, new MetaData(
102                 "The luminous efficacy Kcd of monochromatic radiation of frequency 540×10^12 Hz (540 THz)",
103                 "The exact luminous efficacy Kcd of monochromatic radiation of frequency 540×10^12 Hz (540 THz)", noArguments)),
104         new F0("NEUTRONMASS", Constants.NEUTRONMASS, new MetaData("Mass of a neutron", 
105                 "Mass of a neutron. The value of this physical constant has an uncertainty of 9.5e-37 kg.", noArguments)),
106         new F0("PI", Constants.PI, new MetaData("Ratio of a half circumference of a circle and its radius", 
107                 "Ratio of a half circumference of a circle and its radius", noArguments)),
108         new F0("PHI", Constants.PHI, new MetaData("Golden ratio", "Golden ratio", noArguments)),
109         new F0("PLANCK", Constants.PLANCK, new MetaData("Planck constant; ratio of a photon's energy and its frequency",
110                 "Planck constant; the exact ratio of a photon's energy and its frequency", noArguments)),
111         new F0("PLANCKREDUCED", Constants.PLANCKREDUCED, new MetaData("Reduced Planck constant", 
112                 "Reduced Planck constant, a.k.a. angular Planck constant; Planck constant divided by 2 pi" ,noArguments)),
113         new F0("PROTONCHARGE", Constants.PROTONCHARGE, new MetaData("ElectricalCharge of one proton", "ElectricalCharge of one proton", noArguments)),
114         new F0("PROTONMASS", Constants.PROTONMASS, new MetaData("Mass of a proton", 
115                 "Mass of a proton. The value of this physical constant has an uncertainty of 5.1e-37", noArguments)),
116         new F0("TAU", Constants.TAU, new MetaData("Ratio of circumference of circle and its radius", 
117                 "Ratio of circumference of circle and its radius", noArguments)),
118         new F0("VACUUMIMPEDANCE", Constants.VACUUMIMPEDANCE, new MetaData("Impedance of vacuum", "Impedance of vacuum", noArguments)),
119         new F0("VACUUMPERMEABILITY", Constants.VACUUMPERMEABILITY, new MetaData("Permeability of vacuum",
120                 "Permeability of vacuum. The uncertainty of this value is 1.9e-16N/A^2", noArguments)),
121         new F0("VACUUMPERMITTIVITY", Constants.VACUUMPERMITTIVITY, new MetaData("Permittivity of vacuum.",
122                 "Permittivity of vacuum. The uncertainty of this value is 1.3e-21 F/m.", noArguments)),
123         new F0("TRUE", Boolean.TRUE, new MetaData("The logical value TRUE", "The logical value TRUE", noArguments)),
124         new F0("FALSE", Boolean.FALSE, new MetaData("The logical value FALSE", "The logical value FALSE", noArguments)),
125         new F1("acos", Dimensionless.class, new MetaData("acos", "returns the angle of which the cosine equals the value of the argument",
126                 new ObjectDescriptor("angle", "angle", Dimensionless.class)), (i, a) -> checkDimensionless(i, a).acos()),
127         new F1("asin", Dimensionless.class, new MetaData("asin", "returns the angle of which the sine equals the value of the argument",
128                 new ObjectDescriptor("angle", "angle", Dimensionless.class)), (i, a) -> checkDimensionless(i, a).asin()),
129         new F1("atan", Dimensionless.class, new MetaData("atan", "returns the angle of which the tangent equals the value of the argument",
130                 new ObjectDescriptor("angle", "angle", Dimensionless.class)), (i, a) -> checkDimensionless(i, a).atan()),
131         new F1("cbrt", Dimensionless.class, new MetaData("cbrt", "returns the cubic root of the value of the argument",
132                 new ObjectDescriptor("value", "value", Dimensionless.class)), (i, a) -> checkDimensionless(i, a).cbrt()),
133         new F1("cos", Dimensionless.class, new MetaData("cos", "returns the cosine of the value of the argument",
134                 new ObjectDescriptor("value", "value", Dimensionless.class)), (i, a) -> checkDimensionless(i, a).cos()),
135         new F1("cosh", Dimensionless.class, new MetaData("cosh", "returns the hyperbolic cosine of the value of the argument",
136                 new ObjectDescriptor("value", "value", Dimensionless.class)), (i, a) -> checkDimensionless(i, a).cosh()),
137         new F1("exp", Dimensionless.class, new MetaData("exp", "returns e to the power of the argument",
138                 new ObjectDescriptor("value", "value", Dimensionless.class)), (i, a) -> checkDimensionless(i, a).exp()),
139         new F1("expm1", Dimensionless.class, new MetaData("expm1", "returns e to the power of the argument minus 1",
140                 new ObjectDescriptor("value", "value", Dimensionless.class)), (i, a) -> checkDimensionless(i, a).expm1()),
141         new F1("log", Dimensionless.class, new MetaData("log", "returns natural logarithm (logarithm base e) of the argument",
142                 new ObjectDescriptor("value", "value", Dimensionless.class)), (i, a) -> checkDimensionless(i, a).log()),
143         new F1("log10", Dimensionless.class, new MetaData("log10", "returns logarithm base 10 of the argument",
144                 new ObjectDescriptor("value", "value", Dimensionless.class)), (i, a) -> checkDimensionless(i, a).log10()),
145         new F1("log1p", Dimensionless.class, new MetaData("log1p", "returns natural logarithm (logarithm base e) of the argument plus 1",
146                 new ObjectDescriptor("value", "value", Dimensionless.class)), (i, a) -> checkDimensionless(i, a).log1p()),
147         new F1("signum", Dimensionless.class, new MetaData("signum", "returns sign of the argument (1 if positive, -1 if negative, 0 if zero)",
148                 new ObjectDescriptor("value", "value", Dimensionless.class)), (i, a) -> checkDimensionless(i, a).signum()),
149         new F1("sin", Dimensionless.class, new MetaData("cos", "returns the sine of the value of the argument",
150                 new ObjectDescriptor("value", "value", Dimensionless.class)), (i, a) -> checkDimensionless(i, a).sin()),
151         new F1("sinh", Dimensionless.class, new MetaData("cosh", "returns the hyperbolic sine of the value of the argument",
152                 new ObjectDescriptor("value", "value", Dimensionless.class)), (i, a) -> checkDimensionless(i, a).sinh()),
153         new F1("sqrt", Dimensionless.class, new MetaData("cos", "returns the square root of the value of the argument",
154                 new ObjectDescriptor("value", "value", Dimensionless.class)), (i, a) -> checkDimensionless(i, a).sqrt()),
155         new F1("tan", Dimensionless.class, new MetaData("cos", "returns the tangent of the value of the argument",
156                 new ObjectDescriptor("value", "value", Dimensionless.class)), (i, a) -> checkDimensionless(i, a).tan()),
157         new F1("tanh", Dimensionless.class, new MetaData("cosh", "returns the hyperbolic tangent of the value of the argument",
158                 new ObjectDescriptor("value", "value", Dimensionless.class)), (i, a) -> checkDimensionless(i, a).tanh()),
159         new F2("pow", new MetaData("pow", "raises the first argument to the power of the second argument",                
160                 new ObjectDescriptor("base", "base", Dimensionless.class), 
161                 new ObjectDescriptor("exponent", "exponent", Dimensionless.class)),
162                 (i, b, p)-> performPower(b, p) ),
163         new F2("atan2", new MetaData("atan2", 
164                 "atan2 function (needs two DoubleScalarRel parameters that have the same SI dimensions)", 
165                 new ObjectDescriptor("y", "y", DoubleScalarRel.class),
166                 new ObjectDescriptor("x", "x", DoubleScalarRel.class)), (i, y, x) -> performAtan2(y, x)),
167         };
168     
169     // @formatter:on
170     /** Map of all the built-in functions. */
171     private Map<String, Function> functionData;
172 
173     {
174         this.functionData = new HashMap<>();
175         for (Function function : this.builtinFunctions)
176         {
177             this.functionData.put(function.getId(), function);
178         }
179     }
180 
181     /** User defined functions. */
182     private Map<String, Function> userDefinedFunctions = null;
183 
184     /** User supplied unit parser. */
185     private UnitParser userSuppliedUnitParser = null;
186 
187     /** Map from DoubleScalar sub classes to Quantities */
188     private Map<Class<?>, SIDimensions> siDimensionsMap = new HashMap<>();
189 
190     /**
191      * Construct a new evaluator with no RetrieveValue object and no added/overridden function and no added/overridden units.
192      */
193     public Eval()
194     {
195         // Nothing to do here
196     }
197 
198     /**
199      * Set or replace the RetrieveValue object of this evaluator.
200      * @param retrieveValue the new RetrieveValue object (may be null to only delete the currently active
201      *            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((
544                                 a, b
545                         ) -> (a <= b));
546                     }
547                     else
548                     {
549                         evalLhs(BIND_RELATIONAL);
550                         compareDoubleScalars((
551                                 a, b
552                         ) -> (a < b));
553                     }
554                     break;
555 
556                 case '>':
557                     if (this.stack.isEmpty())
558                     {
559                         throwException("Missing left operand");
560                     }
561                     this.position++;
562                     if (this.position < this.expression.length() && '=' == this.expression.charAt(this.position))
563                     {
564                         this.position++;
565                         evalLhs(BIND_RELATIONAL);
566                         compareDoubleScalars((
567                                 a, b
568                         ) -> (a >= b));
569                     }
570                     else
571                     {
572                         evalLhs(BIND_RELATIONAL);
573                         compareDoubleScalars((
574                                 a, b
575                         ) -> (a > b));
576                     }
577                     break;
578 
579                 case '=':
580                 {
581                     if (this.position >= this.expression.length() - 1 || '=' != this.expression.charAt(this.position + 1))
582                     {
583                         throwException("Single \'=\' is not a valid operator");
584                     }
585                     if (this.stack.isEmpty())
586                     {
587                         throwException("Missing left operand");
588                     }
589                     this.position += 2;
590                     evalLhs(BIND_EQUAL);
591                     Object right = pop();
592                     Object left = pop();
593                     push(left.equals(right)); // This also works for Boolean operands.
594                     break;
595                 }
596 
597                 case '!':
598                 {
599                     if (this.position >= this.expression.length() - 1 || '=' != this.expression.charAt(this.position + 1))
600                     {
601                         throwException("Single \'!\' is not a valid operator");
602                     }
603                     if (this.stack.isEmpty())
604                     {
605                         throwException("Missing left operand");
606                     }
607                     this.position += 2;
608                     evalLhs(BIND_EQUAL);
609                     Object right = pop();
610                     Object left = pop();
611                     push(!left.equals(right)); // This also works for Boolean operands and mixed-type operands.
612                     break;
613                 }
614 
615                 case '?': // Conditional expression
616                 {
617                     if (bindingStrength >= BIND_CONDITIONAL_EXPRESSION)
618                     {
619                         return;
620                     }
621                     this.position++;
622                     // This is special as we really do not want to evaluate or cause side effects on the non-taken part
623                     Object choice = pop();
624                     if (!(choice instanceof Boolean))
625                     {
626                         throwException("Condition does not evaluate to a logical value");
627                     }
628                     if (!((Boolean) choice))
629                     {
630                         // Skip the "then" part
631                         skip(true);
632                     }
633                     else
634                     {
635                         evalLhs(0); // should consume everything up to the ':'
636                     }
637                     if (this.position >= this.expression.length() || ':' != this.expression.charAt(this.position))
638                     {
639                         throwException("Missing \':\' of conditional expression");
640                     }
641                     this.position++; // skip the ':'
642                     if ((Boolean) choice)
643                     {
644                         // Skip the "else" part
645                         skip(false);
646                     }
647                     else
648                     {
649                         evalLhs(BIND_CONDITIONAL_EXPRESSION);
650                     }
651                     break;
652                 }
653 
654                 case ':':
655                     return;
656 
657                 case ',':
658                     return;
659 
660                 default:
661                     throwException("Operator expected (got \"" + token + "\")");
662 
663             }
664         }
665     }
666 
667     /**
668      * Interface for comparator for two double values.
669      */
670     interface CompareValues
671     {
672         /**
673          * Compare two double values.
674          * @param argument1 the left argument of the comparator
675          * @param argument2 the right argument of the comparator
676          * @return the result type of the function
677          */
678         Object execute(double argument1, double argument2);
679 
680     }
681 
682     /**
683      * Pop two operands from the stack and compare them using the provided comparator lambda expression
684      * @param comparator a function that compares two DoubleScalar values.
685      */
686     private void compareDoubleScalars(final CompareValues comparator)
687     {
688         Object right = pop();
689         Object left = pop();
690         if ((left instanceof DoubleScalar) && (right instanceof DoubleScalar)
691                 && getDimensions((DoubleScalar<?, ?>) left).equals(getDimensions((DoubleScalar<?, ?>) right)))
692         {
693             push(comparator.execute(((DoubleScalar<?, ?>) left).si, ((DoubleScalar<?, ?>) right).si));
694             return;
695         }
696         throwException("Cannot compare " + left + " to " + right);
697     }
698 
699     /**
700      * Throw an exception because the expression cannot be evaluated.
701      * @param description description of the problem
702      * @throws RuntimeException always thrown
703      */
704     private void throwException(final String description) throws RuntimeException
705     {
706         throw new RuntimeException(description + " at position " + this.position);
707     }
708 
709     /**
710      * Skip the "then" or the "else" part of a conditional expression.
711      * @param thenPart if true; skip the then part (i.e. skip until a ':'); if false; skip the else part (i.e. until
712      *            end of expression of a binary operator)
713      */
714     private void skip(final boolean thenPart)
715     {
716         while (this.position < this.expression.length())
717         {
718             switch (this.expression.charAt(this.position))
719             {
720                 case '(': // Eat up everything until the matching closing parenthesis
721                 {
722                     int level = 1;
723                     this.position++;
724                     while (this.position < this.expression.length())
725                     {
726                         char c = this.expression.charAt(this.position);
727                         if ('(' == c)
728                         {
729                             level++;
730                         }
731                         else if (')' == c && --level == 0)
732                         {
733                             break;
734                         }
735                         this.position++;
736                     }
737                     if (level > 0)
738                     {
739                         throwException("Missing closing parenthesis");
740                     }
741                     this.position++; // skip the closing parenthesis
742                     break;
743                 }
744 
745                 case '?': // Nested conditional expression
746                     this.position++;
747                     skip(true);
748                     if (this.position >= this.expression.length() || ':' != this.expression.charAt(this.position))
749                     {
750                         throwException("Conditional expression missing \':\'");
751                     }
752                     this.position++;
753                     skip(false);
754                     break;
755 
756                 case ':':
757                     return;
758 
759                 case '^':
760                 case '*':
761                 case '/':
762                 case '+':
763                 case '-':
764                 case '<':
765                 case '=':
766                 case '>':
767                 case '!':
768                     if (!thenPart)
769                     {
770                         return; // Every operator has lower precedence than the else part
771                     }
772                     this.position++;
773                     break;
774 
775                 default:
776                     this.position++;
777                     break;
778             }
779         }
780         if (thenPart)
781         {
782             throwException("Nonterminated conditional expression");
783         }
784         // Else end of expression is end of else part
785     }
786 
787     /**
788      * Perform the boolean AND operation on the two top-most elements on the stack and push the result back onto the stack.
789      */
790     private void and()
791     {
792         Object right = pop();
793         Object left = pop();
794         if ((left instanceof Boolean) && (right instanceof Boolean))
795         {
796             push(((Boolean) left) && ((Boolean) right));
797             return;
798         }
799         throwException("Cannot compute logical AND of " + left + " and " + right);
800     }
801 
802     /**
803      * Perform the boolean OR operation on the two top-most elements on the stack and push the result back onto the stack.
804      */
805     private void or()
806     {
807         Object right = pop();
808         Object left = pop();
809         if ((left instanceof Boolean) && (right instanceof Boolean))
810         {
811             push(((Boolean) left) || ((Boolean) right));
812             return;
813         }
814         throwException("Cannot compute logical AND of " + left + " and " + right);
815     }
816 
817     /**
818      * Perform the power operation on the two top-most elements on stack and push the result back onto the stack.
819      */
820     private void power()
821     {
822         Object exponent = pop();
823         Object base = pop();
824         push(performPower(base, exponent));
825     }
826 
827     /**
828      * Perform the power operation on the two arguments and return the result
829      * @param base the base operand of the power operation
830      * @param exponent the exponent of the power operation
831      * @return the result of the power operation
832      */
833     private Object performPower(final Object base, final Object exponent)
834     {
835         if ((base instanceof DoubleScalarRel) && (exponent instanceof DoubleScalarRel)
836                 && getDimensions((DoubleScalarRel<?, ?>) base).equals(getDimensions(DimensionlessUnit.SI))
837                 && getDimensions((DoubleScalarRel<?, ?>) exponent).equals(getDimensions(DimensionlessUnit.SI)))
838         {
839             DoubleScalar<?, ?> result = DoubleScalarRel.instantiate(
840                     Math.pow(((DoubleScalarRel<?, ?>) base).si, ((DoubleScalarRel<?, ?>) exponent).si), DimensionlessUnit.SI);
841             // System.out.println(base + " ^ " + exponent + " = " + result);
842             return result;
843         }
844         throwException("Cannot raise " + base + " to power " + exponent);
845         return null; // Not reached
846     }
847 
848     /**
849      * Perform the atan2 function on the two arguments and return the result
850      * @param y should be some kind of DoubleScalarRel
851      * @param x should be some kind of DoubleScalarRel with the same SiDimensions as y
852      * @return in fact a DoubleScalarRel with a quantity matching Dimensionless
853      */
854     private Object performAtan2(final Object y, final Object x)
855     {
856         if ((y instanceof DoubleScalarRel) && (x instanceof DoubleScalarRel)
857                 && getDimensions((DoubleScalarRel<?, ?>) y).equals(getDimensions((DoubleScalarRel<?, ?>) x)))
858         {
859             DoubleScalar<?, ?> result = DoubleScalarRel.instantiate(
860                     Math.atan2(((DoubleScalarRel<?, ?>) y).si, ((DoubleScalarRel<?, ?>) x).si), DimensionlessUnit.SI);
861             // System.out.println(base + " ^ " + exponent + " = " + result);
862             return result;
863         }
864         throwException("Cannot compute atan2 of " + y + ", " + x + ")");
865         return null; // Not reached
866 
867     }
868 
869     /**
870      * Multiply the two top-most elements on the stack and push the result back onto the stack.
871      */
872     private void multiply()
873     {
874         Object right = pop();
875         Object left = pop();
876         if ((right instanceof DoubleScalarRel) && (left instanceof DoubleScalarRel))
877         {
878             push(((DoubleScalarRel<?, ?>) left).times((DoubleScalarRel<?, ?>) right));
879             return;
880         }
881         throwException("Cannot multiply with " + right + " as right hand operand");
882     }
883 
884     /**
885      * Divide the two top-most elements on the stack and push the result back onto the stack.
886      */
887     private void divide()
888     {
889         Object right = pop();
890         Object left = pop();
891         if ((left instanceof DoubleScalarRel) && (right instanceof DoubleScalarRel))
892         {
893             if (0.0 == ((DoubleScalarRel<?, ?>) right).si)
894             {
895                 throwException("Division by 0");
896             }
897             push(((DoubleScalarRel<?, ?>) left).divide((DoubleScalarRel<?, ?>) right));
898             return;
899         }
900         throwException("Cannot divide " + left + " by " + right);
901     }
902 
903     /**
904      * Add the two top-most elements on the stack and push the result back onto the stack.
905      */
906     private void add()
907     {
908         Object right = pop();
909         Object left = pop();
910         if (!(left instanceof DoubleScalar))
911         {
912             throwException("Left operand of addition must be a scalar (got \"" + left + "\")");
913         }
914         if (!(right instanceof DoubleScalar))
915         {
916             throwException("Right operand of addition must be a scalar (got \"" + right + "\")");
917         }
918         // Both operands are DoubleScalar
919         if (!((DoubleScalar<?, ?>) left).getDisplayUnit().getQuantity().getSiDimensions()
920                 .equals(getDimensions((DoubleScalar<?, ?>) right)))
921         {
922             // System.out.println("left: " + getDimensions((DoubleScalar<?, ?>) left));
923             // System.out.println("right: " + getDimensions((DoubleScalar<?, ?>) right));
924             throwException("Cannot add " + left + " to " + right + " because the types are incompatible");
925         }
926         // Operands are of compatible unit
927         if ((left instanceof DoubleScalarRel) && (right instanceof DoubleScalarRel))
928         {
929             // Rel + Rel -> Rel
930             DoubleScalar<?, ?> sum =
931                     DoubleScalarRel.instantiateAnonymous(((DoubleScalarRel<?, ?>) left).si + ((DoubleScalarRel<?, ?>) right).si,
932                             ((DoubleScalarRel<?, ?>) left).getDisplayUnit().getStandardUnit());
933             // System.out.println(left + " + " + right + " = " + sum);
934             // Set display unit???
935             push(sum);
936             return;
937         }
938         if (right instanceof DoubleScalarAbs)
939         {
940             throwException("Cannot add an absolute value to some other value");
941         }
942         // Abs + Rel -> Abs
943         DoubleScalar<?,
944                 ?> sum = DoubleScalarAbs.instantiateAnonymous(
945                         ((DoubleScalarAbs<?, ?, ?, ?>) left).si + ((DoubleScalarRel<?, ?>) right).si,
946                         ((DoubleScalarAbs<?, ?, ?, ?>) left).getDisplayUnit().getStandardUnit());
947         // System.out.println(left + " + " + right + " = " + sum);
948         // sum.setDisplayUnit(ds.getDisplayUnit());
949         push(sum);
950     }
951 
952     /**
953      * Subtract the two top-most elements on the stack and push the result back onto the stack.
954      */
955     private void subtract()
956     {
957         Object right = pop();
958         Object left = pop();
959         if (!(left instanceof DoubleScalar))
960         {
961             throwException("Left operand of subtraction must be a scalar (got \"" + left + "\")");
962         }
963         if (!(right instanceof DoubleScalar))
964         {
965             throwException("Right operand of subtraction must be a scalar (got \"" + right + "\")");
966         }
967         // Now we know that we're dealing with DoubleScalar objects
968         if (!getDimensions((DoubleScalar<?, ?>) left).equals(getDimensions((DoubleScalar<?, ?>) right)))
969         {
970             throwException("Cannot subtract " + right + " from " + left + " because the types are incompatible");
971         }
972         if ((left instanceof DoubleScalarAbs) && (right instanceof DoubleScalarAbs))
973         {
974             // Abs - Abs -> Rel
975             DoubleScalar<?, ?> difference =
976                     DoubleScalarRel.instantiateAnonymous(((DoubleScalar<?, ?>) left).si - ((DoubleScalar<?, ?>) right).si,
977                             ((DoubleScalar<?, ?>) left).getDisplayUnit().getStandardUnit());
978             // System.out.println(left + " - " + right + " = " + difference);
979             push(difference);
980             return;
981         }
982         if ((left instanceof DoubleScalarAbs) && (right instanceof DoubleScalarRel))
983         {
984             // Abs - Rel -> Abs
985             DoubleScalar<?, ?> difference =
986                     DoubleScalarAbs.instantiateAnonymous(((DoubleScalar<?, ?>) left).si - ((DoubleScalar<?, ?>) right).si,
987                             ((DoubleScalar<?, ?>) left).getDisplayUnit().getStandardUnit());
988             // System.out.println(left + " - " + right + " = " + difference);
989             push(difference);
990             return;
991         }
992         if ((left instanceof DoubleScalarRel) && (right instanceof DoubleScalarAbs))
993         {
994             // Rel - Abs -> error
995             throwException("Cannot subtract " + right + " from " + left + " because the right operand is absolute");
996         }
997         // Rel - Rel -> Rel
998         DoubleScalar<?, ?> difference =
999                 DoubleScalarRel.instantiateAnonymous(((DoubleScalar<?, ?>) left).si - ((DoubleScalar<?, ?>) right).si,
1000                         ((DoubleScalar<?, ?>) left).getDisplayUnit().getStandardUnit());
1001         // System.out.println(left + " - " + right + " = " + difference);
1002         push(difference);
1003     }
1004 
1005     /**
1006      * Parse a number and convert it to a SIScalar. If it is followed by an SI unit string inside square brackets, parse it into
1007      * the correct type.
1008      * @return the value of the parsed number or value
1009      */
1010     private DoubleScalar<?, ?> handleNumber()
1011     {
1012         // Parse a number value
1013         int startPosition = this.position;
1014         boolean seenExp = false;
1015         boolean seenExpSign = false;
1016         boolean seenExpDigit = false;
1017         boolean seenRadix = false;
1018         while (this.position < this.expression.length())
1019         {
1020             char c = this.expression.charAt(this.position);
1021             // Any leading plus and minus signs at the start of this value are consumed by evalLhs
1022             // A subsequent plus and minus sign indicates an addition or subtraction
1023             if (seenExp && (!seenExpDigit) && ('-' == c || '+' == c))
1024             {
1025                 if (seenExpSign)
1026                 {
1027                     throwException("Too many signs in exponent");
1028                 }
1029                 seenExpSign = true;
1030             }
1031             else if (seenExp && seenExpSign && (!Character.isDigit(c)))
1032             {
1033                 break;
1034             }
1035             else if ('e' == c || 'E' == c)
1036             {
1037                 if (seenExp)
1038                 {
1039                     throwException("Too many 'e' or 'E' indicators");
1040                 }
1041                 seenExp = true;
1042             }
1043             else if ('.' == c)
1044             {
1045                 if (seenRadix)
1046                 {
1047                     throwException("Too many '.'");
1048                 }
1049                 if (seenExp)
1050                 {
1051                     throwException("A \'.\' is not allowed after \'e\' or \'E\'");
1052                 }
1053                 seenRadix = true;
1054             }
1055             else if (Character.isDigit(c))
1056             {
1057                 if (seenExp)
1058                 {
1059                     seenExpDigit = true;
1060                 }
1061             }
1062             else
1063             {
1064                 break;
1065             }
1066             this.position++;
1067         }
1068         eatSpace();
1069         if (seenExp && !seenExpDigit)
1070         {
1071             throwException("Exponent value missing");
1072         }
1073         String number = this.expression.substring(startPosition, this.position);
1074         if (this.position < this.expression.length() && '[' == this.expression.charAt(this.position))
1075         {
1076             this.position++;
1077             startPosition = this.position;
1078             while (this.position < this.expression.length())
1079             {
1080                 char c = this.expression.charAt(this.position);
1081                 if (']' == c)
1082                 {
1083                     String unit = this.expression.substring(startPosition, this.position++);
1084                     DoubleScalar<?, ?> result = null;
1085                     if (null != this.userSuppliedUnitParser)
1086                     {
1087                         result = this.userSuppliedUnitParser.parseUnit(Double.parseDouble(number), unit);
1088                     }
1089                     if (null == result)
1090                     {
1091                         result = SIScalar.valueOf(number + " " + unit);
1092                     }
1093                     return result;
1094                 }
1095                 if ((!Character.isLetterOrDigit(c)) && '-' != c && '^' != c && '/' != c && '.' != c)
1096                 {
1097                     throwException("Bad symbol in SI unit string");
1098                 }
1099                 this.position++;
1100             }
1101             if (this.position >= this.expression.length())
1102             {
1103                 throwException("Missing closing bracket (\']\')");
1104             }
1105         }
1106         return SIScalar.valueOf(number); // No unit specified
1107     }
1108 
1109     /**
1110      * Process a function call, a variable interpolation, or a mathematical, or physical constant. Precondition: this.position
1111      * points to the first letter of the name of the function, variable, or constant.
1112      * @return the value of the function, variable, or mathematical or physical constant
1113      */
1114     private Object handleFunctionOrVariableOrNamedConstant()
1115     {
1116         int startPosition = this.position;
1117         while (this.position < this.expression.length())
1118         {
1119             char c = this.expression.charAt(this.position);
1120             if (Character.isLetterOrDigit(c) || '.' == c || '_' == c || '@' == c || '#' == c)
1121             {
1122                 this.position++;
1123             }
1124             else
1125             {
1126                 break;
1127             }
1128         }
1129         String name = this.expression.substring(startPosition, this.position);
1130         eatSpace();
1131         if (this.position >= this.expression.length() || this.expression.charAt(this.position) != '(')
1132         {
1133             // No opening parenthesis; name must be the name of a variable; look it up
1134             Object result = null == this.retrieveValue ? null : this.retrieveValue.lookup(name);
1135             if (null == result)
1136             {
1137                 throwException("Cannot resolve variable " + name);
1138             }
1139             // Do not check the type of the result to allow expression that return any kind of object
1140             return result;
1141         }
1142         // At an opening parenthesis. This signals the start of a function call; collect the parameters of the function on the
1143         // stack and count them
1144         this.position++;
1145         eatSpace();
1146         int argCount = 0;
1147         while (this.position < this.expression.length() && ')' != this.expression.charAt(this.position))
1148         {
1149             evalLhs(0);
1150             argCount++;
1151             eatSpace();
1152             if (this.position < this.expression.length() && ',' == this.expression.charAt(this.position))
1153             {
1154                 this.position++;
1155             }
1156         }
1157         if (this.position >= this.expression.length() || ')' != this.expression.charAt(this.position))
1158         {
1159             throwException("Missing closing parenthesis");
1160         }
1161         this.position++; // step over the closing parenthesis
1162         Function f = null;
1163         if (null != this.userDefinedFunctions)
1164         {
1165             f = this.userDefinedFunctions.get(name);
1166         }
1167         if (null == f)
1168         {
1169             f = this.functionData.get(name);
1170         }
1171         if (null == f)
1172         {
1173             throwException("Unknown function " + name);
1174         }
1175         Object[] args = new Object[argCount];
1176         for (int i = 0; i < argCount; i++)
1177         {
1178             args[argCount - i - 1] = pop();
1179         }
1180         if (f.getMetaData() != MetaData.NO_META_DATA)
1181         {
1182             // MetaData.verifyComposition does not handle this case correctly.
1183             int argsNeeded = f.getMetaData().getObjectDescriptors().length;
1184             if (argsNeeded != argCount)
1185             {
1186                 throwException(name + " needs " + argsNeeded + " parameter" + (argsNeeded == 1 ? "" : "s") + " (got " + argCount
1187                         + ")");
1188             }
1189             for (int i = 0; i < argCount; i++)
1190             {
1191                 if ((args[i] instanceof Boolean) && (!Boolean.class.isAssignableFrom(f.getMetaData().getObjectClass(i))))
1192                 {
1193                     throwException(name + " does not take " + args[i] + " as parameter " + i);
1194                 }
1195                 else if ((args[i] instanceof DoubleScalar)
1196                         && (DoubleScalar.class.isAssignableFrom(f.getMetaData().getObjectClass(i))))
1197                 {
1198                     DoubleScalar<?, ?> ds = (DoubleScalar<?, ?>) args[i];
1199                     Class<?> clazz = f.getMetaData().getObjectClass(i);
1200                     if (!clazz.equals(DoubleScalarRel.class))
1201                     {
1202                         SIDimensions siDimensions = this.siDimensionsMap.get(clazz);
1203                         if (null == siDimensions)
1204                         {
1205                             // Not in the cache
1206                             try
1207                             {
1208                                 Field field = clazz.getDeclaredField("ZERO"); // Every DoubleScalar type has this
1209                                 DoubleScalar<?, ?> zero = (DoubleScalar<?, ?>) field.get(clazz);
1210                                 siDimensions = zero.getDisplayUnit().getQuantity().getSiDimensions();
1211                                 this.siDimensionsMap.put(clazz, siDimensions); // Add this one to our map
1212                             }
1213                             catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException nsfe)
1214                             {
1215                                 throwException("ERROR: Cannot determine quantity for " + clazz.getCanonicalName());
1216                             }
1217                         }
1218                         if (!siDimensions.equals(getDimensions(ds)))
1219                         {
1220                             throwException("parameter " + i + " of " + name + " has incompatible quantity");
1221                         }
1222                     }
1223                 }
1224                 else
1225                 {
1226                     throwException("Argument " + i + " of function " + name + " is of an unhandled type (" + args[i] + ")");
1227                 }
1228             }
1229         }
1230         // All parameters are apparently compatible; invoke the function
1231         return (f.function(args));
1232     }
1233 
1234     /**
1235      * Push one object onto the evaluation stack.
1236      * @param object the object to push onto the evaluation stack
1237      */
1238     private void push(final Object object)
1239     {
1240         this.stack.add(object);
1241     }
1242 
1243     /**
1244      * Pop one object from the evaluation stack. Throw exception when stack underflows
1245      * @return the object popped from the evaluation stack
1246      * @throws RuntimeException when the stack is currently empty
1247      */
1248     private Object pop() throws RuntimeException
1249     {
1250         if (this.stack.isEmpty())
1251         {
1252             throwException("Stack empty");
1253         }
1254         return (this.stack.remove(this.stack.size() - 1));
1255     }
1256 
1257     /**
1258      * Convert an object to a Dimensionless if possible, or complain.
1259      * @param functionData meta data of the function that wants a Dimensionless
1260      * @param object object that supposedly can be converted to a Dimensionless
1261      * @return the result
1262      */
1263     private Dimensionless checkDimensionless(final Function functionData, final Object object)
1264     {
1265         if (!(object instanceof DoubleScalar))
1266         {
1267             throwException("Function " + functionData.getId() + " cannot be applied to " + object);
1268         }
1269         DoubleScalar<?, ?> ds = (DoubleScalar<?, ?>) object;
1270         if (!getDimensions(ds).equals(getDimensions(DimensionlessUnit.SI)))
1271         {
1272             throwException("Function " + functionData.getId() + " cannot be applied to " + ds);
1273         }
1274         return new Dimensionless(ds.si, DimensionlessUnit.SI);
1275     }
1276 
1277     /**
1278      * Retrieve the SIDimensions of a DoubleScalar.
1279      * @param doubleScalar the DoubleScalar
1280      * @return the SIDimensions object that describes the quantity of the DoubleScalar
1281      */
1282     private static SIDimensions getDimensions(final DoubleScalar<?, ?> doubleScalar)
1283     {
1284         return doubleScalar.getDisplayUnit().getQuantity().getSiDimensions();
1285     }
1286 
1287     /**
1288      * Retrieve the SIDimensions of a unit.
1289      * @param unit the unit
1290      * @return the SIDimensions unit
1291      */
1292     private static SIDimensions getDimensions(final Unit<?> unit)
1293     {
1294         return unit.getQuantity().getSiDimensions();
1295     }
1296 
1297 }