Skip to content

Lasse27/SmartExpressions

Repository files navigation

SmartExpressions

.NET Master

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.

Functionality

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:

Conditional 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) }

Logical functions

Function Description Example/s
andReturns true if both expressions evaluate to trueand(true, false)
orReturns true if at least one expression evaluates to trueor(true, false)
notInverts the boolean value of the expressionnot(false)
xorReturns true if exactly one expression is truexor(true, false)
nandReturns false only if both expressions are truenand(true, true)
norReturns true only if both expressions are falsenor(false, false)
xnorReturns true if both expressions have the same boolean valuexnor(true, false)

Comparison functions

Function Description Example/s
eqChecks if two expressions are equaleq(5,5)
neqChecks if two expressions are not equalneq(5,3)
ltChecks if the first expression is less than the secondlt(2,5)
gtChecks if the first expression is greater than the secondgt(10,3)
lteChecks if the first expression is less than or equal to the secondlte(3,3)
gteChecks if the first expression is greater than or equal to the secondgte(4,2)

Arithmetic functions

Function Description Example/s
addReturns the sum of two expressionsadd(2,3)
subReturns the difference of two expressionssub(5,2)
multReturns the product of two expressionsmult(3,4)
divReturns the quotient of two expressionsdiv(10,2)
modReturns the remainder of a divisionmod(10,3)
powRaises the first expression to the power of the secondpow(2,3)
rootComputes the nth root of a numberroot(16,2)
absReturns the absolute (non-negative) valueabs(-5)
negNegates the value (multiplies by -1)neg(7)
randGenerates a random number within the given rangerand(2,10)

Trigonometric functions

Function Description Example/s
sinReturns the sine of an angle (in radians)sin(0)
cosReturns the cosine of an angle (in radians)cos(0)
tanReturns the tangent of an angle (in radians)tan(0)
sinhReturns the hyperbolic sine of a valuesinh(1)
coshReturns the hyperbolic cosine of a valuecosh(1)
tanhReturns the hyperbolic tangent of a valuetanh(1)
asinReturns the inverse sine (arcsine) of a value, result in radiansasin(0.5)
acosReturns the inverse cosine (arccosine) of a value, result in radiansacos(0.5)
atanReturns the inverse tangent (arctangent) of a value, result in radiansatan(1)
radConverts the given value (in degree) to its radians counterpart. rad(360)
degConverts the given value (in radians) to its degree counterpart.deg(3.14)

Keys and constants

Function Description Example/s
piMathematical constant π (~3.14159)pi
eEuler's number (~2.71828)e
tauthe ratio of a circle's circumference to its radius (~6.283185)tau
trueBoolean constant representing truetrue
falseBoolean constant representing falsefalse
nullRepresents the absence of a valuenull
1..9..100..Numeric literal (integer or floating point, optionally negative)42, -3.14
IdentifierVariable reference using a named identifier@{myVar_1}

Usage

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() and Expression.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

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:

  • bool
  • byte
  • short
  • integer
  • double
  • decimal

Progress

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
}

Benchmarks

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.

Contributing

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.

License

The project is published under the MIT License. See LICENSE.txt for more infos.

About

.NET-based framework for parsing and evaluating expressions.

Topics

Resources

License

Stars

Watchers

Forks

Contributors

Languages