Skip to content

Commit

Permalink
First implementation of Shunting yard algorithm
Browse files Browse the repository at this point in the history
  • Loading branch information
Giorgi committed Feb 27, 2012
1 parent 2b68a62 commit 1117957
Show file tree
Hide file tree
Showing 12 changed files with 11,221 additions and 4 deletions.
86 changes: 86 additions & 0 deletions SimpleExpressionEvaluator.Tests/ExpressionEvaluatorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using NUnit.Framework;

namespace SimpleExpressionEvaluator.Tests
{
public class ExpressionEvaluatorTests
{
private ExpressionEvaluator engine;
private Random generator;

[TestFixtureSetUp]
public void SetUp()
{
engine = new ExpressionEvaluator();
generator = new Random();
}

[Test]
public void Empty_String_Is_Zero()
{
Assert.That(engine.Evaluate(""), Is.EqualTo(0));
}

[Test]
public void Decimal_Is_Treated_As_Decimal()
{
var left = generator.Next(1, 100);

Assert.That(engine.Evaluate(left.ToString()), Is.EqualTo(left));
}

[Test]
public void Two_Plus_Two_Is_Four()
{
Assert.That(engine.Evaluate("2+2"), Is.EqualTo(4));
}

[Test]
public void Can_Add_Two_Decimal_Numbers()
{
Assert.That(engine.Evaluate("2.7+3.2"), Is.EqualTo(2.7m + 3.2m));
}

[Test]
public void Can_Add_Many_Numbers()
{
Assert.That(engine.Evaluate("1.2+3.4+5.6+7.8"), Is.EqualTo(1.2m + 3.4m + 5.6m + 7.8m));
Assert.That(engine.Evaluate("1.7+2.9+14.24+6.58"), Is.EqualTo(1.7m + 2.9m + 14.24m + 6.58m));
}

[Test]
public void Can_Subtract_Two_Numbers()
{
Assert.That(engine.Evaluate("5-2"), Is.EqualTo(5 - 2));
}

[Test]
public void Can_Subtract_Multiple_Numbers()
{
Assert.That(engine.Evaluate("15.2-2.3-4.8-0.58"), Is.EqualTo(15.2m - 2.3m - 4.8m - 0.58m));
}

[Test]
public void Can_Add_And_Subtract_Multiple_Numbers()
{
Assert.That(engine.Evaluate("15+8-4-2+7"), Is.EqualTo(15 + 8 - 4 - 2 + 7));
Assert.That(engine.Evaluate("17.89-2.47+7.16"), Is.EqualTo(17.89m - 2.47m + 7.16m));

}

[Test]
public void Can_Add_Subtract_Multiply_Divide_Multiple_Numbers()
{
Assert.That(engine.Evaluate("50-5*3*2+7"), Is.EqualTo(50 - 5 * 3 * 2 + 7));
Assert.That(engine.Evaluate("84+15+4-4*3*9+24+4-54/3-5-7+47"), Is.EqualTo(84 + 15 + 4 - 4 * 3 * 9 + 24 + 4 - 54 / 3 - 5 - 7 + 47));
Assert.That(engine.Evaluate("50-48/4/3+7*2*4+2+5+8"), Is.EqualTo(50 - 48 / 4 / 3 + 7 * 2 * 4 + 2 + 5 + 8));
Assert.That(engine.Evaluate("5/2/2+1.5*3+4.58"), Is.EqualTo(5 / 2m / 2m + 1.5m * 3m + 4.58m));
Assert.That(engine.Evaluate("25/3+1.34*2.56+1.49+2.36/1.48"), Is.EqualTo(25 / 3m + 1.34m * 2.56m + 1.49m + 2.36m / 1.48m));
Assert.That(engine.Evaluate("2*3+5-4-2*5+7"), Is.EqualTo(2 * 3 + 5 - 4 - 2 * 5 + 7));
}
}
}
36 changes: 36 additions & 0 deletions SimpleExpressionEvaluator.Tests/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("SimpleExpressionEvaluator.Tests")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("SimpleExpressionEvaluator.Tests")]
[assembly: AssemblyCopyright("Copyright © 2012")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]

// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]

// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("95807a00-691e-4cb4-be31-acb06cacf350")]

// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http:https://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProductVersion>8.0.30703</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{08F4484E-5FD8-4590-A8D7-12FBE47120C8}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>SimpleExpressionEvaluator.Tests</RootNamespace>
<AssemblyName>SimpleExpressionEvaluator.Tests</AssemblyName>
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="nunit.framework">
<HintPath>..\packages\NUnit.2.6.0.12054\lib\nunit.framework.dll</HintPath>
</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" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SimpleExpressionEvaluator\SimpleExpressionEvaluator.csproj">
<Project>{30637214-EDE9-4C2E-BFD6-E4B163FA308B}</Project>
<Name>SimpleExpressionEvaluator</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>
4 changes: 4 additions & 0 deletions SimpleExpressionEvaluator.Tests/packages.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="NUnit" version="2.6.0.12054" />
</packages>
6 changes: 6 additions & 0 deletions SimpleExpressionEvaluator.sln
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ Microsoft Visual Studio Solution File, Format Version 11.00
# Visual Studio 2010
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleExpressionEvaluator", "SimpleExpressionEvaluator\SimpleExpressionEvaluator.csproj", "{30637214-EDE9-4C2E-BFD6-E4B163FA308B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleExpressionEvaluator.Tests", "SimpleExpressionEvaluator.Tests\SimpleExpressionEvaluator.Tests.csproj", "{08F4484E-5FD8-4590-A8D7-12FBE47120C8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -13,6 +15,10 @@ Global
{30637214-EDE9-4C2E-BFD6-E4B163FA308B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{30637214-EDE9-4C2E-BFD6-E4B163FA308B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{30637214-EDE9-4C2E-BFD6-E4B163FA308B}.Release|Any CPU.Build.0 = Release|Any CPU
{08F4484E-5FD8-4590-A8D7-12FBE47120C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{08F4484E-5FD8-4590-A8D7-12FBE47120C8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{08F4484E-5FD8-4590-A8D7-12FBE47120C8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{08F4484E-5FD8-4590-A8D7-12FBE47120C8}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
161 changes: 158 additions & 3 deletions SimpleExpressionEvaluator/ExpressionEvaluator.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,166 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Linq.Expressions;

namespace SimpleExpressionEvaluator
{
public class ExpressionEvaluator
{
public decimal Evaluate(string expression)
{
if (string.IsNullOrWhiteSpace(expression))
{
return 0;
}

var expressionStack = new Stack<Expression>();
var operatorStack = new Stack<char>();

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 (Operation.IsDefined(next))
{
var currentOperation = ReadOperation(reader);

while (true)
{
if (operatorStack.Count == 0)
{
operatorStack.Push(next);
break;
}

var lastOperition = operatorStack.Peek();

if (currentOperation.Precedence > ((Operation)lastOperition).Precedence)
{
operatorStack.Push(next);
break;
}

var right = expressionStack.Pop();
var left = expressionStack.Pop();

expressionStack.Push(((Operation)operatorStack.Pop()).Apply(left, right));
}
continue;
}

if (next != ' ')
{
throw new ArgumentException("Invalid character encountered", "expression");
}
}
}

while (operatorStack.Count > 0)
{
var right = expressionStack.Pop();
var left = expressionStack.Pop();

expressionStack.Push(((Operation)operatorStack.Pop()).Apply(left, right));
}

var compiled = Expression.Lambda<Func<decimal>>(expressionStack.Pop()).Compile();
return compiled();
}

private Expression ReadOperand(StringReader reader)
{
var operand = string.Empty;

int peek;

while ((peek = reader.Peek()) > -1)
{
var next = (char)peek;

if (char.IsDigit(next) || next == '.')
{
reader.Read();
operand += next;
}
else
{
break;
}
}

return Expression.Constant(string.IsNullOrEmpty(operand) ? decimal.Zero : decimal.Parse(operand));
}

private Operation ReadOperation(TextReader reader)
{
var operation = (char)reader.Read();
return (Operation)operation;
}
}

internal sealed class Operation
{
private readonly int precedence;
private readonly string name;
private readonly Func<Expression, Expression, Expression> operation;

public static readonly Operation Addition = new Operation(1, Expression.Add, "Addition");
public static readonly Operation Subtraction = new Operation(1, Expression.Subtract, "Subtraction");
public static readonly Operation Multiplication = new Operation(2, Expression.Multiply, "Multiplication");
public static readonly Operation Division = new Operation(2, Expression.Divide, "Division");

private static readonly Dictionary<char, Operation> Operations = new Dictionary<char, Operation>
{
{ '+', Addition },
{ '-', Subtraction },
{ '*', Multiplication},
{ '/', Division }
};

private Operation(int precedence, Func<Expression, Expression, Expression> operation, string name)
{
this.precedence = precedence;
this.operation = operation;
this.name = name;
}

public int Precedence
{
get { return precedence; }
}

public static explicit operator Operation(char operation)
{
Operation result;

if (Operations.TryGetValue(operation, out result))
{
return result;
}
else
{
throw new InvalidCastException();
}
}

public Expression Apply(Expression left, Expression right)
{
return operation(left, right);
}

public static bool IsDefined(char operation)
{
return Operations.ContainsKey(operation);
}
}
}
}
2 changes: 1 addition & 1 deletion SimpleExpressionEvaluator/SimpleExpressionEvaluator.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Class1.cs" />
<Compile Include="ExpressionEvaluator.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
Expand Down
Binary file not shown.
Binary file not shown.
Loading

0 comments on commit 1117957

Please sign in to comment.