diff --git a/docs/fflib_Criteria.asciidoc b/docs/fflib_Criteria.asciidoc index 33e8ea2..fcc35da 100644 --- a/docs/fflib_Criteria.asciidoc +++ b/docs/fflib_Criteria.asciidoc @@ -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 - <> + Evaluates the stored criteria based on the given SObject +- <> + +Adds a criteria based on a formula - <> + Compares the fields values to be greater or equal to the given 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) diff --git a/sfdx-source/apex-extensions/main/default/classes/criteria/fflib_Criteria.cls b/sfdx-source/apex-extensions/main/default/classes/criteria/fflib_Criteria.cls index c582421..ca6e6e1 100644 --- a/sfdx-source/apex-extensions/main/default/classes/criteria/fflib_Criteria.cls +++ b/sfdx-source/apex-extensions/main/default/classes/criteria/fflib_Criteria.cls @@ -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 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 evaluatedResultByNumber); + String toSOQL(Map 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 formulaParts = new List(); + + 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 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 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 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 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 evaluatedResultByNumber = new Map(); + 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 queryByNumber = new Map(); + 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) diff --git a/sfdx-source/apex-extensions/main/default/classes/utils/fflib_StringUtils.cls b/sfdx-source/apex-extensions/main/default/classes/utils/fflib_StringUtils.cls new file mode 100644 index 0000000..40287d2 --- /dev/null +++ b/sfdx-source/apex-extensions/main/default/classes/utils/fflib_StringUtils.cls @@ -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 stack = new List(); + + // 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 {} +} \ No newline at end of file diff --git a/sfdx-source/apex-extensions/main/default/classes/utils/fflib_StringUtils.cls-meta.xml b/sfdx-source/apex-extensions/main/default/classes/utils/fflib_StringUtils.cls-meta.xml new file mode 100644 index 0000000..d75b058 --- /dev/null +++ b/sfdx-source/apex-extensions/main/default/classes/utils/fflib_StringUtils.cls-meta.xml @@ -0,0 +1,5 @@ + + + 51.0 + Active + diff --git a/sfdx-source/apex-extensions/tests/classes/criteria/fflib_CriteriaTest.cls b/sfdx-source/apex-extensions/tests/classes/criteria/fflib_CriteriaTest.cls index 1ae755d..f7818e8 100644 --- a/sfdx-source/apex-extensions/tests/classes/criteria/fflib_CriteriaTest.cls +++ b/sfdx-source/apex-extensions/tests/classes/criteria/fflib_CriteriaTest.cls @@ -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) + ); + } } \ No newline at end of file