Skip to content

A Simple Math and Pseudo C# Expression Evaluator in One C# File

License

Notifications You must be signed in to change notification settings

am-creations/ExpressionEvaluator

 
 

Repository files navigation

ExpressionEvaluator

A Simple Math and Pseudo C# Expression Evaluator in One C# File.

And from version 1.2.0 can execute small C# like scripts

It is largely based on and inspired by the following resources this post on stackoverflow, NCalc, C# Operators and C# Statement Keywords

Status

Branch Status
master Build Status
dev Dev Status
nuget NuGet Status

Features

And with ScriptEvaluate method

  • Small C# like script evaluation (Multi expressions separated by ;)
  • Some conditional and loop blocks keywords (if, while, for, foreach ...)
  • Multi-line (multi expression) Lambda expressions.

Getting started

Install the following nuget package :

Install-Package CodingSeb.ExpressionEvaluator

See on Nuget.org

or copy the CodingSeb.ExpressionEvaluator/ExpressionEvaluator.cs in your project :

Basic C# usage

Simple expressions

using CodingSeb.ExpressionEvaluator;
//...
string expression;
//...
ExpressionEvaluator evaluator = new ExpressionEvaluator();
Console.WriteLine(expression);
Console.WriteLine(evaluator.Evaluate(expression));

Results with some expressions :

1+1
2

2 + 3 * 2
8

(2 + 3) * 2
10

Pi
3.14159265358979

Pow(2, 4)
16

Sqrt(2) / 3
0.471404520791032

"Hello" + " " + "World"
Hello World

Max(1+1, 2+3, 2*6, Pow(2, 3))
12

Array(2, $"Test { 2 + 2 } U", true).Length
3

Array(2, $"Test { 2 + 2 } U", true)[1]
Test 4 U

Array(2, $"Test { 2 + 2 } U", true)[2] ? "yes" : "no"
yes

false ? "yes" : "no"
no

"Hello\nWorld"
Hello
World

@"Hello\nWorld"
Hello\nWorld

Regex.Match("Test 34 Hello/-World", @"\d+").Value
34

int.Parse(Regex.Match("Test 34 Hello/-World", @"\d+").Value) + 2
36

3 / 2
1

3d / 2d
1.5

(float)3 / (float)2
1.5

// use new as a function
new(Random).Next(1,10)
4 // or a random value between 1 and 9

// or as a keyword
new Regex(@"\w*[n]\w*").Match("Which word contains the desired letter ?").Value
contains

List("Test", "Hello", "Bye", "How are you?").Find(t => t.Length < 4)
Bye

Enumerable.Range(1,4).Cast().Sum(x =>(int)x)
10

Enumerable.Repeat(3,6).Cast().ToList().Count
6

Enumerable.Repeat(3,6).Cast().ToList()[4]
3

((x, y) => x * y)(4, 2)
8

"Hello"[2] == 'l'
true

Small scripts

using CodingSeb.ExpressionEvaluator;
//...
string script;
//...
ExpressionEvaluator evaluator = new ExpressionEvaluator();
Console.WriteLine("--------------------------------------------");
Console.WriteLine(script);
Console.WriteLine("---------------- Result --------------------");
Console.WriteLine(evaluator.ScriptEvaluate(script));

Results with some scripts :

--------------------------------------------
x = 0;
result = "";

while(x < 5)
{
    result += $"{x},";
    x++;
}

result.Remove(result.Length - 1);
---------------- Result --------------------
0,1,2,3,4

--------------------------------------------
result = "";

for(x = 0; x < 5;x++)
{
    result += $"{x},";
}

result.Remove(result.Length - 1);
---------------- Result --------------------
0,1,2,3,4

--------------------------------------------
x = 0;
y = 1;
result = 0;

if(y != 0)
{
    return 1;
}
else if(x == 0)
{
    return 2;
}
else if(x < 0)
{
    return 3;
}
else
{
    return 4;
}
---------------- Result --------------------
1

--------------------------------------------
x = 0;
y = 0;
result = 0;

if(y != 0)
{
    return 1;
}
else if(x == 0)
{
    return 2;
}
else if(x < 0)
{
    return 3;
}
else
{
    return 4;
}
---------------- Result --------------------
2

--------------------------------------------
x = 5;
y = 0;
result = 0;

if(y != 0)
{
    return 1;
}
else if(x == 0)
{
    return 2;
}
else if(x < 0)
{
    return 3;
}
else
{
    return 4;
}
---------------- Result --------------------
4

--------------------------------------------
Add = (x, y) => x + y;

return Add(3, 4);
---------------- Result --------------------
7

To see more scripts examples see scripts uses for tests in sub directories CodingSeb.ExpressionEvaluator.Tests/Resources

Standard constants (variables)

Constant Value Type
null C# null value N/A
true C# true value System.Boolean
false C# false value System.Boolean
Pi 3.14159265358979 System.Double
E 2.71828182845905 System.Double

Custom variables

You can define your own variables

Examples :

ExpressionEvaluator evaluator = new ExpressionEvaluator();
evaluator.Variables = new Dictionary<string, object>()
{
  { "x", 2,5 },
  { "y", -3.6 },
  { "myVar", "Hello World" },
  { "myArray", new object[] { 3.5, "Test", false },
};
x+y
-1.1

myVar + " !!!"
Hello World !!!

myArray.Length
3

myArray[0]
3.5

myArray[1].Length
4

myArray[2] || true
True

A very useful functionality is that you can store callable delegates in variables :

ExpressionEvaluator evaluator = new ExpressionEvaluator();
evaluator.Variables = new Dictionary<string, object>()
{
    { "Add", new Func<int,int,int>((x, y) => x + y)},
    { "SayHelloTo", new Action<string>(name => Console.WriteLine($"Hello {name} !!!"))},
};
Add(5, 9)
14

SayHelloTo("John")
Hello John !!!
{null}

Standard functions

The following functions are internally defined. (Most of these are System.Math Methods directly accessible)

Name Description Example Result
Abs(double number) Return a double that is the absolute value of number Abs(-3.2d) 3.2d
Acos(double d) Return a double value that is the angle in radian whose d is the cosine
d must be betwteen -1 and 1
Acos(-0.5d) 2.0943951023032d
Array(object obj1, object obj2 ,...) Return a array (System.Object[]) of all given arguments Array(1, "Hello", true) new object[]{1, "Hello", true}
Asin(double d) Return a double value that is the angle in radian whose d is the sine
d must be betwteen -1 and 1
Asin(-0.2d) 0.304692654015398d
Atan(double d) Return a double value that is the angle in radian whose d is the tangent Atan(2.1) 1.1263771168938d
Atan2(double x, double y) Return a double value that is the angle in radian whose the tangente is the quotient of x and y
Atan2(2.1d, 3.4d) 0.553294325322293d
Avg(double nb1, double nb2 ,...) Return a double value that is the average value of all given arguments Avg(1, 2.5, -4, 6.2) 1.425d
Ceiling(double a) Return a double value that is the smallest integer greater than or equal to the specified number. Ceiling(4.23d) 5d
Cos(double angle) Return a double value that is the cosine of the specified angle in radian Cos(2 * Pi) 1d
Cosh(double angle) Return a double value that is the hyperbolic cosine of the specified angle in radian Cosh(2d) 3.76219569108363d
Exp(double d) Return a double value that is e raised to the specified d power Exp(3d) 20.0855369231877d
Floor(double d) Return a double value that is the largest integer less than or equal to the specified d argument Floor(4.23d) 4d
IEEERemainder(double x, double y) Return a double value that is the remainder resulting from the division of x by y IEEERemainder(9, 8) 1d
in(object valueToFind, object obj1, object obj2...) Return a boolean value that indicate if the first argument is found in the other arguments in(8, 4, 2, 8) true
List(object obj1, object obj2 ,...) Return a List (System.Collections.Generic.List) of all given arguments List(1, "Hello", true) new List<object>(){1, "Hello", true}
Log(double a, double base) Return a double value that is the logarithm of a in the specified base Log(64d, 2d) 6d
Log10(double a) Return a double value that is the base 10 logarithm of a specified a Log10(1000d) 3d
Max(double nb1, double nb2 ,...) Return a double value that is the maximum value of all given arguments Max(1d, 2.5d, -4d) 2.5d
Min(double nb1, double nb2 ,...) Return a double value that is the minimum value of all given arguments Min(1d, 2.5d, -4d) -4d
new(TypeOrClass, constructorArg1, constructorArg2 ...) Create an instance of the specified class as first argument and return it. A optional list of additional arguments can be passed as constructor arguments new(Random).next(0,10) 5d // or a random value between 1 and 9
Pow(double x, double y) Return a double value that is x elevate to the power y Pow(2,4) 16d
Round(double d, (optional) int digits, (optional) MidpointRounding mode) Rounds d to the nearest integer or specified number of decimal places. Round(2.432,1) 2.4d
Sign(double d) Return 1,-1 or 0 indicating the sign of d Sign(-12) -1d
Sin(double angle) Return a double value that is the sine of the specified angle in radian Sin(Pi/2) 1d
Sinh(double angle) Return a double value that is the hyperbolic sine of the specified angle in radian Sinh(2d) 3.62686040784702d
Sqrt(double d) Return a double value that is the square root of the specified d value Sqrt(4d) 2d
Tan(double angle) Return a double value that is the tangent of the specified angle in radian Tan(Pi / 4) 1d
Tanh(double angle) Return a double value that is the hyperbolic tangent of the specified angle in radian Tanh(2d) 0.964027580075817d
Truncate(double d) Return a double value that is the integer part of the specified d value Truncate(2.45d) 2d

Remark : The old if function (NCalc style) has been removed. This to avoid conflicts with the new if, else if, else keywords in script mode. To do something similar on a expression level use the conditional operator ( ? : ) instead.

On the fly variables and functions evaluation

In addition to custom variables, you can add variables and/or functions with on the fly evaluation. 2 C# events are provided that are fired when variables or functions are not found as standard ones in evaluation time.

Remark : Can be use to define or redefine on object instances methods or properties

ExpressionEvaluator evaluator = new ExpressionEvaluator();
evaluator.EvaluateVariable += ExpressionEvaluator_EvaluateVariable;
evaluator.EvaluateFunction += ExpressionEvaluator_EvaluateFunction;
//...

private void ExpressionEvaluator_EvaluateVariable(object sender, VariableEvaluationEventArg e)
{
    if(e.Name.ToLower().Equals("myvar"))
    {
        e.Value = 8;
    }
    else if(e.Name.Equals("MultipliedBy2") && e.This is int intValue)
    {
        e.Value = intValue * 2;
    }
}

private void ExpressionEvaluator_EvaluateFunction(object sender, FunctionEvaluationEventArg e)
{
    if(e.Name.ToLower().Equals("sayhello") && e.Args.Count == 1)
    {
        e.Value = $"Hello {e.EvaluateArg(0)}";
    }
    else if(e.Name.Equals("Add") && e.This is int intValue)
    {
        e.Value = intValue + (int)e.EvaluateArg(0);
    }
}
myVar + 2
10

SayHello("Bob")
Hello Bob

3.MultipliedBy2
6

3.Add(2)
5

Go fluid with a simple methods prefixing convention

Since ExpressionEvaluator evaluate one expression at a time. There are cases where we need to use void methods in a fluid syntax manner.

You only need to prefix the method name with "Fluid" or "Fluent"

// Example Add on List
List("hello", "bye").FluidAdd("test").Count
3

List("hello", "bye").Select(x => x.ToUpper()).ToList().FluentAdd("test")[0]
HELLO

List("hello", "bye").Select(x => x.ToUpper()).ToList().FluentAdd("test")[1]
BYE

List("hello", "bye").Select(x => x.ToUpper()).ToList().FluentAdd("test")[2]
test

If needed this fonctionality can be disabled with :

evaluator.OptionFluidPrefixingActive = false;

Primary types

ExpressionEvaluator manage the following list of C# primary types

  • object
  • string
  • bool/bool?
  • byte/byte?
  • char/char?
  • decimal/decimal?
  • double/double?
  • short/short?
  • int/int?
  • long/long?
  • sbyte/sbyte?
  • float/float?
  • ushort/ushort?
  • uint/uint?
  • ulong/ulong?
  • void

Add the ? for nullable types

Operators

ExpressionEvaluator manage a large set of C# operators (See C# Operators)

ExpressionEvaluator respect the C# precedence rules of operators

Here is a list of which operators are supported in ExpressionEvaluator or not

Type Operator Support
Primary x.y Supported
Primary x?.y Supported
Primary x?[y] Supported
Primary f(x) Supported
Primary a[x] Supported
Primary x++ Supported Warning change the state of the postfixed element
Primary x-- Supported Warning change the state of the postfixed element
Primary new Supported you can also use new() function
Primary typeof Supported
Primary checked Not Supported
Primary unchecked Not Supported
Primary default(T) Supported
Primary delegate Not Supported
Primary sizeof Not Supported
Primary -> Not Supported
Unary +x Supported
Unary -x Supported
Unary !x Supported
Unary ~x Not Supported
Unary ++x Not Supported
Unary --x Not Supported
Unary (T)x Supported
Unary await Not Supported
Unary &x Not Supported
Unary *x Not Supported
Multiplicative x * y Supported
Multiplicative x / y Supported
Multiplicative x % y Supported
Additive x + y Supported
Additive x - y Supported
Shift x << y Supported
Shift x >> y Supported
Relational x < y Supported
Relational x > y Supported
Relational x <= y Supported
Relational x >= y Supported
Type-testing is Supported
Type-testing as Not Supported
Equality x == y Supported
Equality x != y Supported
Logical AND x & y Supported
Logical XOR x ^ y Supported
Logical OR x | y Supported
Conditional AND x && y Supported
Conditional OR x || y Supported
Null-coalescing x ?? y Supported
Conditional t ? x : y Supported
Lambda => Supported

Assignation operators

Warning all of the following operators change the value of their left element.

Assignation operators (and also postfix operators (++ and --)) are usable on :

Elements What is changing Options
Custom variables The variable in the Variables dictionary is changed and if the variable doesn't exists, it automatically created with the = operator Can be disabled with evaluator.OptionVariableAssignationActive = false;
Properties or fields on objects If the property/field is not readonly it is changed Can be disabled with evaluator.OptionPropertyOrFieldSetActive = false;
Indexed object like arrays, list or dictionaries The value at the specified index is changed Can be disabled with evaluator.OptionIndexingAssignationActive = false;

Here is the list of available assignation operator

Operator Support
= Supported (Can be use to declare a new variable that will be injected in the Variables dictionary)
+= Supported
-= Supported
*= Supported
/= Supported
%= Supported
&= Supported
|= Supported
^= Supported
<<= Supported
>>= Supported

To declare a variable, types are not yet supported and are for now dynamically deduced.

// Not supported
int x = 2;
string text = "hello";

for(int i = 0; i < 10; i++)
...

// Write this instead :
x = 2;
text = "hello";

for(i = 0; i < 10; i++)
...

Scripts

In addition to simple expression evaluation you can also evaluate small scripts with the method :

//object ScriptEvaluate(string script)
evaluator.ScriptEvaluate(script);

Scripts are just a serie of expressions to evaluate separated with a ; character and leaded by severals additionals keywords.

Script keywords

Currently the following script keywords are supported

Type Operator Support
Selection if Supported
Selection else if Supported
Selection else Supported
Selection switch case Not yet supported
Iteration do ... while Supported
Iteration for Supported
Iteration foreach, in Supported
Iteration while Supported
Jump break Supported in do, for and while blocks
Jump continue Supported in do, for and while blocks
Jump goto Not supported (But if you looked after it -> Booo !!! Bad code)
Jump return Supported
Jump/Exception throw Not supported
Exception Handling try-catch Supported
Exception Handling try-finally Supported
Exception Handling try-catch-finally Supported

Remark : The way ScriptEvaluate works is to evaluate expressions one by one. There is no syntax check before the evaluation. So be aware that syntax or naming bugs only appears in execution and some code can already be evaluated at this time. Futhermore a syntaxic/naming bug in an if-else block for example can simply be ignored until the corresponding condition is met to evaluate the specific line of code.

Code comments

By default comments are not managed in expressions and scripts evaluations. But they can be manually removed with the specific method string RemoveComments(string scriptWithComments)

To be sure that your commented script is evaluated correctly you can do :

ExpressionEvaluator evaluator = new ExpressionEvaluator();
evaluator.ScriptEvaluate(evaluator.RemoveComments(scriptWithComments));

It remove line comments // and blocks comments /* ... */ but keep them in strings

Assemblies, Namespaces and types

To resolve types and namespaces ExpressionEvaluator search in in assemblies loaded in the evaluator.Assemblies list. By default this list Contains all loaded assemblies in the current AppDomain when the constructor of ExpressionEvaluator is called. You can easily Clear, Add or Remove assemblies on this list.

By default the following list of namespaces are available :

  • System
  • System.Linq
  • System.IO
  • System.Text
  • System.Text.RegularExpressions
  • System.ComponentModel
  • System.Dynamic
  • System.Collections
  • System.Collections.Generic
  • System.Collections.Specialized
  • System.Globalization

You can extend or reduce this list :

ExpressionEvaluator evaluator = new ExpressionEvaluator();
evaluator.Namespaces.Add(namespace);
evaluator.Namespaces.Remove(namespaceToRemove);

All types defined in these namespaces are accessibles.

You can also add a specific type :

evaluator.Types.Add(typeof(MyClass));

ExpandoObject

From version 1.2.2 ExpressionEvaluator manage ExpandObject class. ExpandoObject are object that can dynamically create new properties when you assign a value to it. It is also a dictionnary of properties. In ExpressionEvaluator you can use it as a object or as a dictionnary.

Here some examples :

myVar = new ExpandoObject();

myVar.X = 23.5;
myVar.Y = 34.8;

return myVar.X + myVar.Y;
// 58.3
myVar = new ExpandoObject();

myVar["Text"] = "Hello ";

return myVar["Text"] + " Bob" ;
// "Hello Bob"
myVar = new ExpandoObject();

myVar["Text"] = "Hello ";

return myVar.Text + " Bob" ;
// "Hello Bob"
myVar = new ExpandoObject();

myVar.Text = "Hello ";

return myVar["Text"] + " Bob" ;
// "Hello Bob"
obj = new ExpandoObject();

obj.Add = (x, y) => 
{
    text = "The result is : ";
    return text + (x+y).ToString();
};

return obj.Add(3, 4);
// "The result is : 7"

Similar projects

Free

  • NCalc
  • Jint Support scripting but with Javascript
  • DynamicExpresso
  • Flee
  • CS-Script Best alternative (I use it some times) -> Real C# scripts better than ExpressionEvaluator (But everything is compiled. Read the doc. Execution is faster but compilation can make it very slow. And if not done the right way, it can lead to memory leaks)
  • Roslyn The Microsoft official solution

Commercial

I would say every C# evaluation libraries have drawbacks and benefits, ExpressionEvaluator is not an exception so choose wisely (Read docs and licences).

The biggest difference of ExpressionEvaluator is that everything is evaluated on the fly, nothing is compiled or transpile nor in CLR/JIT/IL nor in lambda expressions nor in javascript or other languages stuffs. So it can be slower in some cases (sometimes not) but it also avoid a lot of memory leaks. It already allow to evaluate some small scripts. If you don't want an another .dll file in your project, you only need to copy one C# file in your project. And it's MIT licence

About

A Simple Math and Pseudo C# Expression Evaluator in One C# File

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • C# 100.0%