SmartExpressions is a .NET-based framework for parsing and evaluating expressions. It provides a custom expression parser and evaluator supporting arithmetic, logical, and comparison operators, as well as variables, constants, and nested expressions. The library is designed for applications that require dynamic runtime calculation and interpretation of expressions.
The project was originally made for personal use but I decided to make it public. Although I want to exclaim that this project is still in the works.
The evaluation ultimately consists of a parser and a lexer/tokenizer. The lexer determines the tokens from the input string using statically registered values. The tokens are then checked with the parser and subsequently converted into an AST (abstract syntax tree).
In general, the evaluation is quite lenient with incorrect types and/or null values.
For example, a True in a LessThan (LT) comparison is handled as 1.
This results in the following functions:
| Function | Description | Example/s |
|---|---|---|
| if | Evaluates a condition and returns one expression if true, otherwise another |
if( eq(1,1) )
{
add(2,3)
}
elif ( eq(1,2) )
{
mult(1,9)
}
else
{
sub(5,2)
}
|
| Function | Description | Example/s |
|---|---|---|
| and | Returns true if both expressions evaluate to true | and(true, false) |
| or | Returns true if at least one expression evaluates to true | or(true, false) |
| not | Inverts the boolean value of the expression | not(false) |
| xor | Returns true if exactly one expression is true | xor(true, false) |
| nand | Returns false only if both expressions are true | nand(true, true) |
| nor | Returns true only if both expressions are false | nor(false, false) |
| xnor | Returns true if both expressions have the same boolean value | xnor(true, false) |
| Function | Description | Example/s |
|---|---|---|
| eq | Checks if two expressions are equal | eq(5,5) |
| neq | Checks if two expressions are not equal | neq(5,3) |
| lt | Checks if the first expression is less than the second | lt(2,5) |
| gt | Checks if the first expression is greater than the second | gt(10,3) |
| lte | Checks if the first expression is less than or equal to the second | lte(3,3) |
| gte | Checks if the first expression is greater than or equal to the second | gte(4,2) |
| Function | Description | Example/s |
|---|---|---|
| add | Returns the sum of two expressions | add(2,3) |
| sub | Returns the difference of two expressions | sub(5,2) |
| mult | Returns the product of two expressions | mult(3,4) |
| div | Returns the quotient of two expressions | div(10,2) |
| mod | Returns the remainder of a division | mod(10,3) |
| pow | Raises the first expression to the power of the second | pow(2,3) |
| root | Computes the nth root of a number | root(16,2) |
| abs | Returns the absolute (non-negative) value | abs(-5) |
| neg | Negates the value (multiplies by -1) | neg(7) |
| rand | Generates a random number within the given range | rand(2,10) |
| Function | Description | Example/s |
|---|---|---|
| sin | Returns the sine of an angle (in radians) | sin(0) |
| cos | Returns the cosine of an angle (in radians) | cos(0) |
| tan | Returns the tangent of an angle (in radians) | tan(0) |
| sinh | Returns the hyperbolic sine of a value | sinh(1) |
| cosh | Returns the hyperbolic cosine of a value | cosh(1) |
| tanh | Returns the hyperbolic tangent of a value | tanh(1) |
| asin | Returns the inverse sine (arcsine) of a value, result in radians | asin(0.5) |
| acos | Returns the inverse cosine (arccosine) of a value, result in radians | acos(0.5) |
| atan | Returns the inverse tangent (arctangent) of a value, result in radians | atan(1) |
| rad | Converts the given value (in degree) to its radians counterpart. | rad(360) |
| deg | Converts the given value (in radians) to its degree counterpart. | deg(3.14) |
| Function | Description | Example/s |
|---|---|---|
| pi | Mathematical constant π (~3.14159) | pi |
| e | Euler's number (~2.71828) | e |
| tau | the ratio of a circle's circumference to its radius (~6.283185) | tau |
| true | Boolean constant representing true | true |
| false | Boolean constant representing false | false |
| null | Represents the absence of a value | null |
| 1..9..100.. | Numeric literal (integer or floating point, optionally negative) | 42, -3.14 |
| Identifier | Variable reference using a named identifier | @{myVar_1} |
An example expression looks like this:
[Fact]
public void Simple_Expression()
{
Expression expression = new Expression("add(1, 1)");
EvaluationResult operation = expression.Evaluate();
// Assert
Assert.NotNull(operation.GetValue());
Assert.Equal(2D, operation.GetValue());
// Output
_outputHelper.WriteLine(operation.GetValue().ToString()); // 2.0D
}Function calls can be nested arbitrarily:
[Fact]
public void Simple_Nested_Expression()
{
Expression expression = new Expression("add(1, mult(5,5))");
EvaluationResult operation = expression.Evaluate();
// Assert
Assert.NotNull(operation.GetValue());
Assert.Equal(26D, operation.GetValue());
// Output
_outputHelper.WriteLine(operation.GetValue().ToString()); // 26.0D
}Expressions are not affected by spaces and line breaks:
public void Simple_Expression_With_Whitespace()
{
Expression expression = new Expression("add (1 , mult (5, 5))");
EvaluationResult operation = expression.Evaluate();
// Assert
Assert.NotNull(operation.GetValue());
Assert.Equal(26D, operation.GetValue());
// Output
_outputHelper.WriteLine(operation.GetValue().ToString()); // 26.0D
}The parsing and tokenization/lexing of the input values is first performed when the formula is evaluated for the first time. After that, it is only performed again when the formula changes, for example, by Expression.SetFormula().
In general, the methods
Expression.Tokenize()andExpression.Parse()are idempotent. They only run once as long as the expression input does not change.
If parsing and/or tokenizing is to be performed earlier, automatic parsing and tokenizing can be disabled using the AssembleOnEvaluation property. The user is then responsible for parsing and tokenizing the expression at the appropriate moment.
Identifiers are the way to integrate your own values into expressions. To do this, the value must first be bound to the expression using the BindParameters() method via a key-value pair.
[Fact]
public void Expression_With_Identifier()
{
Expression expression = new Expression("add(@{Key_1}, 25)");
_ = expression.RegisterBinding("Key_1", 66);
EvaluationResult operation = expression.Evaluate();
// Assert
Assert.NotNull(operation.GetValue());
Assert.Equal(91D, operation.GetValue());
// Output
_outputHelper.WriteLine(operation.GetValue().ToString()); // 91.0D
}Rebinding parameters with the same key overwrites the values of the bound key.
[Fact]
public void Expression_With_Rebound_Identifier()
{
Expression expression = new Expression("add(@{Key_1}, 25)");
_ = expression.RegisterBinding("Key_1", 66);
EvaluationResult operation = expression.Evaluate();
// Assert
Assert.NotNull(operation.GetValue());
Assert.Equal(91D, operation.GetValue());
// Output
_outputHelper.WriteLine(operation.GetValue().ToString()); // 91.0D
_ = expression.RegisterBinding("Key_1", 60);
EvaluationResult operation2 = expression.Evaluate();
// Assert
Assert.NotNull(operation.GetValue());
Assert.Equal(85D, operation2.GetValue());
// Output
_outputHelper.WriteLine(operation2.GetValue().ToString()); // 85.0D
}Binding only supports primary types. To be precise, these are the following types:
boolbyteshortintegerdoubledecimal
The Expression.Evaluate() method accepts an optional parameter IProgress<string>. If you enter a corresponding object of this type into the method, intermediate results of the individual nodes are returned.
[Fact]
public void Simple_Expression_With_Progress()
{
Progress<string> progress = new Progress<string>();
progress.ProgressChanged += (_, e) => _outputHelper.WriteLine(e);
Expression expression = new Expression("add(sub(2,1),mult(5,5))");
EvaluationResult operation = expression.Evaluate(progress);
// Assert
Assert.NotNull(operation.GetValue());
Assert.Equal(26D, operation.GetValue());
// Output
_outputHelper.WriteLine(operation.GetValue().ToString());
// Console output
// SUB(2, 1) = 1
// ADD(SUB(2, 1), MULT(5, 5)) = 26
// MULT(5, 5) = 25
// 26
}I am continuing to work on improving the performance of the lexer, parser, and evaluator.
I am using DotNetBenchmark to analyze performance and will publish benchmark results at appropriate times.
If you don't like something, want to suggest ideas, or have encountered a bug, feel free to create an issue or pull request. Forks are always welcome. I appreciate all feedback, whether positive or negative.
The project will initially be managed exclusively by me. For serious inquiries, please contact me at lassehillen@gmx.de.
The project is published under the MIT License. See LICENSE.txt for more infos.