Skip to content

Commit

Permalink
Variables are now bound by name instead of position.
Browse files Browse the repository at this point in the history
Added new overload for passing variables as named arguments.
Added new overload for getting compiled delegate from the expression.
  • Loading branch information
Giorgi committed Apr 17, 2012
1 parent 4f1fccd commit 3a3e05e
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 20 deletions.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Math Expression Evaluator is a library for evaluating simple mathematical expressions. It supports simple expressions such as `2.5+5.9`, `17.89-2.47+7.16`, `5/2/2+1.5*3+4.58`, expressions with parentheses `(((9-6/2)*2-4)/2-6-1)/(2+24/(2+4))` and expressions with variables:

``` csharp

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))",new { a, b, c}),
Is.EqualTo((((9 - a / 2) * 2 - b) / 2 - a - 1) / (2 + c / (2 + 4))));
```
It is also possible to specify variables by using named arguments like this:

``` csharp

dynamic dynamicEngine = new ExpressionEvaluator();

var a = 6;
var b = 4.5m;
var c = 2.6m;
Assert.That(dynamicEngine.Evaluate("(c+b)*a", a: 6, b: 4.5, c: 2.6),
Is.EqualTo((c + b) * a));
```
42 changes: 36 additions & 6 deletions SimpleExpressionEvaluator.Tests/ExpressionEvaluatorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,18 +98,48 @@ 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));
Assert.That(engine.Evaluate("a", new { a }), Is.EqualTo(a));
Assert.That(engine.Evaluate("a+a", new { a }), Is.EqualTo(a + a));
Assert.That(engine.Evaluate("a+b", new { 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))));
var b = 4.5m;
var c = 2.6m;
Assert.That(engine.Evaluate("(((9-a/2)*2-b)/2-a-1)/(2+c/(2+4))", new { a, b, c }), Is.EqualTo((((9 - a / 2) * 2 - b) / 2 - a - 1) / (2 + c / (2 + 4))));
Assert.That(engine.Evaluate("(c+b)*a", new { a, b, c }), Is.EqualTo((c + b) * a));
}

[Test]
public void Can_Pass_Named_Variables()
{
dynamic dynamicEngine = new ExpressionEvaluator();

var a = 6;
var b = 4.5m;
var c = 2.6m;

Assert.That(dynamicEngine.Evaluate("(c+b)*a", a: 6, b: 4.5, c: 2.6), Is.EqualTo((c + b) * a));
}

