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
Branch | Status |
---|---|
master | |
dev | |
nuget |
- Basic mathematical expression evaluation
- System.Math methods and constants directly available (some like Max, Min, Avg are improved)
- Some useful functions for example to create List and Arrays
- Custom variables definition
- On the fly variables and functions evaluation (To easily extend possibilities, Manage also on instance Property and Methods)
- A large set of C# operators availables
- Instance and static methods and properties access like as in C#
- You can call Methods and/or Properties on your own classes (just pass a object as custom variables)
- C# primary types
- Use strings as in C# (@"",
$"", $ @"" available) - Lambda expressions
- Classes like File, Directory, Regex, List ... available (You can extend the list of Namespaces)
- Create instance with new(MyClassName, constructorArgs) or new MyClassName(constructorArgs)
- Call void methods with fluid prefix convention to chain operations
- Manage now assignation operators like =, +=, -=, *= ...
- Manage now postfix operators ++ and --
- Manage ExpandoObject
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.
Install the following nuget package :
Install-Package CodingSeb.ExpressionEvaluator
or copy the CodingSeb.ExpressionEvaluator/ExpressionEvaluator.cs in your project :
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
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
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 |
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}
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.
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
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;
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
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 |
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++)
...
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.
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.
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
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));
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"
- 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
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