Implementation

Estimated reading: 6 minutes 6 views

Introduction

The rules engine is implemented leveraging different algorithms to parse and evaluate the requested expressions.

The Expressions

The Expressions are defined in INFIX notation, what means that it’s done in the usual way for any human readable expression:

  • 5 / 4
  • 3 + (5 * 2)
  • ( “hi” = “hello ) OR ( “bye” = “cu” )

The reason to use this notation is mainly for usability purposes, as for end users PREFIX (+1,2) or POSTFIX (1,2+) annotation would be actually hard to manage.

The Operators

In the following table the different operators are listed along with its priority and the data types they can be used with.

Note that if you use an operator with an operand of an incompatible type the expression will throw an error.

Operator Description Priority String Number Boolean Date
/ Divide 1 N Y N TBD
* Multiply 1 N Y N TBD
+ Add 2 Y Y N TBD
Substract 2 N Y N TBD
= Equals 3 Y Y Y TBD
!= Not Equals 3 Y Y Y TBD
> Greater than 3 N Y N TBD
>= Greater or Equals 3 N Y N TBD
< Lower than 3 N Y N TBD
<= Lower or Equals 3 N Y N TBD
AND Logic And 4 N N Y TBD
OR Logic Or 5 N N Y TBD
( Open Group 0 TBD
) Close Group 0 TBD

The Operands

The operands can be of different types. The return value

Operand Example Data Type Comments
Number 1 Number
String “hello” String
Boolean true Boolean The rules engine will automatically convert “true” and “false” strings to boolean values if the other operand is boolean too.
Function max(1,2) various The Data Type depends on the function returned value.
Variable $varName various The Data Type depends on the variable type.
Reference &Account.Name various The Data Type depends on the field type.

The Variables

Sometimes you will need to pass some variables to the rule evaluation that would depend on the execution scenario or other functional or technical needs. For this, you pass a Map<String, Object> with the variables that you want to reference in your formulas.

In order to reference a formula in your expression you just need to prepend the variable name with a $ (dollar sign). The name must be exactly the same (case sensitive) as in the source variables Map.

    Map<String,Object> myVars = new Map<String, Object>{'myVar'=>100};
    FormulaEngine.evaluateFormula('5 + $myVariable', myVars);

The Context

The engine also offers the possibility of referencing values in a passed context (Map<String, Object>). This context is usually a record hierarchy, for instance an Account with its Contacts, Contracts and Order.

This functionality is extremely powerful and allows to define rules that would dynamically adapt to the execution context.

For this you would need to pass the engine the context.

The usual way to generate the context is to use the [Object Tree Functionality]({{< relref “../../core/objecttree” >}})

    Map<String,Object> myVars = new Map<String, Object>();
    Map<String,Object> context = ObjectTree.loadRecord(theObjectTreeId, theRecordId);
    FormulaEngine.evaluateFormula('"The Primary Contact Name is " + &Contacts.[Primary=true].Name', myVars, context);

For more information about how to reference Context variable check the [Context Helper Documentation]({{< relref “../../core/contexthelper” >}})

The Functions

The engine also support the use of functions. The functions can receive any number of input paramater and return any type of result.

Some examples:

  • max(1,2)
  • min (4,100)
  • toUpperCase(‘hello’)

The functions are stored in Function Libraries which are Apex Classes that must implement the CustomFunctionLibrary interface. Each implementation can contain multiple functions.

Wattyo comes out of the box with different libraries:

  • Common Functions
  • Math Functions
  • String Functions
  • Pricing Functions
  • CPQ Functions
  • Measures Functions

Moreover, it’s possible to extend the rule engine with any function that you need by just deploying a class that implements the CustomFunctionLibrary. The engine will automatically detect and register all the functions defined in the class.

To get more information visit the [Custom Functions Library Documentation]({{< relref “customfunctionslibraries” >}})

The Algorithm

As the INFIX notation is not the most efficient in order to compute it, the Rules Engine will automatically convert it to POSTFIX annotation at the same time it builds an expression tree to make the evaluation more efficient. More specifically, the algorithm follows the Shunting Yard algorithm along with postfix-to-expression tree conversion.

In order to evaluate an expression, the Rules Engine algorithm execute the following steps:

  1. Tokenize the Expression String
  2. Generate the Expression Tree following a POSTFIX approach
  3. Evaluate the Tree while replacing variables, executing functions or retrieving reference values.

Step 1. Tokenization

In this first stage of the algorithm the Rules Engine parse character by character the expression string, identifing when a token starts and ends and adding it to a List of Tokens in the original order.

This part is actually importat, and somehow complex, as the expressions can use same characters for different purposes. For instance:

  • The parenthesis () can be used to group, to speficy a function’s parameters or to be part of a string.
  • Also, the letters can be used to specify functions or variable names, specify references or to define static strings.

The Tokenization is not only in charge of identifying the different token of the expression but also is responsible of infering the data type each Token is. The Token type is critical for the later evaluation, as that way the Engine would know if it’s an operator,variable, constant, functionor reference.

Step 2. Expression Tree Generation

During this step, the Engine will generate a POSTFIX Expression Tree based on the operators precedence / priority and the groups (parenthesis).

The resulting tree is made from Nodes. The Nodes has different members:

  • Token: The Node’s token.
  • left: The child Left Node.
  • right: The child Right Node.
  • value: The resulting value for this node (calculated in the last step).

For instance the expression: 3 + ((5-9)*2)

erDiagram
    "+" ||--|| "3" : ""
    "+" ||--|| "" : ""
    "" ||--|| "-" : leftNode
    "-" ||--|| "5" : ""
    "-" ||--|| "9" : ""
    "*" ||--|| "2" : rightNode

Step 3. Expression Tree Evaluation

It’s during the Expression Tree Evaluation when:

  • The different operands are replaced by its actual value.
  • The left and right operands data type for an operator are validated to check if the operator can be applied.
  • The evaluation to get the expression final value is done.

Caching Expression

One of the most resource consuming operations is the String management in almost in any Programming Language, that’s why, although efficient, the Tokenization and Tree Generation are the most heavy steps. As these two steps are valid for any execution, it’s a recommended practice to cache the Expression Trees whenever is possible. This way the rules evaluation would be actually speed up.

For this purpose you can call the RulesEngine.getExpressionTree(String expression) for a spefific expression, for later use.

    RulesEngine.getExpressionTree(String expression);

In order to evaluated a specific expression in an specific context you can do it by invoking the evaluatemethod in the RulesEngine.

RulesEngine.evaluateFormula(Node rootNode);
RulesEngine.evaluateFormula(Node rootNode, Map<String, Object> vars);
RulesEngine.evaluateFormula(Node rootNode, Map<String, Object> vars, Map<String, Object> context);

Leave a Comment

       
Euphoria, forever till the end of times

Euphoria

Share this Doc

Implementation

Or copy link

CONTENTS