Add comparing to a formula string
This commit is contained in:
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user