expression_language
Dart library for parsing and evaluating expressions.
Main goal
Main goal of this library is to be able to parse and evaluate expressions like:
4 * 2.5 + 8.5 + 1.5 / 3.0
3* @control1.numberProperty1 < (length(@control2.stringProperty1 + "test string") - 42)
(!@control1.boolProperty1 && length(@control2.stringProperty1) == 21) ? "string1" : "string2"
Features
Currently there are multiple supported data types and operations.
Data types
String
-> maps directly to the DartString
bool
-> maps directly to the Dartbool
Integer
-> wrapper around the Dartint
Decimal
-> Custom typeDateTime
-> maps directly to the DartDateTime
Duration
-> maps directly to the DartDuration
All the data types above are non-nullable. Since veresion 1.0 we also support nullable types that are mapped to the Dart nullable types. To cast away nullability you can use postfix exclamation mark operator !
in an expression.
Note: To be able to easily work with financial data and not to lose precision we decided to use Decimal
data type taken from dart-decimal instead of double
. To keep our expression definitions strongly typed and to have a common way to work with all number data types we introduced base Number
data type class which is simmilar to Dart num
class. Since we can't modify definition of the Dart int
we have also introduced Integer
data type which is a simple wrapper around the int
and which also extends Number
. There is a conversion expression from Integer
to int
and from Decimal
to double
so higher layers can hide those data types as an implementation detail.
To learn more about DateTime data type in expressions see this merge request.
Operations
There are most of the standard operations working on the data types above. For example you can use most of the arithmetic operators like +
,-
, *
, /
, ~/
, %
or the logical operators like &&
, ||
, !
, <
, >
, <=
, >=
, ==
.
To be able to reference another expression from the expression itself we use a construct @element.propertyName
. The element
can map to any type extending ExpressionProviderElement
.
Both element
and propertyName
must consist only from alpha numeric characters or an underscore and can't start with a number.
There are also special functions like length
which returns length of the string. Each function paramter can also come from an expression.
Here is the complete list:
Function | Description | Sample |
---|---|---|
bool contains(String value, String searchValue) | Returns true if value constains searchValue | contains("abcd", "bc") |
String toString<T>(<T> value) | Returns .toString of the value | toString(5) |
int durationInDays(Duration value) | Returns duration in days of a given duration value | durationInDays(duration("P5D1H")) |
int durationInHours(Duration value) | Returns duration in hours of a given duration value | durationInHours(duration("P5D1H")) |
int durationInMinutes(Duration value) | Returns duration in minutes of a given duration value | durationInMinutes(duration("P5D1H")) |
int durationInSeconds(Duration value) | Returns duration in seconds of a given duration value | durationInSeconds(duration("P5D1H")) |
bool startsWith(String value, String searchValue) | Returns true if value starts with searchValue | startsWith("Hello", "He") |
bool endsWith(String value, String searchValue) | Returns true if value ends with searchValue | startsWith("Hello", "lo") |
bool isEmpty(String value) | Returns true if value is empty String | isEmpty("") |
bool isNull(String value) | Returns true if value is null | isNull(someNullExpression) |
bool isNullOrEmpty(String value) | Returns true if value is null or empty String | isNullOrEmpty("") |
bool matches(String value, String regex) | Returns true if value fully matches regex expression | matches("test@email.com","^a-zA-Z0-9_.+- +@a-zA-Z0-9- +.a-zA-Z0-9- +$") |
int length(String value) | length of the string | length("Hi") |
int length(String value) | length of the string | length("Hi") |
int count<T>(List<T> value) | length of the string | count(@element.array) |
DateTime dateTime(String value) | Try to parse value into DateTime, throws InvalidParameterException if it fails | dateTime("1978-03-20 00:00:00.000") |
DateTime now() | Returns DateTime.now() | now() |
DateTime nowInUtc() | Returns DateTime.now().toUtc() | nowInUtc() |
Duration diffDateTime(DateTime left, DateTime right) | Returns difference between two dates - value is always positive | diff(dateTime("1978-03-20"), dateTime("1976-03-20")) |
Duration duration(String value) | Returns duration from Iso8601 String, thows InvalidParameterException if it fails | duration("P5D1H") |
num round(num value, int precision, int roundingMode) | Rounds the value with given precision and rounding mode as an int (described below) | round(1.5, 2, 0) |
num round(num value, int precision, String roundingMode) | Rounds the value with given precision and rounding mode as a String (described below) | round(13.5, 0, "nearestEven") |
Table of the roundingModes
used in the round
function:
Name | Integer representation | Description |
---|---|---|
nearestEven | 0 | Rounds to the nearest value; if the number falls midway, it is rounded to the nearest value with an even least significant digit |
nearestOdd | 1 | Rounds to the nearest value; if the number falls midway, it is rounded to the nearest value with an odd least significant digit |
nearestFromZero | 2 | Rounds to the nearest value; if the number falls midway, it is rounded to the value which is the farthest from zero |
nearestToZero | 3 | Rounds to the nearest value; if the number falls midway, it is rounded to the value which is the closest to zero |
nearestDownward | 4 | Rounds to the nearest value; if the number falls midway, it rounds down |
nearestUpward | 5 | Rounds to the nearest value; if the number falls midway, it rounds down |
towardsZero | 6 | Directed rounding towards zero |
fromZero | 7 | Directed rounding from zero |
up | 8 | Directed rounding towards positive infinity |
down | 9 | Directed rounding towards negative infinity |
Usage
//Create expression parser and pass a map of the types extending ExpressionProviderElement which can hold other expressions.
var expressionGrammarDefinition =
ExpressionGrammarParser({"element": TestFormElement()});
var parser = expressionGrammarDefinition.build();
//Parse the expression.
var result = parser
.parse("(1 + @element.value < 3*5) && false || (2 + 3*(4 + 21)) >= 15");
//The expression now contains strongly typed expression tree representing the expression above.
var expression = result.value as Expression<bool>;
//Evaluate the expression.
bool value = expression.evaluate();
Writing custom expressions
You can write your custom expressions simmilar to the ones in the list above.
First you need to extend the Expression<T>
class, where T is the return value of the Expression. Here is the example of String concatenation expression (in case you don't want to use already implemented + operator):
import 'package:expression_language/expression_language.dart';
class StringConcatenationExpression extends Expression<String> {
final Expression<String> left;
final Expression<String> right;
StringConcatenationExpression(this.left, this.right);
@override
String evaluate() {
return left.evaluate() + right.evaluate();
}
@override
Expression<String> clone(Map<String, ExpressionProviderElement> elementMap) {
return StringConcatenationExpression(left.clone(elementMap), right.clone(elementMap));
}
@override
List<Expression> getChildren() {
return [left, right];
}
}
Now you just need to tell the parser how to create this expression.
To do this you need to pass subclass of FunctionExpressionFactory<T>
to the ExpressionGrammarParser
constructor - there is a customFunctionExpressionFactories
parameter which takes List<FunctionExpressionFactory>
.
In case of a simple expressions you can use already existing subclass ExplicitFunctionExpressionFactory<T>
which takes all the parameters in the constructor so you can avoid subclassing.
This is the registration code for StringConcatenationExpression
:
var expressionGrammarDefinition =
ExpressionGrammarParser({},
customFunctionExpressionFactories: [
ExplicitFunctionExpressionFactory(
name: 'concat',
createFunctionExpression: (parameters) =>
StringConcatenationExpression(parameters[0], parameters[1]),
parametersLength: 2),
],
);