[Test]
public void Can_Invoke_Expression_Multiple_Times()
{
var a = 6m;
var b = 3.9m;
var c = 4.9m;

var compiled = engine.Compile("(a+b)/(a+c)");
Assert.That(compiled(new { a, b, c }), Is.EqualTo((a + b) / (a + c)));

a = 5.4m;
b = -2.4m;
c = 7.5m;

Assert.That(compiled(new { a, b, c }), Is.EqualTo((a + b) / (a + c)));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,7 @@
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="ExpressionEvaluatorTests.cs" />
Expand Down
139 changes: 133 additions & 6 deletions SimpleExpressionEvaluator/ExpressionEvaluator.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,80 @@
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace SimpleExpressionEvaluator
{
public class ExpressionEvaluator
public class ExpressionEvaluator : DynamicObject
{
private readonly Stack<Expression> expressionStack = new Stack<Expression>();
private readonly Stack<char> operatorStack = new Stack<char>();
private readonly List<string> parameters = new List<string>();

public decimal Evaluate(string expression, params decimal[] arguments)

public Func<object, decimal> Compile(string expression)
{
var compiled = Parse(expression);

Func<object, decimal> result = argument =>
{
var arguments = ParseArguments(argument);
return Execute(compiled, arguments);
};

return result;
}

public decimal Evaluate(string expression, object argument = null)
{
var arguments = ParseArguments(argument);

return Evaluate(expression, arguments);
}

public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
if ("Evaluate" != binder.Name)
{
return base.TryInvokeMember(binder, args, out result);
}

if (!(args[0] is string))
{
throw new ArgumentException("No expression specified for parsing");
}

//args will contain expression and arguments,
//ArgumentNames will contain only named arguments
if (args.Length != binder.CallInfo.ArgumentNames.Count + 1)
{
throw new ArgumentException("Argument names missing.");
}

var arguments = new Dictionary<string, decimal>();

for (int i = 0; i < binder.CallInfo.ArgumentNames.Count; i++)
{
if (IsNumeric(args[i + 1].GetType()))
{
arguments.Add(binder.CallInfo.ArgumentNames[i], Convert.ToDecimal(args[i + 1]));
}
}

result = Evaluate((string)args[0], arguments);

return true;
}


private Func<decimal[], decimal> Parse(string expression)
{
if (string.IsNullOrWhiteSpace(expression))
{
return 0;
return s => 0;
}

var arrayParameter = Expression.Parameter(typeof(decimal[]), "args");
Expand Down Expand Up @@ -85,9 +144,57 @@ public decimal Evaluate(string expression, params decimal[] arguments)

var lambda = Expression.Lambda<Func<decimal[], decimal>>(expressionStack.Pop(), arrayParameter);
var compiled = lambda.Compile();
return compiled(arguments);
return compiled;
}

private Dictionary<string, decimal> ParseArguments(object argument)
{
if (argument == null)
{
return new Dictionary<string, decimal>();
}

var argumentType = argument.GetType();

var properties = argumentType.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(p => p.CanRead && IsNumeric(p.PropertyType));

var arguments = properties.ToDictionary(property => property.Name,
property => Convert.ToDecimal(property.GetValue(argument, null)));

return arguments;
}

private decimal Evaluate(string expression, Dictionary<string, decimal> arguments)
{
var compiled = Parse(expression);

return Execute(compiled, arguments);
}

private decimal Execute(Func<decimal[], decimal> compiled, Dictionary<string, decimal> arguments)
{
arguments = arguments ?? new Dictionary<string, decimal>();

if (parameters.Count != arguments.Count)
{
throw new ArgumentException(string.Format("Expression contains {0} parameters but got only {1}",
parameters.Count, arguments.Count));
}

var missingParameters = parameters.Where(p => !arguments.ContainsKey(p)).ToList();

if (missingParameters.Any())
{
throw new ArgumentException("No values provided for parameters: " + string.Join(",", missingParameters));
}

var values = parameters.Select(parameter => arguments[parameter]).ToArray();

return compiled(values);
}


private void EvaluateWhile(Func<bool> condition)
{
while (condition())
Expand All @@ -99,7 +206,6 @@ private void EvaluateWhile(Func<bool> condition)
}
}


private Expression ReadOperand(TextReader reader)
{
var operand = string.Empty;
Expand Down Expand Up @@ -156,7 +262,28 @@ private Expression ReadParameter(TextReader reader, Expression arrayParameter)
parameters.Add(parameter);
}

return Expression.ArrayAccess(arrayParameter, Expression.Constant(parameters.IndexOf(parameter)));
return Expression.ArrayIndex(arrayParameter, Expression.Constant(parameters.IndexOf(parameter)));
}


private bool IsNumeric(Type type)
{
switch (Type.GetTypeCode(type))
{
case TypeCode.SByte:
case TypeCode.Byte:
case TypeCode.Int16:
case TypeCode.UInt16:
case TypeCode.Int32:
case TypeCode.UInt32:
case TypeCode.Int64:
case TypeCode.UInt64:
case TypeCode.Single:
case TypeCode.Double:
case TypeCode.Decimal:
return true;
}
return false;
}
}

Expand Down
4 changes: 0 additions & 4 deletions SimpleExpressionEvaluator/SimpleExpressionEvaluator.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,7 @@
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="ExpressionEvaluator.cs" />
Expand Down

0 comments on commit 3a3e05e

Please sign in to comment.