Get the parameters in an expression using NCalc
Asked Answered
P

4

12

I have an expression which I want to parse to get a list of all used parameters.

For example: "X + 5 / (Y - 1)" should give me the following result: X, Y

I already use NCalc in my project; so is it possible to use NCalc to get the parameters used in an expression?

According to this one discussion entry (https://ncalc.codeplex.com/discussions/361959) it is, but I don't quite understand the answer.

Phonetician answered 11/4, 2014 at 13:9 Comment(0)
V
16

From the discussion/answer here: http://ncalc.codeplex.com/discussions/360990

A implementation that I've tested and works (for your provided sample expression) is to implement a LogicalExpressionVisitor and have it record the parameters as they are found:

class ParameterExtractionVisitor : LogicalExpressionVisitor
{
    public HashSet<string> Parameters = new HashSet<string>();

    public override void Visit(NCalc.Domain.Identifier function)
    {
        //Parameter - add to list
        Parameters.Add(function.Name);
    }

    public override void Visit(NCalc.Domain.UnaryExpression expression)
    {
        expression.Expression.Accept(this);
    }

    public override void Visit(NCalc.Domain.BinaryExpression expression)
    {
        //Visit left and right
        expression.LeftExpression.Accept(this);
        expression.RightExpression.Accept(this);
    }

    public override void Visit(NCalc.Domain.TernaryExpression expression)
    {
        //Visit left, right and middle
        expression.LeftExpression.Accept(this);
        expression.RightExpression.Accept(this);
        expression.MiddleExpression.Accept(this);
    }

    public override void Visit(Function function)
    {
        foreach (var expression in function.Expressions)
        {
            expression.Accept(this);
        }
    }

    public override void Visit(LogicalExpression expression)
    {

    }

    public override void Visit(ValueExpression expression)
    {

    }
}

Then you would use it as:

var expression = NCalc.Expression.Compile("2 * [x] ^ 2 + 5 * [y]", false);

ParameterExtractionVisitor visitor = new ParameterExtractionVisitor();
expression.Accept(visitor);

var extractedParameters = visitor.Parameters;

foreach (var param in extractedParameters)
    Console.WriteLine(param);

This outputs "x" and "y" for me.

Note the use of HashSet in the ParameterExtractionVisitor. This is because if your expression contains the same variable more than once (for example: "[x] + [x]") it will be added twice. If you want to store an entry each time the same variable is used, replace the HashSet with a List.


That all said, I have very little experience with NCalc, so my implementation of the overridden methods of LogicalExpressionVisitor are guesses. When I overrode the void Visit(ValueExpression expression) method with expression.Accept(this), it resulted in a StackOverflowException. So I simply left the implementation blank and it seemed to work. So I would suggest that you take my answer here with a very large grain of salt. Your mileage may vary and I can't say if this works for all types of expressions.

Vivica answered 11/4, 2014 at 13:38 Comment(4)
For Visit(Function function) you need remove function.Accept(this) and add foreach(var expression in function.Expressions) expression.Accept(this);Conflux
There is a bug in ParameterExtractionVisitor, it should be expression.Expression.accept(this);Antiar
This will cause stack overflow (lol) with this formula: round((X + Y) * 0.05, 2)Heronry
should be: public override void Visit(Function function) { if (function.Expressions != null) { foreach (var expression in function.Expressions) { expression.Accept(this); } } }Heronry
U
3

This works for me. Your mileage may vary.

   public List<string> GetParameters(string expression) {
       List<string> parameters = new List<string>();
       Random random = new Random();
       NCalc.Expression e = new NCalc.Expression(expression);

       e.EvaluateFunction += delegate(string name, NCalc.FunctionArgs args) {
           args.EvaluateParameters();
           args.Result = random.Next(0, 100);
       };
       e.EvaluateParameter += delegate(string name, NCalc.ParameterArgs args) {
           parameters.Add(name);
           args.Result = random.Next(0, 100);
       };
       try {
           e.Evaluate();
           }
       catch {
            }
       return parameters;
    }

ref: https://ncalc.codeplex.com/discussions/79258#editor

Utopian answered 2/10, 2014 at 20:43 Comment(3)
This will likely return the name of functions rather than parameters.Flats
@EhsanWaris well, I just tried it and it does get a list of all the parameters just fine. I changed the try/catch to a try/finally instead and desubscribed the methods (using local functions). I also changed from List to HashSet so it does not return duplicate parameters and removed the random (mine returns 1 instead). So it seems to work as advertised.Getupandgo
I wonder if the evaluation can choke in some expressions depending on the value replacing the parameter. I tried 10 / (val - 1), while having val replaced with val and there was no trouble. Any idea of a special expression that can choke?Getupandgo
S
0

Here is another approach I use:

I built a NCalc extension method that allows to process parameters and functions on the fly.

internal static class NCalcExtensions
{
    public static object Evaluate(this Expression exp, EvaluateParameterHandler evaluateParameters = null, EvaluateFunctionHandler evaluateFunctions = null)
    {
        try
        {
            if (evaluateParameters != null)
                exp.EvaluateParameter += evaluateParameters;

            if (evaluateFunctions != null)
                exp.EvaluateFunction += evaluateFunctions;

            return exp.Evaluate();
        }
        finally
        {
            exp.EvaluateParameter -= evaluateParameters;
            exp.EvaluateFunction -= evaluateFunctions;
        }
    }
}

Among other things, I can use it to run a dummy evaluation to get parameters and functions names.

var paramNames = new List<string>();
var functionNames = new List<string>();

expression.Evaluate(
    new EvaluateParameterHandler((s, a) =>
    {
        paramNames.Add(s);
        a.Result = 1; // dummy value
    }),
    new EvaluateFunctionHandler((s, a) =>
    {
        functionNames.Add(s);
        a.Result = 1; // dummy value
    }));
Sidon answered 5/8, 2014 at 15:3 Comment(1)
By the way, I wonder why the designer implemented EvaluateParameter and EvaluateFunction as events and not delegate.Sidon
M
0

Based on Chris Sinclairs answer, now is very popular NCalcAsync nuget package. In that case, you can have something like this:

class ParameterExtractionVisitor : LogicalExpressionVisitor
{
    public HashSet<string> Parameters = new();

    public override Task VisitAsync(Identifier function)
    {
        //Parameter - add to list
        Parameters.Add(function.Name);
        return Task.CompletedTask;
    }

    public override async Task VisitAsync(UnaryExpression expression)
    {
        await expression.Expression.AcceptAsync(this);
    }

    public override async Task VisitAsync(BinaryExpression expression)
    {
        //Visit left and right
        await expression.LeftExpression.AcceptAsync(this);
        await expression.RightExpression.AcceptAsync(this);
    }

    public override async Task VisitAsync(TernaryExpression expression)
    {
        //Visit left, right and middle
        await expression.LeftExpression.AcceptAsync(this);
        await expression.RightExpression.AcceptAsync(this);
        await expression.MiddleExpression.AcceptAsync(this);
    }

    public override async Task VisitAsync(Function function)
    {
        foreach (var expression in function.Expressions)
        {
            await expression.AcceptAsync(this);
        }
    }

    public override Task VisitAsync(LogicalExpression expression)
    {
        return Task.CompletedTask;
    }

    public override Task VisitAsync(ValueExpression expression)
    {
        return Task.CompletedTask;
    }
}
Maximo answered 12/4, 2022 at 7:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.