Expressions Language (Formula Evaluator Library)
Formula Evaluator is a utility to evaluate simple formulas with binding variables.
Usage
Map binding = [a:1, b:2, c:3, map:[property1: 1], list: [1,2,3] ]
String formula = " a + b * 2"
def result = libs.FormulaEvaluator.Builder.setFormula(formula).setBinding(binding).eval() // = 5
Supported Operators, Functions and Symbols
Operator | Purpose | Samples |
---|---|---|
+ | Arithmetic – addition | a + b |
- | Arithmetic – subtraction | a - b |
* | Arithmetic – multiplication | a * b |
/ | Arithmetic – division | a / b |
% | Arithmetic – modulo | a % b |
** | Arithmetic – power | a ** b |
== | Relational – equal | a == b true == true |
!= | Relational – different | a != b |
> | Relational – greater than | a > b |
< | Relational – less than | a < b |
>= | Relational – greater than or equal | a >= b |
<= | Relational – less than or equal | a <=b |
! | Logical "not" | !a !true |
&& | Logical "and" | a && b true && false |
|| | Logical "or" | a || b true || false |
AND | Logical "and" | a AND b |
OR | Logical "or" | a OR b |
IN | Logical IN | a IN [ 1, 2, 3]. Check if the left value is in the right value |
IF | Function | IF (condition, return when true, return when false) |
IS_NULL | Function | IS_NULL (value to check null) return true/false |
SIZE | Function | SIZE (string or array to get size) return size in integer |
AS_STRING | Function | AS_STRING (string) Cast parameter to a string |
AS_NUMBER | Function | AS_NUMBER(string or number). Convert parameter to the number |
VAR | Function | VAR("String as variable"). Consider the parameter as a variable and lookup for its value from the binding list |
( ) | Brackets/parentheses – to modify the order of evaluation | a * ( b + c ) |
. (dot) | Property access | map.property1 + 2 |
?. | Safe property access | map?.property1 + 2. Check the target availability before getting the property, and return null when the target object is unavailable. |
[ ] | Property access, list index access | list[0] or map['property1'] |
[item1, item2] | Literal list | Define a list, eg. a in [1,2,3 ] |
?: | Elvis operator | a ?: 1 |
? : | Ternary operator | a > 0 ? a : b |
Cache
For better performance, the evaluator uses api.global
to cache the parsed formula and the evaluated result for expression, so it is required to set api.retainGlobal = true
.
Binding
A binding map provides the value of the variables that are used in the formula.
Custom Function
Besides the predefined functions (IF, AS_NUMBER, AS_STRING, IS_NULL, SIZE, VAR), you can define your custom function using libs.FormulaEvaluator.Builder.addFunctionOperator(String functionName, Closure executor, Integer minParamRequired = null, Boolean evaluatedParam = true, Boolean constantAnalyze = true)
.
Let's build a custom function named LOG_INFO
for logging.
Example 1 - custom function without param
Closure logInfoExecutor = { -> api.logInfo("Logging from FormulaEvaluator expression") }
libs.FormulaEvaluator.Builder.addFunctionOperator("LOG_INFO ", logInfoExecutor)
String expression = "LOG_INFO()"
libs.FormulaEvaluator.Builder.setFormula(expression)
.eval() // Run and check the server log for "Logging from FormulaEvaluator expression"
Example 2 - custom function with param
Closure logInfoExecutor = { String message, def object-> api.logInfo(message, object) }
libs.FormulaEvaluator.Builder.addFunctionOperator("LOG_INFO", logInfoExecutor, 2)
String expression = "LOG_INFO('Expression message', 2)"
libs.FormulaEvaluator.Builder.setFormula(expression)
.eval() // Run and check server log for "Expression message: 2"
Function Configurations
functionName
– Name of the function that can be used in the expression.executor
– Closure that defines how the function should be evaluated. The executor should accept the parameters as provided in the expression.delegation
– Context Map is set as executor delegation and can be accessed inside the executor bygetDelegate()
, e.g.Map context = getDelegate() as Map.
The context map contains:node
– Current node is being evaluated. This is useful when you want to access node properties, e.g. expression, type, token, etc.operator
– Operator configuration that is being evaluated.bindings
– Binding list that is provided to the evaluator.cache
– Evaluation cache that is provided to the evaluator.
minParamRequired
– Defines the minimum parameters that are required by the executor. The evaluator will raise an exception if the actual count of parameters provided in the expression is less than this parameter.constantAnalyze
– Defines if the function node should be analyzed as a constant node by the parser. A constant node evaluated result will be cached in the global cache and will be shared for all expressions to improve the performance. Basically, string, number, true, false, and null values will be analyzed as constant nodes. Any operators with all constant nodes as children will also be considered as a constant. The none constant nodes evaluated results are still cached within the evaluation session.
Evaluation Configurations
The evaluation configurations provide some conveniences while writing expressions. However, they drive the parser & evaluator's normal behavior, then might end up with some performance issues. You should use it with caution.
String Expression Binding Lookup (stringExpressionBindingLookup)
A string can be used as a variable, and therefore the library will look up in the provided bindings for a value.
This behavior leads to a counter-intuitive expression ( see https://pricefx.atlassian.net/browse/PFPCS-4771). So from version 1.1.5, this behavior is changed and the string will not be looked up for variable value by default. A default function named VAR
is provided if you need to look up a value from the provided bindings by a key.
There is a configuration that allows you to enable the string expression binding lookup behavior and it can be set by using libs.FormulaEvaluator.Builder.setStringExpressionBindingLookup(true|false)
.
String expression binding lookup enabled
String expression binding lookup disabled by default
By enabling the string expression binding lookup configuration, you cannot use the VAR
default function.
VAR() vs String expression binding lookup enabled
Dot Access Expression Binding Lookup Fallback (dotAccessExpressionBindingLookupFallback)
This configuration allows you to use the property access expression, e.g. "someObject.property"
to look up in the provided binding for a value as a fallback when the left node (someObject
in our case) has a null evaluated result.
The configuration can be set by using libs.FormulaEvaluator.Builder.setDotAccessExpressionBindingLookupFallback(true|false)
.
Dot Access Expression Binding Lookup Fallback
Error Handling
When the expression is not valid, the system will return the feature name and error codes that help with troubleshooting:
The error message structure: FEATURE_NAME-ERROR_CODE::ERROR_MESSAGE
Feature | Name |
---|---|
Tokenizer | FEA_TOKENIZER_ERR |
Parser | FEA_PARSER_ERR |
Operator | FEA_OPERATOR_ERR |
Evaluator | FEA_EVALUATOR_ERR |
Error code | Context | Invalid expression example | Error message example |
---|---|---|---|
INVALID_EXPRESSION |
| a + b) | FEA_PARSER_ERR-INVALID_EXPRESSION::Invalid expression at position 5 |
UNEXPECTED_EXPRESSION |
| a > b ? a : | FEA_PARSER_ERR-UNEXPECTED_EXPRESSION::An expression is expected at position 10, but found ':' |
INVALID_BINARY_EXPRESSION |
| TBD | |
INVALID_FUNCTION_EXPRESSION |
| IF(a > b, a, , ) | FEA_PARSER_ERR-INVALID_FUNCTION_EXPRESSION::Invalid function IF expression |
FUNCTION_NAME_ALREADY_DEFINED |
| TBD | |
FUNCTION_NAME_REQUIRED |
| TBD | |
FUNCTION_EXECUTOR_REQUIRED |
| TBD | |
INVALID_OPERATOR_PARAM |
| AS_NUMBER() | FEA_EVALUATOR_ERR-INVALID_OPERATOR_PARAM::The number of parameters must be at least 1, but there are only 0 parameter(s) |
INVALID_PROPERTY_ACCESS_EXPRESSION |
| map.IF(a > b, 1, 2) | FEA_PARSER_ERR-INVALID_PROPERTY_ACCESS_EXPRESSION::Invalid access property |
INVALID_CHARACTER |
| TBD | |
INVALID_NUMBER |
| 1 + 4.400. | FEA_TOKENIZER_ERR-INVALID_NUMBER::Invalid number 4.400. at position 9 |
INVALID_SYMBOL |
| 1+=1 | FEA_TOKENIZER_ERR-INVALID_SYMBOL::Unknown symbol = at position 2 |
INVALID_STRING |
| 'TEST' + 'CASE | FEA_TOKENIZER_ERR-INVALID_STRING::Invalid string CASE at position 14 |
INVALID_VARIABLE |
| a > 1 ? a : 1 | FEA_OPERATOR_ERR-INVALID_VARIABLE::Variable 'a' not defined |