expression_language

pub package

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 Dart String
  • bool -> maps directly to the Dart bool
  • Integer -> wrapper around the Dart int
  • Decimal -> Custom type
  • DateTime -> maps directly to the Dart DateTime
  • Duration -> maps directly to the Dart Duration

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),
        ],
    );