Getting list of variables in an expression...

Dec 29, 2009 at 9:46 AM
Edited Dec 29, 2009 at 10:44 AM

Hi,

In my application, I get data for a large number of variables (e.g. a1,a2 .. a100). I need to compute an expression based on some of these variables (e.g. a1+a34+a49). This expression is defined by the user at application runtime.

Is there a way that I can find out which variables are being used in the expression, so that I can add only those parameters in the expression through Parameters collection. I am looking for something like the following pseudo code:

 

public object Evaluate(string sFormula, ValueCollection values)
{
   Expression expression = new Expression(sFormula);
   
   // get list of variables used in the expression
string[] variables = expression.GetVariablesList(); for(int i = 0; i < variables.Length; i++) { string sParamName = variables[i];
// Get current value of variable
object oValue = ValueCollection[sParamName];
// add variable value to the expression
expression.Parameters[sParamName] = oValue; } return expression.Evaluate(); }

 

Is there something equivalent of GetVariablesList that could get me the list of variables being used in an expression?

I know I can do this by blindly adding all the variables to expression evaluator, but obviously there could be performance hit.


Thanks & Regards,

- Vikram Lele

Dec 29, 2009 at 11:38 AM
Edited Dec 29, 2009 at 11:44 AM

Hi,

I could not find any readymade function in NCalc to do this. So I have written quick and dirty recursive function to do this:

private void ExtractIdentifiers(LogicalExpression expression, List<string> identifiers)
{
if (expression is UnaryExpression)
 {
 UnaryExpression ue = expression as UnaryExpression;

ExtractIdentifiers(ue.Expression, identifiers);
}
else if (expression is BinaryExpression)
{
BinaryExpression be = expression as BinaryExpression;

ExtractIdentifiers(be.LeftExpression, identifiers);
ExtractIdentifiers(be.RightExpression, identifiers);
}
else if (expression is TernaryExpression)
{
TernaryExpression te = expression as TernaryExpression;

ExtractIdentifiers(te.LeftExpression, identifiers);
ExtractIdentifiers(te.MiddleExpression, identifiers);
ExtractIdentifiers(te.RightExpression, identifiers);
}
else if (expression is Function)
{
Function fn = expression as Function;

LogicalExpression[] expressions = fn.Expressions;
if (expressions != null && expressions.Length > 0)
{
for (int i = 0; i < expressions.Length; i++)
{
ExtractIdentifiers(expressions[i], identifiers);
}
}
}
else if (expression is Identifier)
{
Identifier identifier = expression as Identifier;

if (!identifiers.Contains(identifier.Name))
{
identifiers.Add(identifier.Name);
}
}
}

You must first call HasErrors to ensure that the expression gets parsed before this function could be used.
Here is how I use it :

 

Expression expression = new Expression(sExpression, EvaluateOptions.IgnoreCase);

List<string> identifiers = new List<string>();
bool bHasErrors = expression.HasErrors();
if (!bHasErrors)
{
   ExtractIdentifiers(expression.ParsedExpression, identifiers);
}


 

Now I have a few more queries:

a. Is there any better way of doing this?

b. Does this cover all scenarios?

c. Can this feature be made part of NCalc library?

If this feature could be part of NCalc library, then better approach could be to add a property to LogicalExpression to get all identifiers used by that expression. This could recursively operate to get the list of identifiers without resorting to the type casting approach that I have used here.

 

Regards,

- Vikram Lele

 

 

 

        private void ExtractIdentifiers(LogicalExpression expression, List<string> identifiers)
        {
            if (expression is UnaryExpression)
            {
                UnaryExpression ue = expression as UnaryExpression;
                ExtractIdentifiers(ue.Expression, identifiers);
            }
            else if (expression is BinaryExpression)
            {
                BinaryExpression be = expression as BinaryExpression;
                ExtractIdentifiers(be.LeftExpression, identifiers);
                ExtractIdentifiers(be.RightExpression, identifiers);
            }
            else if (expression is TernaryExpression)
            {
                TernaryExpression te = expression as TernaryExpression;
                ExtractIdentifiers(te.LeftExpression, identifiers);
                ExtractIdentifiers(te.MiddleExpression, identifiers);
                ExtractIdentifiers(te.RightExpression, identifiers);
            }
            else if (expression is Function)
            {
                Function fn = expression as Function;
                LogicalExpression[] expressions = fn.Expressions;
                if (expressions != null && expressions.Length > 0)
                {
                    for (int i = 0; i < expressions.Length; i++)
                    {
                        ExtractIdentifiers(expressions[i], identifiers);
                    }
                }
            }
            else if (expression is Identifier)
            {
                Identifier identifier = expression as Identifier;
                if (!identifiers.Contains(identifier.Name))
                {
                    identifiers.Add(identifier.Name);
                }
            }
        }

Coordinator
Dec 29, 2009 at 6:42 PM

I would have done it by implementing a specific visitor for that, implementing each of the Visit(Expression) overload.

Optionally, you can also not define any parameter, and define a delegate for the parameters. Then you can only give the requested value on demand. To see if it's a performance optimization you should make some tests.

Nov 30, 2011 at 11:17 PM

How about using the parameter delegate like this:

        public List<string> GetParameters(string expression)
        {
            List<string> parameters = new List<string>();

            Random random = new Random();

            Expression e = new Expression(expression);

            e.EvaluateParameter += delegate(string name, ParameterArgs args)
            {
                parameters.Add(name);
                args.Result = random.Next(0, 100);
            };

            try
            {
                e.Evaluate();
            }
            catch
            {
            }

            return parameters;
        }

Oct 2, 2014 at 9:36 PM
This did not work for me, but when I added an EvaluateFunction delegate, it worked like a charm. Thanks for the lead.
            e.EvaluateFunction += delegate(string name, NCalc.FunctionArgs args) {
                args.EvaluateParameters();
                args.Result = random.Next(0, 100);
            };
Nov 2, 2015 at 10:18 PM
Be careful, I don't think using EvaluateParameter and EvaluateFunction delegates to record the name will work reliably for a Logical/Binary expression since the expression evaluator uses short circuit evaluation. So if you have and expression like: "(A > 10) && (B < 5)" and if you give it a dummy "A" value that's > 10, then it won't evaluate the "(B < 5)" and the EvaluateParameter call won't be made for "(B < 5)" so you won't get the "B" parameter. In this situation I think "viklele" is the right approach (not sure if it covers all cases).