Add comparing to a formula string

This commit is contained in:
Wim Velzeboer
2021-08-10 17:03:53 +01:00
parent 3540e56fe8
commit d279ab6fc2
5 changed files with 304 additions and 1 deletions
+41
View File
@@ -24,6 +24,8 @@ Add an equal to criteria comparing a fields value to a given value
Add an equal to criteria comparing a related fields value to a given value
- <<evaluate, evaluate(Object record)>> +
Evaluates the stored criteria based on the given SObject
- <<formula, formulaCriteria(String formula)>> +
Adds a criteria based on a formula
- <<greaterOrEqualTo, greaterOrEqualTo(Schema.SObjectField field, Object value)>> +
Compares the fields values to be greater or equal to the given value
- <<greaterOrEqualTo2, greaterOrEqualTo(String relatedField, Object value)>> +
@@ -247,6 +249,45 @@ System.assert(
);
```
[[formula]]
=== formulaCriteria(String formula)(Object record)
Adds a criteria based on a formula that can dynamically compare criteria with complex AND or OR structure.
==== _Signature_
```java
public virtual fflib_Criteria formulaCriteria(String formula)
```
==== _Parameters_
_**formula**_ +
The condition formula to evaluate
==== _Returns_
The Boolean result of the evaluated criteria
==== _Example_
```java
Contact record = new Contact(
LastName = 'Smith',
FirstName = 'John',
Department = 'MIB'
);
new fflib_Criteria()
.FormulaCriteria('(1 AND 2) OR (1 AND 3)')
.equalTo(Contact.LastName, 'Smith')
.equalTo(Contact.FirstName, 'John')
.equalTo(Contact.Department, 'MIB')
.evaluate(record); // true
new fflib_Criteria()
.FormulaCriteria('(1 AND 2) OR 3')
.equalTo(Contact.LastName, 'Smith')
.equalTo(Contact.FirstName, 'John')
.equalTo(Contact.Department, 'MIB')
.toSOQL(); // "(LastName='Smith' OR FirstName='John') AND Department='MIB'"
```
___
[[greaterOrEqualTo]]
=== greaterOrEqualTo(Schema.SObjectField field, Object value)
@@ -31,14 +31,20 @@
public virtual with sharing class fflib_Criteria
implements Evaluator
{
private static final String OPEN_BRACKET = '(';
private static final String CLOSE_BRACKET = ')';
private static final String FORMULA = 'FORMULA';
// Add brace when generated to SQL
private Boolean embraced = false;
// List to hold all evaluators
private List<Evaluator> evaluators;
// The type of comparator NONE | AND | OR
// The type of comparator NONE | AND | OR | FORMULA
private String type;
private IFormulaEvaluator formulaEvaluator;
/**
@@ -145,6 +151,154 @@ public virtual with sharing class fflib_Criteria
return this;
}
/**
* @param formula Formula string
*
* @return An instance of itself to enable method chaining
*
* @example
* new fflib_Criteria()
* .formulaCriteria('(1 OR 2) AND 3')
* .equalTo(Account.AccountNumber, '0001')
* .equalTo(Account.AccountNumber, '0002')
* .equalTo(Account.ShippingCountry, 'USA'))
*
* Evaluates:
* (AccountNumber = '0001' OR AccountNumber = '0001') AND ShippingCountry = 'USA'
*/
public virtual fflib_Criteria formulaCriteria(String formula)
{
this.type = 'FORMULA';
this.formulaEvaluator = new FormulaEvaluator(formula.toUpperCase());
return this;
}
private interface IFormulaEvaluator
{
Boolean evaluate(Object record, Map<Integer, Boolean> evaluatedResultByNumber);
String toSOQL(Map<Integer, String> queryByNumber);
}
public class FormulaEvaluator implements IFormulaEvaluator
{
private String formula;
private String type
{
get;
set
{
if (type != null && type != value)
throw new CriteriaException('Can not mix AND and OR, use brackets');
type = value;
}
}
List<IFormulaEvaluator> formulaParts = new List<IFormulaEvaluator>();
public FormulaEvaluator(String formula)
{
this.formula = formula;
String expression = String.valueOf(formula);
while(expression.length() > 0)
{
if (expression.startsWith(' '))
{
expression = expression.removeStart(' ');
}
else if (expression.startsWith(OPEN_BRACKET))
{
String subFormula = fflib_StringUtils.getBetweenBrackets(OPEN_BRACKET, CLOSE_BRACKET, expression, 0);
if (subFormula == null)
throw new CriteriaException('Missing closing bracket');
formulaParts.add(new FormulaEvaluator(subFormula));
expression = expression.substring(subFormula.length() + 2);
}
else if (expression.startsWith('OR'))
{
type = 'OR';
expression = expression.removeStart('OR');
}
else if (expression.startsWith('AND'))
{
type = 'AND';
expression = expression.removeStart('AND');
}
else
{
String numberValue;
if (expression.indexOf(' ') == -1) // at the end of string
{
numberValue = expression;
expression = '';
}
else
{
numberValue = expression.substring(0, expression.indexOf(' '));
expression = expression.substring(expression.indexOf(' '));
}
formulaParts.add(new FormulaNumberEvaluator(Integer.valueOf(numberValue)));
}
}
}
public Boolean evaluate(Object record, Map<Integer, Boolean> evaluatedResultByNumber)
{
Boolean result = !(this.type == 'OR');
for (IFormulaEvaluator evaluator : formulaParts)
{
Boolean evaluateResult = evaluator.evaluate(record, evaluatedResultByNumber);
if (evaluateResult && (this.type == 'OR'))
{
return true;
}
else if (!evaluateResult && this.type == 'AND')
{
return false;
}
}
return result;
}
public String toSOQL(Map<Integer, String> queryByNumber)
{
String result = '';
for (IFormulaEvaluator evaluator : formulaParts)
{
if (result != '')
result += ' ' + this.type + ' ';
result += evaluator.toSOQL(queryByNumber);
}
return OPEN_BRACKET + result + CLOSE_BRACKET;
}
}
private class FormulaNumberEvaluator implements IFormulaEvaluator
{
private Integer expressionNumber;
public FormulaNumberEvaluator(Integer expressionNumber)
{
this.expressionNumber = expressionNumber;
}
public Boolean evaluate(Object record, Map<Integer, Boolean> evaluatedResultByNumber)
{
if (!evaluatedResultByNumber.containsKey(expressionNumber - 1))
throw new CriteriaException('Expected a formula expression with number: ' + expressionNumber);
return evaluatedResultByNumber.get(expressionNumber - 1);
}
public String toSOQL(Map<Integer, String> queryByNumber)
{
if (!queryByNumber.containsKey(expressionNumber - 1))
throw new CriteriaException('Expected a formula expression with number: ' + expressionNumber);
return queryByNumber.get(expressionNumber - 1);
}
}
/**
* Add an equal to criteria comparing a fields value to a given value
*
@@ -210,6 +364,8 @@ public virtual with sharing class fflib_Criteria
{
if (this.evaluators.isEmpty()) return true;
if (this.type == FORMULA) return evaluateFormula(record);
Boolean result = !(this.type == 'OR');
for (Evaluator evaluator : evaluators)
{
@@ -226,6 +382,16 @@ public virtual with sharing class fflib_Criteria
return result;
}
private Boolean evaluateFormula(Object record)
{
Map<Integer, Boolean> evaluatedResultByNumber = new Map<Integer, Boolean>();
for (Integer i = 0; i < evaluators.size(); i++)
{
evaluatedResultByNumber.put(i, evaluators.get(i).evaluate(record));
}
return formulaEvaluator.evaluate(record, evaluatedResultByNumber);
}
/**
* Compares the fields values to be greater or equal to the given value
* Evaluates to false if the field value is null
@@ -491,6 +657,8 @@ public virtual with sharing class fflib_Criteria
{
if (this.evaluators.isEmpty()) return '';
if (this.type == FORMULA) return formulaToSOQL();
String result = '';
for (Evaluator evaluator : this.evaluators)
{
@@ -506,6 +674,19 @@ public virtual with sharing class fflib_Criteria
return result;
}
private String formulaToSOQL()
{
Map<Integer, String> queryByNumber = new Map<Integer, String>();
for (Integer i = 0; i < this.evaluators.size(); i++)
{
queryByNumber.put(i, evaluators.get(i).toSOQL());
}
String result = this.formulaEvaluator
.toSOQL(queryByNumber);
return result.substring(1, result.length() - 1);
}
private static String operatorToString(fflib_Operator operator)
{
if (operator == fflib_Operator.EQUALS)
@@ -0,0 +1,47 @@
/**
* File Name: fflib_StringUtils
*
* @author : architect ir. Wilhelmus G.J. Velzeboer
*/
public with sharing class fflib_StringUtils
{
public static String getBetweenBrackets(String open, String close, String expression, Integer index)
{
Integer openChar = open.charAt(0);
Integer closeChar = close.charAt(0);
// If index given is invalid and is
// not an opening bracket.
if (expression.charAt(index) != openChar)
new StringException('Expected char \'' + open + '\' as position ' + index);
// Stack to store opening brackets.
List<Integer> stack = new List<Integer>();
// Traverse through string starting from
// given index.
for (Integer i = index; i < expression.length(); i++)
{
// If current character is an opening bracket push it in stack.
if (expression.charAt(i) == openChar)
{
stack.add(expression.charAt(i));
}
// If current character is a closing bracket, pop from stack.
// If stack is empty, then this closing bracket is required bracket.
else if (expression.charAt(i) == closeChar)
{
stack.remove(stack.size()-1);
if (stack.isEmpty())
{
return expression.substring(index + 1, i);
}
}
}
return null;
}
public class StringException extends Exception {}
}
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>51.0</apiVersion>
<status>Active</status>
</ApexClass>
@@ -569,4 +569,33 @@ private with sharing class fflib_CriteriaTest
.evaluate(record)
);
}
@IsTest
static void itShouldEvaluateFormula()
{
// GIVEN a record
Contact record = new Contact(
LastName = 'Smith',
FirstName = 'John',
Department = 'MIB1'
);
// WHEN we process a formula criteria it should evaluate the formula
System.assert(
new fflib_Criteria()
.FormulaCriteria('(1 AND 2) OR (1 AND 3)')
.equalTo(Contact.LastName, 'Smith')
.equalTo(Contact.FirstName, 'John')
.equalTo(Contact.Department, 'MIB')
.evaluate(record)
);
System.assertEquals(false,
new fflib_Criteria()
.FormulaCriteria('(1 AND 2) OR (1 AND 3)')
.equalTo(Contact.LastName, 'Ford')
.equalTo(Contact.FirstName, 'John')
.equalTo(Contact.Department, 'MIB')
.evaluate(record)
);
}
}