Math.evaluation
This .NET library allows you to evaluate and compile any mathematical expression from a string dynamically at runtime. It supports a wide range of operations and allows for the use of custom variables, operators, and functions. The evaluator can be configured for different contexts, such as scientific, programming, boolean math expressions.
Install / Use
/learn @AntonovAnton/Math.evaluationREADME
Math Expression Evaluator in .NET
NuGet packages:
MathEvaluator is a .NET library that allows you to evaluate and compile any mathematical expressions from a string dynamically.
Features
- Supports different mathematical contexts, such as scientific, programming, and other custom contexts.
- Evaluates Boolean logic, as well as Double, Decimal, and Complex numbers.
- Supports
INumberBase<T>types (.NET 7+), includingBigInteger,int,long,float,Half, and other numeric types. - Compiles a math expression string into executable code and produces a delegate that represents the math expression.
- Provides variable support within math expressions (including expression-defined variables).
- Extensible with custom functions and operators.
- Fast and comprehensive. More than 6000 tests are passed, including complex math expressions (for example, -3^4sin(-π/2) or sin-3/cos1).
- Multi-targets .NET 7, .NET 8, .NET 9, and .NET 10 for optimal performance on each platform.
- .NET Standard 2.1 compatible in versions prior to 3.0.0.
Articles
Evaluating Boolean logical expressions.
Performance
This math expression evaluator is designed for exceptional performance by leveraging modern .NET features and best practices, which is why it targets .NET Standard 2.1 or higher.
NOTE: In version 3.0.0 or higher, minimal .NET 7 is required.
This high-performance evaluator stands out due to its use of ReadOnlySpan<char>, and avoidance of regular expressions. These design choices collectively ensure minimal memory allocation, fast parsing, and efficient execution.
The evaluator uses recursive method calls to handle mathematical operations based on operator precedence and rules, an operator with highest precedence is evaluating first. This approach avoids the overhead associated with stack or queue data structures.
The evaluator uses a prefix tree, also known as a trie (pronounced "try"), for efficient searching of variables, operators, and functions by their keys (names) when providing a specific mathematical context or adding custom variables, operators, and functions is required.
Benchmark Results
Let's compare performance of evaluating the mathematical expression:
22888.32 * 30 / 323.34 / .5 - -1 / (2 + 22888.32) * 4 - 6
Below are the results comparing MathEvaluator with the NCalc library:
| Method | Runtime | Mean | Error | StdDev | Gen0 | Allocated | |--------|---------|------|-------|--------|------|-----------| | MathEvaluator | .NET 10.0 | 465.7 ns | 1.14 ns | 1.07 ns | 0.0033 | 112 B | | NCalc | .NET 10.0 | 6,236.1 ns | 26.36 ns | 23.36 ns | 0.1373 | 4480 B | | MathEvaluator | .NET 8.0 | 591.8 ns | 3.14 ns | 2.94 ns | 0.0029 | 112 B | | NCalc | .NET 8.0 | 7,225.6 ns | 91.16 ns | 85.27 ns | 0.1450 | 4512 B |
Performance gain: MathEvaluator is 10-13x faster than NCalc with 40x less memory allocation.
More Complex Expressions
| Expression | Library | .NET 10.0 | .NET 8.0 | Speedup |
|------------|---------|-----------|----------|---------|
| "Sin(a) + Cos(b)" | MathEvaluator | 317.5 ns | 373.4 ns | 13-15x |
| | NCalc | 4,275.0 ns | 4,954.0 ns | |
| "A or not B and (C or B)" | MathEvaluator | 411.1 ns | 506.8 ns | 11-12x |
| | NCalc | 4,623.2 ns | 5,431.4 ns | |
NOTE: If the evaluation results depend on variable values, compilation is a better alternative for repeated evaluations.
Compilation
Added in version 2.0.0
By using compilation, you can convert any mathematical expression string into a delegate, such as Func<T, TResult> or Func<TResult>, which significantly improves performance when evaluating the expression.
However, since compilation takes additional time, and MathEvaluator already provides fast evaluation, it is recommended to compile the expression only if you plan to evaluate it repeatedly, particularly for 150 or more iterations. For detailed performance insights, refer to the benchmarks.
The compiled delegate can be executed with different parameters, allowing you to pass variables and functions as arguments. This feature is particularly useful for scenarios where the same expression needs to be evaluated with different variable values or functions.
In version 2.3.0 you can also use a Dictionary<string, TResult> as a parameter. This allows you to pass variables and their values in a more flexible way, especially when working with dynamic inputs or when the structure of input parameters is not known in advance.
In version 2.6.0 support for ExpandoObject as a parameter type has been added. This allows for dynamic binding of variables in a more flexible manner, no predefined class structure required.
In version 2.3.1 added IExpressionCompiler interface, which allows you to inject your own compiler. This is useful if you want to use a different compiler or if you want to customize the compilation process in some way.
MathEvaluator.FastExpressionCompiler is an extension of the MathEvaluator library that uses the FastExpressionCompiler to provide performance improvements of up to 10-40x compared to the built-in .NET LambdaExpression.Compile() method.
This library includes all features of the MathEvaluator library but adds a dependency on FastExpressionCompiler. For more details, refer to the documentation.
How to use
Examples of using string extentions:
"22888.32 * 30 / 323.34 / .5 - -1 / (2 + 22888.32) * 4 - 6".Evaluate();
"22888.32 * 30 / 323.34 / .5 - -1 / (2 + 22888.32) * 4 - 6".EvaluateDecimal();
"$22,888.32 * 30 / 323.34 / .5 - - 1 / (2 + $22,888.32) * 4 - 6".Evaluate(null, new CultureInfo("en-US"));
"22’888.32 CHF * 30 / 323.34 / .5 - - 1 / (2 + 22’888.32 CHF) * 4 - 6".EvaluateDecimal(null, new CultureInfo("de-CH"));
"ln(1/-0.5 + √(1/(0.5^2) + 1))".Evaluate(new ScientificMathContext());
"P * (1 + r/n)^d".EvaluateDecimal(new { P = 10000, r = 0.05, n = 365, d = 31 }, new DecimalScientificMathContext());
"2 ** 100".Evaluate<BigInteger>(new ProgrammingMathContext());
"4 ^ 3".Evaluate(new ScientificMathContext());
"4 <> 4 OR 5.4 = 5.4 AND NOT 0 < 1 XOR 1.0 - 1.95 * 2 >= -12.9 + 0.1 / 0.01".EvaluateBoolean(new ProgrammingMathContext());
"¬⊥∧⊤∨¬⊤⇒¬⊤".EvaluateBoolean(new ScientificMathContext());
"sin(2 + 3i) * arctan(4i)/(1 - 6i)".EvaluateComplex(new ComplexScientificMathContext());
"0b1010 * 0o12 / 0xA + 10".Evaluate(); // Mixed numeric systems: 10 * 10 / 10 + 10 = 20
Examples of using an instance of the MathExpression class:
new MathExpression("22’888.32 CHF * 30 / 323.34 / .5 - - 1 / (2 + 22’888.32 CHF) * 4 - 6", null, new CultureInfo("de-CH")).EvaluateDecimal();
new MathExpression("sin(2 + 3i) * arctan(4i)/(1 - 6i)", new ComplexScientificMathContext()).EvaluateComplex();
Examples of passing custom variables and functions as parameters (Support expression-defined variables added in version 2.4.0):
var x1 = 0.5;
var x2 = -0.5;
var y = "x1 * x2"; // expression-defined variable
var sqrt = Math.Sqrt;
Func<double, double> ln = Math.Log;
var value1 = "ln(1/-x1 + sqrt(1/(x2*x2) + 1)) + y"
.Evaluate(new { x1, x2, y, sqrt, ln });
var parameters = new MathParameters();
parameters.BindVariable(x1);
parameters.BindVariable(x2);
parameters.BindExpressionVariable(y); // expression-defined variable
parameters.BindFunction(sqrt);
parameters.BindFunction(d => Math.Log(d), "ln");
var value2 = "ln(1/-x1 + sqrt(1/(x2*x2) + 1)) + y"
.Evaluate(parameters);
Example of using custom context:
var context = new MathContext();
context.BindFunction(Math.Sqrt);
context.BindFunction(d => Math.Log(d), "ln");
context.BindExpressionVariable("x1 * x2", "y"); // expression-defined variable
var value = "ln(1/-x1 + Math.Sqrt(1/(x2*x2) + 1)) + y"
.Evaluate(new { x1 = 0.5, x2 = -0.5 }, context);
Example of evaluating C# expression:
var value = "-2 * Math.Log(1/0.5f + Math.Sqrt(1/Math.Pow(0.5d, 2) + 1L))"
.Evaluate(new DotNetMathContext());
Example of compilation with an object as a parameter:
var fn = "ln(1/x1 + √(1/(x2*x2) + 1))"
.Compile(new { x1 = 0.0, x2 = 0.0 }, new ScientificMathContext());
var value = fn(new { x1 = -0.5, x2
