Introduction
In part two of this series, we built an expression evaluator capable of parsing expressions with parentheses. In this part, we are going to add support for expression with variables.
Adding Support for Single Variable to Expression Evaluator
The simplest case is to make the following test pass:
[Test]
public void Can_Process_Simple_Variables()
{
decimal a = 2.6m;
Assert.That(engine.Evaluate("a", a), Is.EqualTo(a));
}
First of all, we need to add a second parameter to Evaluate
method. To keep the existing tests green, we will also specify a default value for it. Running the test causes Evaluate
method to throw an exception as it does not recognize variables. To avoid exception during parsing the expression when we encounter a letter, we will read the variable name and as variables are similar to numeric operands, store them on expression stack. To represent the variables in expression tree, we are going to use Expression.Parameter Method which is a factory method for creating instances of ParameterExpression
class. The method for parsing variables looks like this:
private ParameterExpression ReadParameter(TextReader reader)
{
var operand = string.Empty;
int peek;
while ((peek = reader.Peek()) > -1)
{
var next = (char)peek;
if (char.IsLetter(next))
{
reader.Read();
operand += next;
}
else
{
break;
}
}
var parameter = Expression.Parameter(typeof(decimal), operand);
return parameter;
}
This change solves the issue of parsing variables but running the test causes an InvalidOperationException
as we are not passing any parameters to Expression.Lambda
method. To fix this issue, we need to keep track of all variables that we encounter while parsing the expression and pass them to Expression.Lambda method. As we need to pass the value of the parameter to the compiled expression tree, we also have to change type of the delegate.
The Evaluate
method now looks like this:
public decimal Evaluate(string expression, decimal variable1 = 0)
{
if (string.IsNullOrWhiteSpace(expression))
{
return 0;
}
operatorStack.Clear();
expressionStack.Clear();
using (var reader = new StringReader(expression))
{
int peek;
while ((peek = reader.Peek()) > -1)
{
var next = (char)peek;
if (char.IsDigit(next))
{
expressionStack.Push(ReadOperand(reader));
continue;
}
if (char.IsLetter(next))
{
var parameterExpression = ReadParameter(reader);
parameters.Add(parameterExpression);
expressionStack.Push(parameterExpression);
continue;
}
if (Operation.IsDefined(next))
{
var currentOperation = ReadOperation(reader);
EvaluateWhile(() => operatorStack.Count > 0
&& operatorStack.Peek() != '(' &&
currentOperation.Precedence <= ((Operation)operatorStack.Peek()).Precedence);
operatorStack.Push(next);
continue;
}
if (next == '(')
{
reader.Read();
operatorStack.Push('(');
continue;
}
if (next == ')')
{
reader.Read();
EvaluateWhile(() => operatorStack.Count > 0
&& operatorStack.Peek() != '(');
operatorStack.Pop();
continue;
}
if (next != ' ')
{
throw new ArgumentException(string.Format
("Encountered invalid character {0}", next), "expression");
}
}
}
EvaluateWhile(() => operatorStack.Count > 0);
var compiled = Expression.Lambda<Func<decimal,
decimal>>(expressionStack.Pop(), parameters).Compile();
return compiled(variable1);
}
The new test now passes but we have successfully broken the previous test when we do not pass any variable. To make all tests pass again, we will need to build the delegate based on number of parameters and pass corresponding variable if there is one. The simplest solution is to build the appropriate delegate based on number of parameters like this:
if (parameters.Count > 0)
{
var compiled = Expression.Lambda<Func<decimal,
decimal>>(expressionStack.Pop(), parameters).Compile();
return compiled(variable1);
}
else
{
var compiled = Expression.Lambda<Func<decimal>>
(expressionStack.Pop()).Compile();
return compiled();
}
This works but it only supports one parameter and this approach cannot be used when we do not know how many variables will be in the expression. Also, the Func
delegate supports only 16 parameters and it would be very ugly to use it.
Adding Support for Multiple Variables
A better solution is to build an expression tree that accepts an array of variables. This way, when there is no parameter, we will simply pass an empty array and when there are several parameters, we will simply pass them. The delegate signature will be the same in all situations so we will not need any if else
statements to handle different scenarios.
To help us with the implementation of the new approach, we are going to add these tests to our test suite:
[Test]
public void Can_Process_Simple_Variables()
{
decimal a = 2.6m;
decimal b = 5.7m;
Assert.That(engine.Evaluate("a", a), Is.EqualTo(a));
Assert.That(engine.Evaluate("a+a", a), Is.EqualTo(a + a));
Assert.That(engine.Evaluate("a+b", a, b), Is.EqualTo(a + b));
}
[Test]
public void Can_Process_Multiple_Variables()
{
var a = 6;
var b = 4.32m;
var c = 24.15m;
Assert.That(engine.Evaluate("(((9-a/2)*2-b)/2-a-1)/(2+c/(2+4))", a, b, c),
Is.EqualTo((((9 - a / 2) * 2 - b) / 2 - a - 1) / (2 + c / (2 + 4))));
}
The expression tree that we are going to build will have one array parameter. To access individual members of this array in the expression, we will use Expression.ArrayIndex Method We will also change the Evaluate
method to accept variable number of parameters by params
keyword. Let’s look at the modified code:
public decimal Evaluate(string expression, params decimal[] arguments)
{
if (string.IsNullOrWhiteSpace(expression))
{
return 0;
}
operatorStack.Clear();
expressionStack.Clear();
var arrayParameter = Expression.Parameter(typeof(decimal[]), "args");
using (var reader = new StringReader(expression))
{
int peek;
while ((peek = reader.Peek()) > -1)
{
var next = (char)peek;
if (char.IsDigit(next))
{
expressionStack.Push(ReadOperand(reader));
continue;
}
if (char.IsLetter(next))
{
var parameterExpression = ReadParameter(reader, arrayParameter);
expressionStack.Push(parameterExpression);
continue;
}
if (Operation.IsDefined(next))
{
var currentOperation = ReadOperation(reader);
EvaluateWhile(() => operatorStack.Count > 0
&& operatorStack.Peek() != '(' &&
currentOperation.Precedence <= ((Operation)operatorStack.Peek()).Precedence);
operatorStack.Push(next);
continue;
}
if (next == '(')
{
reader.Read();
operatorStack.Push('(');
continue;
}
if (next == ')')
{
reader.Read();
EvaluateWhile(() => operatorStack.Count > 0
&& operatorStack.Peek() != '(');
operatorStack.Pop();
continue;
}
if (next != ' ')
{
throw new ArgumentException(string.Format
("Encountered invalid character {0}", next),
"expression");
}
}
}
EvaluateWhile(() => operatorStack.Count > 0);
var compiled = Expression.Lambda<Func<decimal[], decimal>>(expressionStack.Pop(), arrayParameter).Compile();
return compiled(arguments);
}
You will notice that there are several changes from the previous version: There is only one parameter in the expression tree we are building, we are passing this parameter to the ReadParameter
method and we are generating the same type of delegate for all cases.
The only remaining change is needed in ReadParameter
method so that it returns the correct item from the parameter array when we encounter a variable. We also need to make sure that we return the same item when the same variable is used again the expression. For this, we will need to keep track of the variables and return the corresponding item from the ReadParameter
method. Here is how it works:
private Expression ReadParameter(TextReader reader, Expression arrayParameter)
{
var parameter = string.Empty;
int peek;
while ((peek = reader.Peek()) > -1)
{
var next = (char)peek;
if (char.IsLetter(next))
{
reader.Read();
parameter += next;
}
else
{
break;
}
}
if (!parameters.Contains(parameter))
{
parameters.Add(parameter);
}
return Expression.ArrayIndex
(arrayParameter, Expression.Constant(parameters.IndexOf(parameter)));
}
As you can see, whenever we encounter a variable, we add it to parameter collection if it is not already present and then build an array index expression using the corresponding index.
Using expression tree visualizer, we can see how the expression tree that we build using the expression evaluator for last test case looks like:
Conclusion
Our new implementation of expression evaluator now supports expressions that contain numbers as well as variables. In the next post, we will add several overloads to make passing parameters easier and add a new overload for better performance when we need to evaluate the same expression with different parameter values. The full source code will be made available on github in a couple of days.