Skip to content

Commit

Permalink
Merge pull request #60 from cesarParra/more_fns_2
Browse files Browse the repository at this point in the history
Additional functions + Custom Label + CMT support
  • Loading branch information
cesarParra authored Oct 8, 2023
2 parents b5f541d + 24aca19 commit 92e539e
Show file tree
Hide file tree
Showing 24 changed files with 709 additions and 62 deletions.
144 changes: 139 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,19 @@ Powerful formula-syntax evaluator for Apex and LWC.

### Unlocked Package (`expression` namespace)

[![Install Unlocked Package in a Sandbox](assets/btn-install-unlocked-package-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04tDm0000011MfoIAE)
[![Install Unlocked Package in Production](assets/btn-install-unlocked-package-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04tDm0000011MfoIAE)
[![Install Unlocked Package in a Sandbox](assets/btn-install-unlocked-package-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04tDm0000011MgDIAU)
[![Install Unlocked Package in Production](assets/btn-install-unlocked-package-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04tDm0000011MgDIAU)

Install with SF CLI:

```shell
sf package install --apex-compile package --wait 20 --package 04tDm0000011MfoIAE
sf package install --apex-compile package --wait 20 --package 04tDm0000011MgDIAU
```

Install with SFDX CLI:

```shell
sfdx force:package:install --apexcompile package --wait 20 --package 04tDm0000011MfoIAE
sfdx force:package:install --apexcompile package --wait 20 --package 04tDm0000011MgDIAU
```

### Direct Deployment to Salesforce
Expand Down Expand Up @@ -226,6 +226,32 @@ At this moment advanced querying capabilities like filtering, sorting, or limiti
are not supported. To get over these limitations, you can create a custom formula using Apex. See the
[Advanced Usage](#advanced-usage) section for more information.

## Referencing Org Data

### Custom Labels

You can reference custom labels using the `$Label` global variable.

> ️❗ A namespace needs to be provided when referencing a label. To use the current namesapce
> (or no namespace at all), use the letter `c`.
Label references will automatically be translated to the current user's language.

```apex
Object result = expression.Evaluator.run('$Label.c.MyCustomLabel');
Object result = expression.Evaluator.run('$Label.namespace.MyCustomLabel');
```

### Custom Metadata

You can reference custom metadata records using the `$CustomMetadata` global variable.

To access the data of a custom metadata record, you need to specify the type, the record name, and the field:

```apex
Object result = expression.Evaluator.run('$CustomMetadata.MyCustomMetadataType.MyCustomMetadataRecord.MyField__c');
```

## Advanced Usage

### Custom Formula Functions
Expand Down Expand Up @@ -699,6 +725,16 @@ Accepts 1 argument: the text to convert.
expression.Evaluator.run('UPPER("Hello World")'); // "HELLO WORLD"
```

- `VALUE`

Converts a text string that represents a number to a number.

Accepts 1 argument: the text to convert.

```apex
expression.Evaluator.run('VALUE("1")'); // 1
```

#### Date and Time Functions

- `DATE`
Expand Down Expand Up @@ -856,7 +892,7 @@ expression.Evaluator.run('MINUTE(TIMEVALUE("12:10:00"))'); // 10

- `SECOND`

REturns the second value of a provided time.
Returns the second value of a provided time.

Accepts 1 argument: the time to evaluate.

Expand Down Expand Up @@ -899,8 +935,67 @@ Accepts 1 argument: the date to evaluate.
expression.Evaluator.run('WEEKDAY(DATE(2020, 1, 1))'); // 4
```

- `FORMATDURATION`

Calculates the difference between 2 Times or 2 DateTimes
and formats it as "HH:MM:SS".

Accepts 2 arguments: either 2 Times or 2 DateTimes.

Note that the order of the argument is not important, the
function will always return a positive duration.

```apex
expression.Evaluator.run('FORMATDURATION(TIMEVALUE("12:00:00"), TIMEVALUE("12:00:01"))'); // "00:00:01"
expression.Evaluator.run('FORMATDURATION(DATETIMEVALUE("2015-01-01 00:00:00"), DATETIMEVALUE("2015-01-02 00:00:00"))'); // "24:00:00"
```

- `MONTH`

Returns the month, a number between 1 and 12 (December) in number format of a given date.

Accepts 1 argument: the date to evaluate.

```apex
expression.Evaluator.run('MONTH(DATE(2020, 1, 1))'); // 1
```

#### List Functions

- `FIRST`

Returns the first element of a list.

> Note: If the list is empty, this function will return null.
Accepts 1 argument: the list to evaluate.

```apex
expression.Evaluator.run('FIRST([1, 2, 3])'); // 1
```

- `LAST`

Returns the last element of a list.

> Note: If the list is empty, this function will return null.
Accepts 1 argument: the list to evaluate.

```apex
expression.Evaluator.run('LAST([1, 2, 3])'); // 3
```

- `CONTAINS`

Returns true if the list contains the given value.

Accepts 2 arguments: the list to evaluate and the value to check.

```apex
expression.Evaluator.run('CONTAINS([1, 2, 3], 2)'); // true
```

- `MAP`

Maps to a list using the first argument as the context and the second argument as the expression to evaluate.
Expand Down Expand Up @@ -943,6 +1038,35 @@ Account parentAccountWithChildren = [SELECT Id, Name, (SELECT Id, NumberOfEmploy
Object result = expression.Evaluator.run('AVERAGE(MAP(ChildAccounts, NumberOfEmployees))', parentAccountWithChildren); // 10
```

- `REDUCE`

Reduces a list to a single value using the first argument as the context, the second argument as the expression to evaluate,
and the third argument as the initial value.

Accepts 3 arguments: List of objects, an expression to evaluate, and the initial value.

Provides 2 special variables in the inner expression:
- `$current` - the current item being iterated over
- `$accumulator` - the current value of the accumulator that will be returned

```apex
Object result = expression.Evaluator.run('REDUCE([1, 2, 3], $accumulator + $current, 0)'); // 6
```

This function can be used to build complex objects from a list of data. For example, to aggregate
the number of employees and revenue for an account based on the values from its children, an expression
as follows can be used:

```apex
Id parentAccountId = '001000000000000AAA';
String formula = 'REDUCE(ChildAccounts, ' +
'{"employees": NumberOfEmployees + GET($accumulator, "employees"), "revenue": AnnualRevenue + GET($accumulator, "revenue")}, ' +
'{"employees": 0, "revenue": 0}' +
')';
Object result = Evaluator.run(formula, parentAccountId);
// { "employees": 10, "revenue": 1000000 }
```

- `WHERE`

Filters a list using the first argument as the context and the second argument as the expression to evaluate.
Expand Down Expand Up @@ -1149,6 +1273,16 @@ expression.Evaluator.run('MIN(LIST(1, 2, 3))'); // 1
expression.Evaluator.run('MIN(1, 2, 3)'); // 1
```

- `MOD`

Returns the remainder of one number divided by another.

Accepts 2 arguments: the dividend and the divisor.

```apex
expression.Evaluator.run('MOD(5, 2)'); // 1
```

- `ROUND`

Returns a rounded number. Optionally specify the number of decimal places to round to.
Expand Down
164 changes: 164 additions & 0 deletions expression-src/main/api/tests/EvaluatorTest.cls
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,16 @@ private class EvaluatorTest {
Assert.areEqual(false, Evaluator.run('CONTAINS("Hello World", "Goodbye")'));
}

@IsTest
private static void containsFunctionReturnsTrueIfAListContainsTheGivenValue() {
Assert.areEqual(true, Evaluator.run('CONTAINS(LIST(1, 2, 3), 2)'));
}

@IsTest
private static void containsFunctionReturnsFalseIfAListDoesNotContainTheGivenValue() {
Assert.areEqual(false, Evaluator.run('CONTAINS(LIST(1, 2, 3), 4)'));
}

@IsTest
private static void lowerFunctionReturnsLowercaseString() {
Assert.areEqual('hello world', Evaluator.run('LOWER("Hello World")'));
Expand Down Expand Up @@ -1321,4 +1331,158 @@ private class EvaluatorTest {
return concatenated;
}
}

@IsTest
private static void formatDurationFormatsTheTimeBetweenTwoTimes_hoursDifference() {
String formula = 'FORMATDURATION(TIMEVALUE("01:00:00"), TIMEVALUE("02:00:00"))';
Assert.areEqual('01:00:00', Evaluator.run(formula));
}

@IsTest
private static void formatDurationFormatsTheTimeBetweenTwoTimes_minutesDifference() {
String formula = 'FORMATDURATION(TIMEVALUE("01:30:00"), TIMEVALUE("02:00:00"))';
Assert.areEqual('00:30:00', Evaluator.run(formula));
}

@IsTest
private static void formatDurationFormatsTheTimeBetweenTwoTimes_secondsDifference() {
String formula = 'FORMATDURATION(TIMEVALUE("01:30:00"), TIMEVALUE("01:30:45"))';
Assert.areEqual('00:00:45', Evaluator.run(formula));
}

@IsTest
private static void formatDurationFormatsTheTimeBetweenTwoTimes_multipleDifferences() {
String formula = 'FORMATDURATION(TIMEVALUE("01:45:00"), TIMEVALUE("09:00:01"))';
Assert.areEqual('07:15:01', Evaluator.run(formula));
}

@IsTest
private static void formatDurationFormatsTheTimeBetweenTwoTimes_firstTimeGreaterThanSecond() {
String formula = 'FORMATDURATION(TIMEVALUE("02:00:00"), TIMEVALUE("01:00:00"))';
Assert.areEqual('01:00:00', Evaluator.run(formula));
}

@IsTest
private static void formatDurationFormatsTheTimeBetweenTwoDateTimes_daysDifference() {
String formula = 'FORMATDURATION(DATETIMEVALUE("2015-01-01 00:00:00"), DATETIMEVALUE("2015-01-02 00:00:00"))';
Assert.areEqual('24:00:00', Evaluator.run(formula));
}

@IsTest
private static void formatDurationFormatsTheTimeBetweenTwoDateTimes_hoursDifference() {
String formula = 'FORMATDURATION(DATETIMEVALUE("2015-01-01 01:00:00"), DATETIMEVALUE("2015-01-01 02:00:00"))';
Assert.areEqual('01:00:00', Evaluator.run(formula));
}

@IsTest
private static void formatDurationFormatsTheTimeBetweenTwoDateTimes_minutesDifference() {
String formula = 'FORMATDURATION(DATETIMEVALUE("2015-01-01 01:30:00"), DATETIMEVALUE("2015-01-01 02:00:00"))';
Assert.areEqual('00:30:00', Evaluator.run(formula));
}

@IsTest
private static void formatDurationFormatsTheTimeBetweenTwoDateTimes_secondsDifference() {
String formula = 'FORMATDURATION(DATETIMEVALUE("2015-01-01 01:30:00"), DATETIMEVALUE("2015-01-01 01:30:45"))';
Assert.areEqual('00:00:45', Evaluator.run(formula));
}

@IsTest
private static void formatDurationFormatsTheTimeBetweenTwoDateTimes_multipleDifferences() {
String formula = 'FORMATDURATION(DATETIMEVALUE("2015-01-01 01:45:00"), DATETIMEVALUE("2015-01-01 09:00:01"))';
Assert.areEqual('07:15:01', Evaluator.run(formula));
}

@IsTest
private static void monthReturnsANumberBetween1And12ForTheSpecifiedDate() {
Assert.areEqual(1, Evaluator.run('MONTH(DATE(2015, 1, 1))'));
Assert.areEqual(12, Evaluator.run('MONTH(DATE(2015, 12, 1))'));
}

@IsTest
private static void modFunctionReturnsTheRemainderOfTheDivision() {
Assert.areEqual(1, Evaluator.run('MOD(5, 2)'));
}

@IsTest
private static void valueFunctionConvertsAStringToANumber() {
Assert.areEqual(1, Evaluator.run('VALUE("1")'));
}

@IsTest
private static void firstFunctionReturnsTheFirstElementOfAList() {
Assert.areEqual(1, Evaluator.run('FIRST([1, 2, 3])'));
}

@IsTest
private static void firstFunctionReturnsNullIfTheListIsEmpty() {
Assert.isNull(Evaluator.run('FIRST([])'));
}

@IsTest
private static void lastFunctionReturnsTheLastElementOfAList() {
Assert.areEqual(3, Evaluator.run('LAST([1, 2, 3])'));
}

@IsTest
private static void lastFunctionReturnsNullIfTheListIsEmpty() {
Assert.isNull(Evaluator.run('LAST([])'));
}

@IsTest
private static void reduceCanSumAListOfNumbers() {
Assert.areEqual(6, Evaluator.run('REDUCE([1, 2, 3], $current + $accumulator, 0)'));
}

@IsTest
private static void reduceWorksWithSObjectExpressions() {
Account parentAccount = new Account(Name = 'Parent');
insert parentAccount;

Account childAccount1 = new Account(Name = 'Child1', ParentId = parentAccount.Id, NumberOfEmployees = 10, AnnualRevenue = 100);
Account childAccount2 = new Account(Name = 'Child2', ParentId = parentAccount.Id, NumberOfEmployees = 20, AnnualRevenue = 200);
Account childAccount3 = new Account(Name = 'Child3', ParentId = parentAccount.Id, NumberOfEmployees = 30, AnnualRevenue = 300);
insert new List<SObject>{
childAccount1, childAccount2, childAccount3
};

String formula = 'REDUCE(ChildAccounts, NumberOfEmployees + $accumulator, 0)';
Object result = Evaluator.run(formula, parentAccount.Id);

Assert.areEqual(60, result);
}

@IsTest
private static void reduceToMap() {
Account parentAccount = new Account(Name = 'Parent');
insert parentAccount;

Account childAccount1 = new Account(Name = 'Child1', ParentId = parentAccount.Id, NumberOfEmployees = 10, AnnualRevenue = 100);
Account childAccount2 = new Account(Name = 'Child2', ParentId = parentAccount.Id, NumberOfEmployees = 20, AnnualRevenue = 200);
Account childAccount3 = new Account(Name = 'Child3', ParentId = parentAccount.Id, NumberOfEmployees = 30, AnnualRevenue = 300);
insert new List<SObject>{
childAccount1, childAccount2, childAccount3
};

String formula = 'REDUCE(ChildAccounts, ' +
'{"employees": NumberOfEmployees + GET($accumulator, "employees"), "revenue": AnnualRevenue + GET($accumulator, "revenue")}, ' +
'{"employees": 0, "revenue": 0}' +
')';
Object result = Evaluator.run(formula, parentAccount.Id);

Assert.areEqual(60, ((Map<Object, Object>) result).get('employees'));
Assert.areEqual(600, ((Map<Object, Object>) result).get('revenue'));
}

@IsTest
private static void canEvaluateCustomLabels() {
LabelWrapper.mockLabel = new DummyLabel();
String formula = '$Label.c.MyLabelName';
Assert.areEqual('Value', Evaluator.run(formula));
}

private class DummyLabel implements LabelWrapper.ILabel {
public String get(String namespace, String label, String language) {
return 'Value';
}
}
}
4 changes: 2 additions & 2 deletions expression-src/main/src/helpers/AstPrinter.cls
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public with sharing class AstPrinter implements Visitor {
}

public Object visit(Expr.Variable variable) {
return variable.name.lexeme;
return 'Variable(' + variable.name.lexeme + ')';
}

public Object visit(Expr.MergeField mergeField) {
Expand All @@ -43,7 +43,7 @@ public with sharing class AstPrinter implements Visitor {

public Object visit(Expr.GetExpr getExpr) {
return parenthesize(
'GET:' + String.valueOf(visit(getExpr.field)),
'GET:' + getExpr.field.lexeme,
new List<Expr>{
getExpr.objectExpr
}
Expand Down
Loading

0 comments on commit 92e539e

Please sign in to comment.