Evaluate Boolean Expression from String in C# (.NET)

In this article, I will demonstrate how to dynamically evaluate logical mathematical expressions from strings in C#, with high performance. The solution, implemented using the .NET MathEvaluator library, supports logical operations in various mathematical contexts, including programming, scientific computing, and C#. In addition, the library allows you to extend these contexts, as well as add custom variables and functions.

The MathEvaluator library and its documentation are available at GitHub.

In the previous article I have already described in detail the implementation and methods of fast calculation of mathematical expressions in C#. Here I will focus on the specific case of calculating logical expressions and compare the capabilities and performance of MathEvaluator with the well-known NCalc library.

Supported mathematical functions, operators and constants

The MathEvaluator library includes built-in contexts for evaluating complex scientific, programming, and C# mathematical expressions. It offers a set of functions, operators, and constants tailored to these specific contexts. For a complete list of supported functions and capabilities, see documentation. When evaluating a logical expression, if the function returns a value of 0.0, it is interpreted as false, and any other value is considered true, the function behaves the same way Convert.ToBoolean provided by .NET.

Comparison with NCalc

We will use BenchmarkDotNet for performance comparison. Test environment details:

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.4037/23H2/2023Update/SunValley3)
11th Gen Intel Core i7-11800H 2.30GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.300
  [Host]   : .NET 6.0.30 (6.0.3024.21525), X64 RyuJIT AVX2
  .NET 6.0 : .NET 6.0.30 (6.0.3024.21525), X64 RyuJIT AVX2
  .NET 8.0 : .NET 8.0.5 (8.0.524.21615), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI

Example 1: Calculation of a logical program mathematical expression.

Let's compare the performance of calculating a logical expression
A or not B and (C or B):

BenchmarkRunner.Run<Benchmarks>();

[SimpleJob(RuntimeMoniker.Net80)]
[SimpleJob(RuntimeMoniker.Net60)]
[MemoryDiagnoser]
public class Benchmarks
{
    private readonly ProgrammingMathContext _context = new();

    private int _count;

    [Benchmark(Description = "MathEvaluator")]
    public bool MathEvaluator_EvaluateBoolean_ProgrammingExpression()
    {
        _count++;
        bool a = _count % 2 == 0; //randomizing values

        return "A or not B and (C or B)"
            .SetContext(_context)
            .BindVariable(a, "A")
            .BindVariable(!a, "B")
            .BindVariable(a, "C")
            .EvaluateBoolean();
    }

    [Benchmark(Description = "NCalc")]
    public bool NCalc_Evaluate_ProgrammingExpression()
    {
        _count++;
        bool a = _count % 2 == 0; //randomizing values

        var expression = new Expression("A or not B and (C or B)");
        expression.Parameters["A"] = a;
        expression.Parameters["B"] = !a;
        expression.Parameters["C"] = a;

        return (bool)expression.Evaluate();
    }
}

Below are the performance comparison results:

Example 2: Calculation of a logical algebraic mathematical expression.

NCalc does not support logical algebraic expressions such as A∨¬B∧(C∨B). In contrast, MathEvaluator can evaluate such expressions and can be extended with custom math contexts, since MathEvaluator follows simple mathematical rules based on operator precedence, adding other contexts is straightforward.

Let's look at an example of creating a context that supports logical algebraic expressions:

using MathEvaluation.Context;

public class BooleanAlgebraMathContext : MathContext
{
    public BooleanAlgebraMathContext()
    {
        static double andFn(double leftOperand, double rigntOperand)
            => leftOperand != default && rigntOperand != default ? 1.0 : default;

        BindOperator(andFn, '∧', (int)EvalPrecedence.LogicalAnd);

        static double orFn(double leftOperand, double rigntOperand)
            => leftOperand != default || rigntOperand != default ? 1.0 : default;

        BindOperator(orFn, '∨', (int)EvalPrecedence.LogicalOr);

        static double xorFn(double leftOperand, double rigntOperand)
            => leftOperand != default ^ rigntOperand != default ? 1.0 : default;

        BindOperator(xorFn, '⊕', (int)EvalPrecedence.LogicalXor);

        static double logicalNegationFn(double rigntOperand)
            => rigntOperand == default ? 1.0 : default;

        BindConverter(logicalNegationFn, '¬');
    }
}

Let's measure the performance of calculating the expression A∨¬B∧(C∨B) using the newly created BooleanAlgebraMathContext class:

BenchmarkRunner.Run<Benchmarks>();

[SimpleJob(RuntimeMoniker.Net80)]
[SimpleJob(RuntimeMoniker.Net60)]
[MemoryDiagnoser]
public class Benchmarks
{
    private readonly BooleanAlgebraMathContext _context = new();

    private int _count;

    [Benchmark(Description = "MathEvaluator")]
    public bool MathEvaluator_EvaluateBoolean_ProgrammingExpression()
    {
        _count++;
        bool a = _count % 2 == 0; //randomizing values

        return "A∨¬B∧(C∨B)"
            .SetContext(_context)
            .BindVariable(a, "A")
            .BindVariable(!a, "B")
            .BindVariable(a, "C")
            .EvaluateBoolean();
    }
}

Below are the performance measurement results:

Example 3: Calculating a mathematical expression in C#.

Let's compare the performance of calculating a complex logical expression
A!= !B || A == C && false ^ -2.9 >= -12.9 + 0.1 / 0.01:

BenchmarkRunner.Run<Benchmarks>();

[SimpleJob(RuntimeMoniker.Net80)]
[SimpleJob(RuntimeMoniker.Net60)]
[MemoryDiagnoser]
public class Benchmarks
{
    private readonly DotNetStandartMathContext _context = new();

    private int _count;

    [Benchmark(Description = "MathEvaluator")]
    public bool MathEvaluator_EvaluateBoolean_ProgrammingExpression()
    {
        _count++;
        bool a = _count % 2 == 0; //randomizing values

        return "A != !B || A == C && false ^ -2.9 >= -12.9 + 0.1 / 0.01"
            .SetContext(_context)
            .BindVariable(a, "A")
            .BindVariable(!a, "B")
            .BindVariable(a, "C")
            .EvaluateBoolean();
    }

    [Benchmark(Description = "NCalc")]
    public bool NCalc_Evaluate_ProgrammingExpression()
    {
        _count++;
        bool a = _count % 2 == 0; //randomizing values

        var str = "A != !B || A == C && false ^ -2.9 >= -12.9 + 0.1 / 0.01";
        var expression = new Expression(str);
        expression.Parameters["A"] = a;
        expression.Parameters["B"] = !a;
        expression.Parameters["C"] = a;

        return Convert.ToBoolean(expression.Evaluate());
    }
}

Below are the performance comparison results:

Example 4: Calculating a user-defined logical function.

Let's look at how to extend the programming context to support additional logical functions. For example, let's create a function ifwhich takes three arguments: the first is the condition, the second specifies what to return if the condition is true, and the third specifies what to return if it is false:

var context = new ProgrammingMathContext();
context.BindFunction((c, v1, v2) => c != 0.0 ? v1 : v2, "if");

var a = 2.0;
var result = "if(3 % a = 1, true, false)"
    .SetContext(context)
    .BindVariable(a)
    .EvaluateBoolean();

Conclusion

The MathEvaluator library is a powerful and flexible tool for evaluating logical mathematical expressions in various contexts. Whether you need to process logical operations in programming, scientific calculations, or expressions in C#, MathEvaluator provides superior performance and extensibility compared to alternative solutions.

If you find this project valuable, please consider contributing. sponsor me on GitHub.

Thank you! If you have any ideas or suggestions, please leave them in the comments.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *