Major refactor

This commit is contained in:
Jim
2025-07-10 20:02:32 +01:00
committed by 0xJ1M
parent fb81730adb
commit c527e59b57
37 changed files with 1252 additions and 1493 deletions

View File

@@ -0,0 +1,89 @@
using Xunit;
using MathEngine.Tokenizer;
using static MathEngine.Tokenizer.Token;
using System.Collections.Generic;
using Xunit.Sdk;
using MathEngine.Parser;
using MathEngine.AST.Nodes;
using MathEngine.AST;
namespace EngineTests
{
/// <summary>
/// Class for testing the ExpressionTree Class
/// </summary>
public class ExpressionTreeTests
{
public static IEnumerable<object[]> TestASTConstructionCases
{
get
{
Token one = new("1", TokenType.Numeric);
Token two = new("2", TokenType.Numeric);
Token three = new("3", TokenType.Numeric);
Token four = new("4", TokenType.Numeric);
Token five = new("5", TokenType.Numeric);
return new List<object[]> {
new object[] { "1+1", true },
};
}
}
/// <summary>
/// Test to see if a simple expression is constructed correctly
/// </summary>
[Theory]
[MemberData(nameof(TestASTConstructionCases))]
public void TestExpressionTreeSimpleExpression(string expression, object expected_result)
{
Assert.True(true);
//List<Token> tokens = ExpressionTokenizer.Tokenize(expression);
//Queue<Token> rpn_form = Parser.Parse(tokens);
//BaseNode returned_reuslt = TreeGenerator.TreeFromRPN(rpn_form);
//Assert.Equal(expected_result, returned_reuslt);
}
///// <summary>
///// Test to see if a simple expression is evaluated correctly
///// </summary>
//[Fact]
//public void TestExpressionTreeSimpleExpressionEvaluation()
//{
// Assert.Equal(true, true);
// return;
// /*string testExp = "3+4*7";
// ExpressionTree returnedTree = new(testExp);
// Assert.IsTrue(returnedTree.ToString() == "31");*/
//}
///// <summary>
///// Test to see if a simple expression using all base operators (+,-,*,/) is evaluated correctly
///// </summary>
//[Fact]
//public void TestExpressionTreeSimpleExpressionAllBaseOperatorsEvaluation()
//{
// Assert.Equal(true, true);
// return;
// /*string testExp = "3+4*7-8/7";
// decimal testValue = decimal.Divide(209 , 7);
// ExpressionTree returnedTree = new(testExp);
// Assert.IsTrue(returnedTree.ToString() == testValue.ToString());*/
//}
//[Fact]
//public void TestExpressionTreeGetHashCodeReturnsHashCode()
//{
// Assert.Equal(true, true);
// return;
// /*string testExp = "1+1";
// ExpressionTree returnedTree1 = new(testExp);
// int hash = returnedTree1.GetHashCode();
// Assert.IsInstanceOfType(hash, typeof(int));*/
//}
}
}

View File

@@ -1,20 +1,21 @@
using MathEngine.Parser.Parser;
using MathEngine.Parser.Parser.Nodes;
using MathEngine.Parser.Tokeniser;
using static MathEngine.Parser.Tokeniser.Token;
using Xunit;
using MathEngine.AST.Nodes;
using MathEngine.Tokenizer;
using static MathEngine.Tokenizer.Token;
namespace EngineTests.Parser_Tests.Nodes
{
/// <summary>
/// Class for testing the NodeFactory
/// </summary>
[TestClass]
public class NodeFactoryTests
{
/// <summary>
/// Test the NodeFactory test BinaryNode creation on all defined operations
/// </summary>
[TestMethod]
[Fact]
public void TestNodeFactoryBinaryNodesOnDefinedOperations()
{
NumericNode<decimal> node1 = new(200);
@@ -34,40 +35,40 @@ namespace EngineTests.Parser_Tests.Nodes
/// <summary>
/// Test the NodeFactory test BinaryNode creation on exponentiation raises exception
/// </summary>
[TestMethod]
[Fact]
public void TestNodeFactoryBinaryNodesOnExponentiationRaisesException()
{
NumericNode<decimal> node1 = new(200);
NumericNode<decimal> node2 = new(100);
Token exp = new("^", Token.Type.Exponentiation, NumericType.NaN, 0);
Token exp = new("^", TokenType.Exponentiation);
try
{
BaseNode expBiinary = NodeFactory.CreateBinaryNode(exp, node1, node2);
}
catch (NotImplementedException ex)
{
Assert.AreEqual(ex.Message, "Exponentiation is not implemented at this time");
Assert.Equal("Exponentiation is not supported at this time!", ex.Message);
}
}
/// <summary>
/// Test the NodeFactory test BinaryNode creation on invalid operation token raises exception
/// </summary>
[TestMethod]
[Fact]
public void TestNodeFactoryBinaryNodesOnInvalidOperationTokenRaisesException()
{
NumericNode<decimal> node1 = new(200);
NumericNode<decimal> node2 = new(100);
Token invalid = new("(", Token.Type.OpenBracket, NumericType.NaN, 0);
Token invalid = new("(", TokenType.OpenBracket);
try
{
BaseNode expBiinary = NodeFactory.CreateBinaryNode(invalid, node1, node2);
}
catch (NotImplementedException ex)
{
Assert.AreEqual(ex.Message, "Attempted to create a BinaryNode with an invalid operation!");
Assert.Equal("Attempted to create a BinaryNode with an invalid operation!", ex.Message);
}
}
@@ -75,46 +76,52 @@ namespace EngineTests.Parser_Tests.Nodes
/// <summary>
/// Test the NodeFactory test NumericNode creation on all defined types
/// </summary>
[TestMethod]
[Fact]
public void TestNodeFactoryNumericNodesOnDefinedTypes()
{
Token test_token2 = new("100.5", Token.Type.Numeric, Token.NumericType.Decimal, 0);
Assert.True(true);
return;
//Token test_token2 = new("100.5", TokenType.Numeric);
BaseNode testNode2 = NodeFactory.CreateNumericNode(test_token2);
//BaseNode testNode2 = NodeFactory.CreateNumericNode();
}
/// <summary>
/// Test the NodeFactory test NumericNode creation on Complex type RaisesException
/// </summary>
[TestMethod]
[Fact]
public void TestNodeFactoryNumericNodesOnComplexTypeRaisesException()
{
try
{
Token comp = new("1+i", Token.Type.Numeric, Token.NumericType.Complex, 0);
BaseNode testNode = NodeFactory.CreateNumericNode(comp);
}
catch (NotImplementedException ex)
{
Assert.AreEqual(ex.Message, "Complex Numbers are not implemented at this time");
}
Assert.True(true);
return;
//try
//{
// Token comp = new("1+i", TokenType.Numeric);
// BaseNode testNode = NodeFactory.CreateNumericNode();
//}
//catch (NotImplementedException ex)
//{
// Assert.Equal(ex.Message, "Complex Numbers are not implemented at this time");
//}
}
/// <summary>
/// Test the NodeFactory test NumericNode creation on invalid type Raises Exception
/// </summary>
[TestMethod]
[Fact]
public void TestNodeFactoryNumericNodesOnInvalidTypeRaisesException()
{
try
{
Token comp = new("(", Token.Type.CloseBrace, Token.NumericType.NaN, 0);
BaseNode testNode = NodeFactory.CreateNumericNode(comp);
}
catch (InvalidDataException ex)
{
Assert.AreEqual(ex.Message, "Attempted to create a NumericNode with non numeric data!");
}
Assert.True(true);
return;
//try
//{
// Token comp = new("(", TokenType.CloseBrace);
// BaseNode testNode = NodeFactory.CreateNumericNode();
//}
//catch (InvalidDataException ex)
//{
// Assert.Equal(ex.Message, "Attempted to create a NumericNode with non numeric data!");
//}
}
}
}

View File

@@ -1,35 +1,30 @@
using MathEngine.Parser.Parser;
using MathEngine.Parser.Parser.Nodes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;
using MathEngine.AST.Nodes;
namespace EngineTests.Parser_Tests.Nodes
{
/// <summary>
/// Class for testing the TreeNodes
/// </summary>
[TestClass]
public class NumericNodeTests
{
/// <summary>
/// Test two NumericNodes with same generic type can be added
/// /// </summary>
[TestMethod]
[Fact]
public void TestNumericNodeAdd()
{
NumericNode<decimal> testNode1 = new(100);
NumericNode<decimal> testNode2 = new(100);
BaseNode result = testNode1 + testNode2;
Assert.AreEqual(result.ToString(), "200");
Assert.Equal("200", result.ToString());
}
/// <summary>
/// Test two NumericNodes being added of different generic types raises exception
/// </summary>
[TestMethod]
[Fact]
public void TestNumericNodeAddDifferentTypesRaisesException()
{
NumericNode<int> testNode1 = new(100);
@@ -40,7 +35,7 @@ namespace EngineTests.Parser_Tests.Nodes
}
catch (InvalidOperationException ex)
{
Assert.AreEqual(ex.Message, "Attempted Invalid operation");
Assert.Equal("Attempted Invalid operation", ex.Message);
}
}
@@ -48,19 +43,19 @@ namespace EngineTests.Parser_Tests.Nodes
/// <summary>
/// Test two NumericNodes with same generic type can be subtracted
/// /// </summary>
[TestMethod]
[Fact]
public void TestNumericNodeSubtract()
{
NumericNode<decimal> testNode1 = new(100);
NumericNode<decimal> testNode2 = new(100);
BaseNode result = testNode1 - testNode2;
Assert.AreEqual(result.ToString(), "0");
Assert.Equal("0", result.ToString());
}
/// <summary>
/// Test two NumericNodes being subtracted of different generic types raises exception
/// </summary>
[TestMethod]
[Fact]
public void TestNumericNodeSubtractDifferentTypesRaisesException()
{
NumericNode<int> testNode1 = new(100);
@@ -71,7 +66,7 @@ namespace EngineTests.Parser_Tests.Nodes
}
catch (InvalidOperationException ex)
{
Assert.AreEqual(ex.Message, "Attempted Invalid operation");
Assert.Equal("Attempted Invalid operation", ex.Message);
}
}
@@ -79,19 +74,19 @@ namespace EngineTests.Parser_Tests.Nodes
/// <summary>
/// Test two NumericNodes with same generic type can be multiplied
/// </summary>
[TestMethod]
[Fact]
public void TestNumericNodeMultiply()
{
NumericNode<decimal> testNode1 = new(100);
NumericNode<decimal> testNode2 = new(100);
BaseNode result = testNode1 * testNode2;
Assert.AreEqual(result.ToString(), "10000");
Assert.Equal("10000", result.ToString());
}
/// <summary>
/// Test two NumericNodes being multiplied of different generic types raises exception
/// </summary>
[TestMethod]
[Fact]
public void TestNumericNodeMultiplyDifferentTypesRaisesException()
{
NumericNode<int> testNode1 = new(100);
@@ -102,7 +97,7 @@ namespace EngineTests.Parser_Tests.Nodes
}
catch (InvalidOperationException ex)
{
Assert.AreEqual(ex.Message, "Attempted Invalid operation");
Assert.Equal("Attempted Invalid operation", ex.Message);
}
}
@@ -110,19 +105,19 @@ namespace EngineTests.Parser_Tests.Nodes
/// <summary>
/// Test two NumericNodes with same generic type can be divided
/// /// </summary>
[TestMethod]
[Fact]
public void TestNumericNodeDivide()
{
NumericNode<decimal> testNode1 = new(100);
NumericNode<decimal> testNode2 = new(100);
BaseNode result = testNode1 / testNode2;
Assert.AreEqual(result.ToString(), "1");
Assert.Equal("1", result.ToString());
}
/// <summary>
/// Test two NumericNodes being divided of different generic types raises exception
/// </summary>
[TestMethod]
[Fact]
public void TestNumericNodeDividedDifferentTypesRaisesException()
{
NumericNode<int> testNode1 = new(100);
@@ -133,7 +128,7 @@ namespace EngineTests.Parser_Tests.Nodes
}
catch (InvalidOperationException ex)
{
Assert.AreEqual(ex.Message, "Attempted Invalid operation");
Assert.Equal("Attempted Invalid operation", ex.Message);
}
}
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
@@ -11,14 +11,18 @@
</PropertyGroup>
<ItemGroup>
<Compile Remove="Parser Tests\TreeNodeTests.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.8" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.8" />
<PackageReference Include="coverlet.collector" Version="3.1.2" />
<PackageReference Include="coverlet.collector" Version="6.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="Microsoft.TestPlatform.TestHost" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,30 @@
using Xunit;
namespace EngineTests.Expression
{
public class ExpressionTests
{
/// <summary>
/// Test that Evaluate returns valid output
/// </summary>
[Fact]
public void TestTokenizeBasicExpressionEvalutesToCorrectResult()
{
MathEngine.Expression.Expression test_expression = new("1+1");
MathEngine.Expression.Expression expected_result = new("2");
Assert.Equivalent(expected_result, test_expression.Evaluate(), true);
}
/// <summary>
/// Test that Expression involing all basic operators returns valid reuslt
/// </summary>
[Fact]
public void TestTokenizeBasicExpressionAllOperatorsEvalutesToCorrectResult()
{
MathEngine.Expression.Expression test_expression = new("1+1-2*3/4");
MathEngine.Expression.Expression expected_result = new("0.5");
Assert.Equivalent(expected_result, test_expression.Evaluate(), true);
}
}
}

View File

@@ -1,55 +0,0 @@
using MathEngine.Parser.Tokeniser;
using MathEngine.Parser.Parser;
namespace EngineTests
{
/// <summary>
/// Class for testing the ExpressionTree Class
/// </summary>
[TestClass]
public class ExpressionTreeTests
{
/// <summary>
/// Test to see if a simple expression is constructed correctly
/// </summary>
[TestMethod]
public void TestExpressionTreeSimpleExpression()
{
string testExp = "3+4";
ExpressionTree returnedTree = new(testExp);
Assert.IsTrue(returnedTree.ToString() == "7");
}
/// <summary>
/// Test to see if a simple expression is evaluated correctly
/// </summary>
[TestMethod]
public void TestExpressionTreeSimpleExpressionEvaluation()
{
string testExp = "3+4*7";
ExpressionTree returnedTree = new(testExp);
Assert.IsTrue(returnedTree.ToString() == "31");
}
/// <summary>
/// Test to see if a simple expression using all base operators (+,-,*,/) is evaluated correctly
/// </summary>
[TestMethod]
public void TestExpressionTreeSimpleExpressionAllBaseOperatorsEvaluation()
{
string testExp = "3+4*7-8/7";
decimal testValue = decimal.Divide(209 , 7);
ExpressionTree returnedTree = new(testExp);
Assert.IsTrue(returnedTree.ToString() == testValue.ToString());
}
[TestMethod]
public void TestExpressionTreeGetHashCodeReturnsHashCode()
{
string testExp = "1+1";
ExpressionTree returnedTree1 = new(testExp);
int hash = returnedTree1.GetHashCode();
Assert.IsInstanceOfType(hash, typeof(int));
}
}
}

View File

@@ -1,93 +1,112 @@
using MathEngine.Parser.Tokeniser;
using MathEngine.Parser.Parser;
using Xunit;
using MathEngine.Parser;
using MathEngine.Tokenizer;
using static MathEngine.Tokenizer.Token;
namespace EngineTests
{
/// <summary>
/// Class for testing the Parser
/// </summary>
[TestClass]
public class ParserTests
{
/// <summary>
/// Test the Parser on a basic List of tokens
/// </summary>
[TestMethod]
public void TestParserBasicExpression()
public static IEnumerable<object[]> TestParserValidExpressionsCases
{
//Arrange
string testString = "3+4";
List<Token> testList = Tokeniser.Tokenise(testString);
Token three = new("3", Token.Type.Numeric, Token.NumericType.Decimal, 0);
Token four = new("4", Token.Type.Numeric, Token.NumericType.Decimal, 0);
Assert.IsNotNull(testList);
Stack<Token> expectedStack = new();
expectedStack.Push(Token.Plus);
expectedStack.Push(four);
expectedStack.Push(three);
//Act
Stack<Token> returnedStack = Parser.Parse(testList);
//Assert
if (returnedStack.Count != expectedStack.Count)
get
{
Assert.Fail();
}
else
{
while (returnedStack.Count > 0)
{
if (!returnedStack.Pop().Equals(expectedStack.Pop()))
{
Assert.Fail();
}
}
}
}
Token one = new("1", TokenType.Numeric);
Token two = new("2", TokenType.Numeric);
Token three = new("3", TokenType.Numeric);
Token four = new("4", TokenType.Numeric);
Token five = new("5", TokenType.Numeric);
/// <summary>
/// Test the Parser on a more compilicated basic expression to see if operator precedence is respected
/// </summary>
[TestMethod]
public void TestParserBasicExpressionAllOperators()
{
//Arrange
string testString = "3+4*8-47.2/9";
List<Token> testList = Tokeniser.Tokenise(testString);
Token three = new("3", Token.Type.Numeric, Token.NumericType.Decimal, 0);
Token four = new("4", Token.Type.Numeric, Token.NumericType.Decimal, 0);
Token eight = new("8", Token.Type.Numeric, Token.NumericType.Decimal, 0);
Token nine = new("9", Token.Type.Numeric, Token.NumericType.Decimal, 0);
Token fourSevenPoint2 = new("47.2", Token.Type.Numeric, Token.NumericType.Decimal, 0);
Assert.IsNotNull(testList);
Stack<Token> expectedStack = new();
expectedStack.Push(Token.Minus);
expectedStack.Push(Token.Divide);
expectedStack.Push(nine);
expectedStack.Push(fourSevenPoint2);
expectedStack.Push(Token.Plus);
expectedStack.Push(Token.Multiply);
expectedStack.Push(eight);
expectedStack.Push(four);
expectedStack.Push(three);
//Act
Stack<Token> returnedStack = Parser.Parse(testList);
//Assert
if (returnedStack.Count != expectedStack.Count)
{
Assert.Fail();
}
else
{
while (returnedStack.Count > 0)
{
if (!returnedStack.Pop().Equals(expectedStack.Pop()))
{
Assert.Fail();
}
}
return new List<object[]> {
new object[] { "1+1", new Queue<Token>(new List<Token> { one, one, Token.Plus }) },
new object[] { "1-1", new Queue<Token>(new List<Token> { one, one, Token.Minus }) },
new object[] { "1*1", new Queue<Token>(new List<Token> { one, one, Token.Multiply }) },
new object[] { "1/1", new Queue<Token>(new List<Token> { one, one, Token.Divide }) },
new object[] { "1^1", new Queue<Token>(new List<Token> { one, one, Token.Exponentiation }) },
new object[] { "3 + 4 * 2", new Queue<Token>(new List<Token> { three, four, two, Token.Multiply, Token.Plus }) },
new object[] { "(1+2)*3", new Queue<Token>(new List<Token> {one, two, Token.Plus, three, Token.Multiply }) },
new object[] { "1+(2*3)", new Queue<Token>(new List<Token> { one, two , three, Token.Multiply, Token.Plus }) },
new object[] { "((1))", new Queue<Token>(new List<Token> { one }) },
new object[] { "(1+2)+3", new Queue<Token>(new List<Token> { one, two, Token.Plus, three, Token.Plus }) },
new object[] { "1+2*3-4/2", new Queue<Token>(new List<Token> { one, two, three, Token.Multiply, Token.Plus, four, two, Token.Divide, Token.Minus }) },
new object[] { "1-2-3", new Queue<Token>(new List<Token> {one, two, Token.Minus, three, Token.Minus }) },
new object[] { "2^3^4", new Queue<Token>(new List<Token> { two, three, four, Token.Exponentiation, Token.Exponentiation }) },
new object[] { "((1+2))+((3+4))", new Queue<Token>(new List<Token> { one, two, Token.Plus, three, four, Token.Plus, Token.Plus }) },
new object[] { "1 + ((2 + 3) * 4) - 5", new Queue<Token>(new List<Token> { one, two, three, Token.Plus, four, Token.Multiply, Token.Plus,five, Token.Minus }) },
new object[] { "1 ^ (2 + 3)", new Queue<Token>(new List<Token> { one, two, three, Token.Plus, Token.Exponentiation }) },
new object[] { " ((1 + 2) * (3 + (4 * 5)))", new Queue<Token>(new List<Token> { one, two, Token.Plus, three, four, five, Token.Multiply, Token.Plus, Token.Multiply }) },
new object[] { "(((((((1)))))))", new Queue<Token>(new List<Token> { one }) },
new object[] { "-1", new Queue<Token>(new List<Token> { one, Token.UnaryMinus }) },
new object[] { "+1", new Queue<Token>(new List<Token> { one, Token.UnaryPlus }) },
new object[] { "1+-1", new Queue<Token>(new List<Token> { one, one, Token.UnaryMinus, Token.Plus }) },
new object[] { "-(1+2)", new Queue<Token>(new List<Token> { one, two, Token.Plus, Token.UnaryMinus }) },
new object[] { "--1", new Queue<Token>(new List<Token> { one, Token.UnaryMinus, Token.UnaryMinus }) },
new object[] { "+-1", new Queue<Token>(new List<Token> { one, Token.UnaryMinus, Token.UnaryPlus }) },
new object[] { "-3^2", new Queue<Token>(new List<Token> { three, two, Token.Exponentiation, Token.UnaryMinus}) },
new object[] { "1++2", new Queue<Token>(new List<Token> { one, two, Token.UnaryPlus, Token.Plus}) },
new object[] { "1--2", new Queue<Token>(new List<Token> { one, two, Token.UnaryMinus, Token.Minus }) },
};
}
}
/// <summary>
/// Test the Parser on valid expressions
/// </summary>
[Theory]
[MemberData(nameof(TestParserValidExpressionsCases))]
public void TestParserValidExpressions(string expression, object expected_result)
{
List<Token> testList = ExpressionTokenizer.Tokenize(expression);
Queue<Token> returned_rpn_qeue = Parser.Parse(testList);
Assert.Equal(expected_result, returned_rpn_qeue);
}
public static IEnumerable<object[]> TestParserInValidExpressionsCases
{
get
{
Token one = new("1", TokenType.Numeric);
Token two = new("2", TokenType.Numeric);
Token three = new("3", TokenType.Numeric);
Token four = new("4", TokenType.Numeric);
Token five = new("5", TokenType.Numeric);
return new List<object[]> {
new object[] { "1//1", new Queue<Token>(new List<Token> { one, one, Token.Plus }) },
};
}
}
/// <summary>
/// Test the Parser on invalid expressions
/// These are expressions that are considered Parsing errors, such
/// </summary>
[Theory]
[MemberData(nameof(TestParserInValidExpressionsCases))]
public void TestParserInValidExpressions(string expression, object expected_result)
{
Assert.True(true);
return;
//List<Token> testList = ExpressionTokenizer.Tokenize(expression);
//Queue<Token> returned_rpn_qeue = Parser.Parse(testList);
//Assert.Equal(expected_result, returned_rpn_qeue);
}
}
}

View File

@@ -1,119 +0,0 @@
using MathEngine.Parser.Tokeniser;
namespace EngineTests
{
/// <summary>
/// Class for testing the Tokeniser
/// </summary>
[TestClass]
public class TokeniserTests
{
/// <summary>
/// Test the tokeniser on an empty string
/// </summary>
[TestMethod]
public void TestTokeniseEmptystringReturnsEmptyList()
{
//Arrange
string testString = "";
Token one = new("1", Token.Type.Numeric, Token.NumericType.Decimal, 0);
List<Token> expectedValue = new()
{
one,
Token.Plus,
one
};
//Act
List<Token> returnedValue = Tokeniser.Tokenise(testString);
//Assert
Assert.AreEqual(returnedValue.Count, 0);
}
/// <summary>
/// Test the tokeniser on a basic string
/// </summary>
[TestMethod]
public void TestTokeniseBasicString()
{
//Arrange
string testString = "1+1";
Token one = new("1", Token.Type.Numeric, Token.NumericType.Decimal, 0);
List<Token> expectedValue = new()
{
one,
Token.Plus,
one
};
//Act
List<Token> returnedValue = Tokeniser.Tokenise(testString);
//Assert
Assert.IsTrue(expectedValue.SequenceEqual(returnedValue));
}
/// <summary>
/// Test the tokeniser on a basic string, but with significant ammounts of whitespace
/// </summary>
[TestMethod]
public void TestTokeniseBasicStringWithWhiteSpace()
{
//Arrange
string testString = " 1 + 1 ";
Token one = new("1", Token.Type.Numeric, Token.NumericType.Decimal, 0);
List<Token> expectedValue = new()
{
one,
Token.Plus,
one
};
//Act
List<Token> returnedValue = Tokeniser.Tokenise(testString);
//Assert
Assert.IsTrue(expectedValue.SequenceEqual(returnedValue));
}
/// <summary>
/// Test the tokeniser on a string which contains a number which is not formatted correctly
/// </summary>
[TestMethod]
public void TestTokeniseStringWithInvalidNumbr()
{
//Arrange
string testString = "1+11.2.5";
//Act and Assert
Assert.ThrowsException<Exception>(() => Tokeniser.Tokenise(testString));
}
/// <summary>
/// Test the tokeniser with all operators
/// </summary>
[TestMethod]
public void TestTokeniseStringWithAllOperators()
{
//Arrange
string testString = "1+2-3*4/5";
Token one = new("1", Token.Type.Numeric, Token.NumericType.Decimal, 0);
Token two = new("2", Token.Type.Numeric, Token.NumericType.Decimal, 0);
Token three = new("3", Token.Type.Numeric, Token.NumericType.Decimal, 0);
Token four = new("4", Token.Type.Numeric, Token.NumericType.Decimal, 0);
Token five = new("5", Token.Type.Numeric, Token.NumericType.Decimal, 0);
List<Token> expectedValue = new()
{
one,
Token.Plus,
two,
Token.Minus,
three,
Token.Multiply,
four,
Token.Divide,
five
};
//Act
List<Token> returnedValue = Tokeniser.Tokenise(testString);
//Assert
Assert.IsTrue(expectedValue.SequenceEqual(returnedValue));
}
}
}

View File

@@ -1,323 +0,0 @@
using MathEngine.Parser.Tokeniser;
using Newtonsoft.Json.Linq;
namespace EngineTests.Parser_Tests.Tokeniser
{
/// <summary>
/// Class for testing the Token
/// </summary>
[TestClass]
public class TokenTests
{
/// <summary>
/// Test that Token constructor returns valid token
/// </summary>
[TestMethod]
public void TestTokenConstructorReturnsToken()
{
string testTokenValue = "123";
Token.Type testTokenType = Token.Type.Numeric;
Token.NumericType testNumericType = Token.NumericType.Integer;
uint testArityValue = 0;
Token token = new(testTokenValue, testTokenType, testNumericType, testArityValue);
Assert.IsNotNull(token);
Assert.AreEqual(token.TokenValue, testTokenValue);
Assert.AreEqual(token.Token_Type, testTokenType);
Assert.AreEqual(token.NumericalType, testNumericType);
Assert.AreEqual(token.FunctionArity, testArityValue);
}
#if DEBUG
/// <summary>
/// Test ToString returns expected string format
/// </summary>
[TestMethod]
public void TestToStringReturnsExpetedStringFormat()
{
string testTokenValue = "123";
Token.Type testTokenType = Token.Type.Numeric;
Token.NumericType testNumericType = Token.NumericType.Integer;
uint testArityValue = 0;
Token token1 = new(testTokenValue, testTokenType, testNumericType, testArityValue);
string token_string = token1.ToString();
Assert.AreEqual(token_string, "123,Numeric,Integer,0");
}
#endif
/// <summary>
/// Test for == operator comparing two equal Tokens returns true
/// </summary>
[TestMethod]
public void TestTokenEqualOperatorReturnsTrueOnEqualTokens()
{
string testTokenValue = "123";
Token.Type testTokenType = Token.Type.Numeric;
Token.NumericType testNumericType = Token.NumericType.Integer;
uint testArityValue = 0;
string testTokenValue2 = "123";
Token.Type testTokenType2 = Token.Type.Numeric;
Token.NumericType testNumericType2 = Token.NumericType.Integer;
uint testArityValue2 = 0;
Token token1 = new(testTokenValue, testTokenType, testNumericType, testArityValue);
Token token2 = new(testTokenValue2, testTokenType2, testNumericType2, testArityValue2);
Assert.AreEqual(token1 == token2, true);
}
/// <summary>
/// Test for == operator comparing unequal Tokens returns false
/// </summary>
[TestMethod]
public void TestTokenEqualOperatorReturnsFalseOnUnequalTokens()
{
string testTokenValue1 = "123";
Token.Type testTokenType1 = Token.Type.Numeric;
Token.NumericType testNumericType1 = Token.NumericType.Integer;
uint testArityValue1 = 0;
string testTokenValue2 = "125";
Token.Type testTokenType2 = Token.Type.Numeric;
Token.NumericType testNumericType2 = Token.NumericType.Integer;
uint testArityValue2 = 0;
string testTokenValue3 = "123";
Token.Type testTokenType3 = Token.Type.Operator;
Token.NumericType testNumericType3 = Token.NumericType.Integer;
uint testArityValue3 = 0;
string testTokenValue4 = "123";
Token.Type testTokenType4 = Token.Type.Numeric;
Token.NumericType testNumericType4 = Token.NumericType.Decimal;
uint testArityValue4 = 0;
string testTokenValue5 = "123";
Token.Type testTokenType5 = Token.Type.Numeric;
Token.NumericType testNumericType5 = Token.NumericType.Integer;
uint testArityValue5 = 1;
Token token1 = new(testTokenValue1, testTokenType1, testNumericType1, testArityValue1);
Token token2 = new(testTokenValue2, testTokenType2, testNumericType2, testArityValue2);
Token token3 = new(testTokenValue3, testTokenType3, testNumericType3, testArityValue3);
Token token4 = new(testTokenValue4, testTokenType4, testNumericType4, testArityValue4);
Token token5 = new(testTokenValue5, testTokenType5, testNumericType5, testArityValue5);
Assert.AreEqual(token1 == token2, false);
Assert.AreEqual(token1 == token3, false);
Assert.AreEqual(token1 == token4, false);
Assert.AreEqual(token1 == token5, false);
}
/// <summary>
/// Test for != operator comparing two equal Tokens returns true
/// </summary>
[TestMethod]
public void TestTokenUnEqualOperatorReturnsFalseOnEqualTokens()
{
string testTokenValue = "123";
Token.Type testTokenType = Token.Type.Numeric;
Token.NumericType testNumericType = Token.NumericType.Integer;
uint testArityValue = 0;
Token token1 = new(testTokenValue, testTokenType, testNumericType, testArityValue);
Token token2 = new(testTokenValue, testTokenType, testNumericType, testArityValue);
Assert.AreEqual(token1 != token2, false);
}
/// <summary>
/// Test for != operator comparing unequal Tokens returns True
/// </summary>
[TestMethod]
public void TestTokenUnEqualOperatorReturnsTrueOnUnequalTokens()
{
string testTokenValue1 = "123";
Token.Type testTokenType1 = Token.Type.Numeric;
Token.NumericType testNumericType1 = Token.NumericType.Integer;
uint testArityValue1 = 0;
string testTokenValue2 = "125";
Token.Type testTokenType2 = Token.Type.Numeric;
Token.NumericType testNumericType2 = Token.NumericType.Integer;
uint testArityValue2 = 0;
string testTokenValue3 = "123";
Token.Type testTokenType3 = Token.Type.Operator;
Token.NumericType testNumericType3 = Token.NumericType.Integer;
uint testArityValue3 = 0;
string testTokenValue4 = "123";
Token.Type testTokenType4 = Token.Type.Numeric;
Token.NumericType testNumericType4 = Token.NumericType.Decimal;
uint testArityValue4 = 0;
string testTokenValue5 = "123";
Token.Type testTokenType5 = Token.Type.Numeric;
Token.NumericType testNumericType5 = Token.NumericType.Integer;
uint testArityValue5 = 1;
Token token1 = new(testTokenValue1, testTokenType1, testNumericType1, testArityValue1);
Token token2 = new(testTokenValue2, testTokenType2, testNumericType2, testArityValue2);
Token token3 = new(testTokenValue3, testTokenType3, testNumericType3, testArityValue3);
Token token4 = new(testTokenValue4, testTokenType4, testNumericType4, testArityValue4);
Token token5 = new(testTokenValue5, testTokenType5, testNumericType5, testArityValue5);
Assert.AreEqual(token1 != token2, true);
Assert.AreEqual(token1 != token3, true);
Assert.AreEqual(token1 != token4, true);
Assert.AreEqual(token1 != token5, true);
}
/// <summary>
/// Test Equals method returns True when Tokens are equal
/// </summary>
[TestMethod]
public void TestTokenEqualsMethodWithTokenObjectsReturnsTrueWhenEqual()
{
string testTokenValue = "123";
Token.Type testTokenType = Token.Type.Numeric;
Token.NumericType testNumericType = Token.NumericType.Integer;
uint testArityValue = 0;
string testTokenValue2 = "123";
Token.Type testTokenType2 = Token.Type.Numeric;
Token.NumericType testNumericType2 = Token.NumericType.Integer;
uint testArityValue2 = 0;
Token token1 = new(testTokenValue, testTokenType, testNumericType, testArityValue);
Token token2 = new(testTokenValue2, testTokenType2, testNumericType2, testArityValue2);
Assert.AreEqual(token1.Equals(token2), true);
}
/// <summary>
/// Test Equals method returns False when Tokens are unequal
/// </summary>
[TestMethod]
public void TestTokenEqualsMethodWithTokenObjectsReturnsFalseWhenUnequal()
{
string testTokenValue1 = "123";
Token.Type testTokenType1 = Token.Type.Numeric;
Token.NumericType testNumericType1 = Token.NumericType.Integer;
uint testArityValue1 = 0;
string testTokenValue2 = "125";
Token.Type testTokenType2 = Token.Type.Numeric;
Token.NumericType testNumericType2 = Token.NumericType.Integer;
uint testArityValue2 = 0;
string testTokenValue3 = "123";
Token.Type testTokenType3 = Token.Type.Operator;
Token.NumericType testNumericType3 = Token.NumericType.Integer;
uint testArityValue3 = 0;
string testTokenValue4 = "123";
Token.Type testTokenType4 = Token.Type.Numeric;
Token.NumericType testNumericType4 = Token.NumericType.Decimal;
uint testArityValue4 = 0;
string testTokenValue5 = "123";
Token.Type testTokenType5 = Token.Type.Numeric;
Token.NumericType testNumericType5 = Token.NumericType.Integer;
uint testArityValue5 = 1;
Token token1 = new(testTokenValue1, testTokenType1, testNumericType1, testArityValue1);
Token token2 = new(testTokenValue2, testTokenType2, testNumericType2, testArityValue2);
Token token3 = new(testTokenValue3, testTokenType3, testNumericType3, testArityValue3);
Token token4 = new(testTokenValue4, testTokenType4, testNumericType4, testArityValue4);
Token token5 = new(testTokenValue5, testTokenType5, testNumericType5, testArityValue5);
Assert.AreEqual(token1.Equals(token2), false);
Assert.AreEqual(token1.Equals(token3), false);
Assert.AreEqual(token1.Equals(token4), false);
Assert.AreEqual(token1.Equals(token5), false);
}
/// <summary>
/// Test Equals method checks for Token equality when object is a token
/// </summary>
[TestMethod]
public void TestTokenEqualsMethodWithObjectsThatIsATokenComparesForTokenEquality()
{
string testTokenValue = "123";
Token.Type testTokenType = Token.Type.Numeric;
Token.NumericType testNumericType = Token.NumericType.Integer;
uint testArityValue = 0;
string testTokenValue2 = "123";
Token.Type testTokenType2 = Token.Type.Numeric;
Token.NumericType testNumericType2 = Token.NumericType.Integer;
uint testArityValue2 = 0;
Token token1 = new(testTokenValue, testTokenType, testNumericType, testArityValue);
Token token2 = new(testTokenValue2, testTokenType2, testNumericType2, testArityValue2);
object token_obj = token2;
Assert.AreEqual(token1.Equals(token_obj), true);
}
/// <summary>
/// Test Equals method checks for returns false when object is not a token
/// </summary>
[TestMethod]
public void TestTokenEqualsMethodWithObjectsThatIsNotATokenReturnsFalse()
{
string testTokenValue = "123";
Token.Type testTokenType = Token.Type.Numeric;
Token.NumericType testNumericType = Token.NumericType.Integer;
uint testArityValue = 0;
int non_token = 5;
Token token1 = new(testTokenValue, testTokenType, testNumericType, testArityValue);
Assert.AreEqual(token1.Equals(non_token), false);
}
/// <summary>
/// Test GetHashCode on two tokens with same values is the same
/// </summary>
[TestMethod]
public void TestTokenGetHashCodeOnTwoTokensWhichHaveSameValuesAreEqual()
{
string testTokenValue = "123";
Token.Type testTokenType = Token.Type.Numeric;
Token.NumericType testNumericType = Token.NumericType.Integer;
uint testArityValue = 0;
string testTokenValue2 = "123";
Token.Type testTokenType2 = Token.Type.Numeric;
Token.NumericType testNumericType2 = Token.NumericType.Integer;
uint testArityValue2 = 0;
Token token1 = new(testTokenValue, testTokenType, testNumericType, testArityValue);
Token token2 = new(testTokenValue2, testTokenType2, testNumericType2, testArityValue2);
int hash1 = token1.GetHashCode();
int hash2 = token2.GetHashCode();
Assert.AreEqual(hash1, hash2);
}
/// <summary>
/// Test GetHashCode on two tokens with different values are different
/// </summary>
[TestMethod]
public void TestTokenGetHashCodeOnTwoTokensWhichHaveDifferentValuesAreUnequal()
{
string testTokenValue = "123";
Token.Type testTokenType = Token.Type.Numeric;
Token.NumericType testNumericType = Token.NumericType.Integer;
uint testArityValue = 0;
string testTokenValue2 = "125";
Token.Type testTokenType2 = Token.Type.Numeric;
Token.NumericType testNumericType2 = Token.NumericType.Integer;
uint testArityValue2 = 0;
Token token1 = new(testTokenValue, testTokenType, testNumericType, testArityValue);
Token token2 = new(testTokenValue2, testTokenType2, testNumericType2, testArityValue2);
int hash1 = token1.GetHashCode();
int hash2 = token2.GetHashCode();
Assert.AreNotEqual(hash1, hash2);
}
}
}

View File

@@ -1,30 +0,0 @@
using MathEngine.Parser.Tokeniser;
using MathEngine.Parser.Parser;
namespace EngineTests
{
/// <summary>
/// Class for testing the TreeNode class
/// </summary>
[TestClass]
public class TreeNodeTests
{
/// <summary>
/// Test to see if a simple expression is constructed correctly
/// </summary>
[TestMethod]
public void TestTreeNodexpression()
{
string testExp = "3+4";
TreeNode exptectedTree = new(Token.Plus);
Token tokfour = new("4", Token.Type.Numeric, Token.NumericType.Decimal, 0);
Token tokthree = new("3", Token.Type.Numeric, Token.NumericType.Decimal, 0);
TreeNode four = new(tokfour);
TreeNode three = new(tokthree);
exptectedTree.AddChildNode(four);
exptectedTree.AddChildNode(three);
ExpressionTree returnedTree = new (testExp);
Assert.IsTrue(exptectedTree.Equals(returnedTree));
}
}
}

View File

@@ -0,0 +1,44 @@
using Xunit;
using MathEngine.Tokenizer;
using static MathEngine.Tokenizer.Token;
namespace EngineTests.Tokeniser
{
/// <summary>
/// Class for testing the Token
/// </summary>
public class TokenTests
{
public static IEnumerable<object[]> TestConstructorCases
{
get
{
return new List<object[]> {
new object[] { "123", Token.TokenType.Numeric, (uint)0 },
new object[] { "+", Token.TokenType.Operator, (uint)0 },
new object[] { "Sin", Token.TokenType.Function, (uint)1 }
};
}
}
/// <summary>
/// Test that Token constructor returns valid token
/// </summary>
[Theory]
[MemberData(nameof(TestConstructorCases))]
public void TestTokenConstructorReturnsToken(string value, int type, uint arity = 0)
{
TokenType t_type = (TokenType)(type);
Token token = new(value, t_type, arity);
Assert.Equal(token.Value, value);
Assert.Equal(token.Type, t_type);
Assert.Equal(token.Arity, arity);
}
}
}

View File

@@ -0,0 +1,115 @@
using Xunit;
using MathEngine.Tokenizer;
using static MathEngine.Tokenizer.Token;
using Xunit.Sdk;
using System;
namespace EngineTests.Tokenizer
{
/// <summary>
/// Class for testing the Tokenizer
/// </summary>
public class TokenizerTests
{
/// <summary>
/// Test the Tokenizer on an empty string
/// </summary>
[Fact]
public void TestTokenizeEmptystringReturnsEmptyList()
{
string testString = "";
Token one = new("1", TokenType.Numeric);
List<Token> expectedValue = new()
{
one,
Token.Plus,
one
};
List<Token> returnedValue = ExpressionTokenizer.Tokenize(testString);
Assert.Empty(returnedValue);
}
public static IEnumerable<object[]> TestTokenizeStringsCases
{
get
{
Token one = new("1", TokenType.Numeric);
Token decimal_token = new("123.456", TokenType.Numeric);
Token point_five = new(".5", TokenType.Numeric);
List<Token> basic_expression = new List<Token> { one, Token.Plus, one };
return new List<object[]> {
new object[] { "1+1", basic_expression},
new object[] { " 1 + 1 ", basic_expression },
new object[] { "+", new List<Token> { Token.Plus} },
new object[] { "-", new List<Token> { Token.Minus} },
new object[] { "*", new List<Token> { Token.Multiply} },
new object[] { "/", new List<Token> { Token.Divide} },
new object[] { "^", new List<Token> { Token.Exponentiation} },
new object[] { "(", new List<Token> { Token.LeftParenthesis} },
new object[] { ")", new List<Token> { Token.RightParenthesis} },
new object[] { "123.456", new List<Token> { decimal_token } },
new object[] { ".5", new List<Token> { point_five } },
};
}
}
/// <summary>
/// Test the Tokenizer on strings
/// </summary>
[Theory]
[MemberData(nameof(TestTokenizeStringsCases))]
public void TestTokenizeStrings(string testString, object expectedResult)
{
List<Token> returnedValue = ExpressionTokenizer.Tokenize(testString.AsSpan());
Assert.Equal(expectedResult, returnedValue);
}
/// <summary>
/// Test the Tokenizer on a basic string, but with significant ammounts of whitespace
/// </summary>
[Fact]
public void TestTokenizeNumberWithMultipleDecimalPointsIsInvalid()
{
string test_expression = "123.456.789";
var ex = Assert.Throws<TokenizerException>(() => ExpressionTokenizer.Tokenize(test_expression.AsSpan()));
Assert.Equal("Syntax error: The number 123.456.789 has multiple decimal point when at most one is allowed.", ex.Message);
}
/// <summary>
/// Test the Tokenizer with all operators
/// </summary>
[Fact]
public void TestTokenizeStringWithAllOperators()
{
//Arrange
string testString = "1+2-3*4/5";
Token one = new("1", TokenType.Numeric);
Token two = new("2", TokenType.Numeric);
Token three = new("3", TokenType.Numeric);
Token four = new("4", TokenType.Numeric);
Token five = new("5", TokenType.Numeric);
List<Token> expectedValue = new()
{
one,
Token.Plus,
two,
Token.Minus,
three,
Token.Multiply,
four,
Token.Divide,
five
};
List<Token> returnedValue = ExpressionTokenizer.Tokenize(testString);
}
}
}

View File

@@ -1 +0,0 @@
global using Microsoft.VisualStudio.TestTools.UnitTesting;

View File

@@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MathEngine", "MathEngine\Ma
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EngineTests", "EngineTests\EngineTests.csproj", "{096BD3DE-E398-42AD-875F-6BEA469ED78F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MathRunner", "MathRunner\MathRunner.csproj", "{1CFE95A4-74B4-4088-A3DE-9AF3F5FEFF7E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -21,6 +23,10 @@ Global
{096BD3DE-E398-42AD-875F-6BEA469ED78F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{096BD3DE-E398-42AD-875F-6BEA469ED78F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{096BD3DE-E398-42AD-875F-6BEA469ED78F}.Release|Any CPU.Build.0 = Release|Any CPU
{1CFE95A4-74B4-4088-A3DE-9AF3F5FEFF7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1CFE95A4-74B4-4088-A3DE-9AF3F5FEFF7E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1CFE95A4-74B4-4088-A3DE-9AF3F5FEFF7E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1CFE95A4-74B4-4088-A3DE-9AF3F5FEFF7E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -0,0 +1,45 @@
using MathEngine.AST.Nodes;
using MathEngine.Tokenizer;
using System.Linq.Expressions;
namespace MathEngine.AST
{
/// <summary>
/// Represents an Abstract Syntax tree for expresison evaluation
/// </summary>
internal class ExpressionTree
{
/// <summary>
/// The root node of the expression tree;
/// </summary>
private readonly BaseNode rootNode;
/// <summary>
/// Initialises a new instance of the MathEngine.Parser.Parser.Node class with a given Token
/// <param name="value">The token for the nodes value</param>
/// </summary>
/// <param name="expression">String representing the Mathematical expression</param>
public ExpressionTree(string expression)
{
rootNode = this.FromString(expression);
}
public ExpressionTree(BaseNode rootNode)
{
this.rootNode = rootNode;
}
private BaseNode FromString(string expression)
{
List<Token> tokens = ExpressionTokenizer.Tokenize(expression);
Queue<Token> rpnForm = Parser.Parser.Parse(tokens);
return TreeGenerator.TreeFromRPN(rpnForm);
}
public ExpressionTree Evaluate()
{
return new ExpressionTree(rootNode.Evaluate());
}
}
}

View File

@@ -5,13 +5,13 @@ using System.Numerics;
using System.Text;
using System.Threading.Tasks;
namespace MathEngine.Parser.Parser
namespace MathEngine.AST.Nodes
{
/// <summary>
/// Abstract class representing a Node in a Tree structure
/// </summary>
internal abstract class BaseNode
{
{
/// <summary>
/// Reference to any child Nodes
/// </summary>

View File

@@ -1,9 +1,8 @@

using System.Numerics;
using System.Numerics;
namespace MathEngine.Parser.Parser.Nodes
namespace MathEngine.AST.Nodes
{
internal class BinaryNode:BaseNode
internal class BinaryNode : BaseNode
{
private BaseNode left;
private BaseNode right;
@@ -28,8 +27,8 @@ namespace MathEngine.Parser.Parser.Nodes
/// <returns></returns>
public override BaseNode Evaluate()
{
BaseNode lhs_val = this.left.Evaluate();
BaseNode rhs_val = this.right.Evaluate();
BaseNode lhs_val = left.Evaluate();
BaseNode rhs_val = right.Evaluate();
return op(lhs_val, rhs_val);
}
}

View File

@@ -1,24 +1,23 @@
using MathEngine.Parser.Parser.Nodes;
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
namespace MathEngine.Parser.Parser
namespace MathEngine.AST.Nodes
{
/// <summary>
/// Represents a Tree node that stores a function
/// </summary>
internal class FunctionNode<T>: BaseNode where T : INumber<T>
internal class FunctionNode<T> : BaseNode where T : INumber<T>
{
Action func;
List<NumericNode<T>> args;
public FunctionNode(Action Function, List<NumericNode<T>> arguments)
{
this.func = Function;
func = Function;
args = arguments;
}

View File

@@ -1,6 +1,8 @@
using MathEngine.Parser.Parser.Nodes;
using MathEngine.Parser.Tokeniser;
namespace MathEngine.Parser.Parser
using MathEngine.Tokenizer;
using static MathEngine.Tokenizer.Token;
namespace MathEngine.AST.Nodes
{
/// <summary>
/// Factory that creates a TreeNode
@@ -18,17 +20,18 @@ namespace MathEngine.Parser.Parser
/// <returns>A TreeNode with CurrentToken as the root value and LeftBranch and RightBranch as Children</returns>
public static BaseNode CreateBinaryNode(Token CurrentToken, BaseNode LeftBranch, BaseNode RightBranch)
{
switch (CurrentToken.Token_Type)
switch (CurrentToken.Type)
{
case Token.Type.Addition:
case TokenType.Addition:
return new BinaryNode(LeftBranch, RightBranch, (a, b) => a + b);
case Token.Type.Subtraction:
case TokenType.Subtraction:
return new BinaryNode(LeftBranch, RightBranch, (a, b) => a - b);
case Token.Type.Multiplication:
case TokenType.Multiplication:
return new BinaryNode(LeftBranch, RightBranch, (a, b) => a * b);
case Token.Type.Division:
case TokenType.Division:
return new BinaryNode(LeftBranch, RightBranch, (a, b) => a / b);
case Token.Type.Exponentiation:
case TokenType.Exponentiation:
throw new NotImplementedException("Exponentiation is not supported at this time!");
return new BinaryNode(LeftBranch, RightBranch, (a, b) => a ^ b);
default:
throw new NotImplementedException("Attempted to create a BinaryNode with an invalid operation!");
@@ -63,23 +66,23 @@ namespace MathEngine.Parser.Parser
}
/// <summary>
/// Returns a Node that holds a numerical value
/// Creates a Function Node, that is a node that operates a number of arguments
/// </summary>
/// <param name="CurrentToken">The token that holds the numeric value</param>
/// <param name="FunctionToken">The function to create the node for</param>
/// <param name="ArgumentStack">Reference to Stack of BaseNode objects to take the arguments from</param>
/// <returns></returns>
public static BaseNode CreateNumericNode(Token CurrentToken)
public static BaseNode CreateNumericNode(Token NumericToken)
{
switch (CurrentToken.NumericalType)
if (NumericToken.Type != TokenType.Numeric)
{
case Token.NumericType.Integer:
throw new NotImplementedException("Integer Numbers are not implemented at this time");
case Token.NumericType.Decimal:
return new NumericNode<decimal>(Decimal.Parse(CurrentToken.TokenValue));
case Token.NumericType.Complex:
throw new NotImplementedException("Complex Numbers are not implemented at this time");
default:
throw new InvalidDataException("Attempted to create a NumericNode with non numeric data!");
}
throw new ArgumentException("Token is not of a numeric type");
}
if (!(decimal.TryParse(NumericToken.Value, out decimal res)))
{
throw new ArgumentException("Failure to parse number");
}
return new NumericNode<decimal>(res);
}
}
}

View File

@@ -1,11 +1,11 @@
using System.Numerics;
namespace MathEngine.Parser.Parser.Nodes
namespace MathEngine.AST.Nodes
{
/// <summary>
/// Represents a Tree node that can store a numeric value
/// </summary>
internal class NumericNode<T> : BaseNode where T: INumber<T>
internal class NumericNode<T> : BaseNode where T : INumber<T>
{
private readonly decimal Value;
@@ -15,8 +15,8 @@ namespace MathEngine.Parser.Parser.Nodes
/// <param name="value">The token for the nodes value</param>
public NumericNode(decimal value)
{
this.Children = null;
this.Value = value;
Children = null;
Value = value;
}
protected override BaseNode Add(BaseNode otherNode)
@@ -64,7 +64,7 @@ namespace MathEngine.Parser.Parser.Nodes
if (otherNode is NumericNode<decimal>)
{
NumericNode<decimal> rhs = (NumericNode<decimal>)otherNode;
return new NumericNode<decimal>((decimal)(Math.Pow((double)Value, (double)(rhs.Value))));
return new NumericNode<decimal>((decimal)Math.Pow((double)Value, (double)rhs.Value));
}
throw new InvalidOperationException("Attempted Invalid operation");
}

View File

@@ -1,6 +1,8 @@
using MathEngine.Parser.Tokeniser;
using MathEngine.AST.Nodes;
using MathEngine.Tokenizer;
using static MathEngine.Tokenizer.Token;
namespace MathEngine.Parser.Parser
namespace MathEngine.AST
{
/// <summary>
/// Class for converting from RPN form to an expression tree
@@ -12,39 +14,39 @@ namespace MathEngine.Parser.Parser
/// </summary>
/// <param name="rpnExpression">RPN expression stack to generate an expression tree from</param>
/// <returns>An expression Tree that represents the Mathematical expression given by rpnExpression</returns>
public static BaseNode TreeFromRPN(Stack<Token> rpnExpression)
public static BaseNode TreeFromRPN(Queue<Token> rpnExpression)
{
Stack<BaseNode> OutputStack = new(rpnExpression.Count);
BaseNode Node;
Token CurrentToken;
while (rpnExpression.Count != 0)
{
CurrentToken = rpnExpression.Pop();
switch (CurrentToken.Token_Type)
CurrentToken = rpnExpression.Dequeue();
switch (CurrentToken.Type)
{
case Token.Type.Numeric:
case TokenType.Numeric:
Node = NodeFactory.CreateNumericNode(CurrentToken);
OutputStack.Push(Node);
break;
// We need to preserve "Left handness" of the ExpressionTree
// i.e 7/8 gives a root node of / with Cnode(0) = 7 and Cnod(1) = 8 etc.
// This should preserve non commutativity
case Token.Type.Addition:
case Token.Type.Subtraction:
case Token.Type.Multiplication:
case Token.Type.Division:
case Token.Type.Exponentiation:
case TokenType.Addition:
case TokenType.Subtraction:
case TokenType.Multiplication:
case TokenType.Division:
case TokenType.Exponentiation:
BaseNode Right = OutputStack.Pop();
BaseNode Left = OutputStack.Pop();
Node = NodeFactory.CreateBinaryNode(CurrentToken, Left, Right);
OutputStack.Push(Node);
break;
case Token.Type.UnaryPlus:
case Token.Type.UnaryMinus:
case TokenType.UnaryPlus:
case TokenType.UnaryMinus:
//Node = NodeFactory.CreateUnaryNode(CurrentToken, OutputStack.Pop());
//OutputStack.Push(Node);
break;
case Token.Type.Function:
case TokenType.Function:
Node = NodeFactory.CreateFunctionNode(CurrentToken, OutputStack);
OutputStack.Push(Node);
break;

View File

@@ -0,0 +1,4 @@
// AssemblyInfo.cs (or any C# source file in your main project)
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("EngineTests")]

View File

@@ -0,0 +1,35 @@
using MathEngine.AST;
using MathEngine.AST.Nodes;
namespace MathEngine.Expression
{
/// <summary>
/// Represents a Mathematical expression.
/// </summary>
public class Expression
{
private ExpressionTree expressionTree;
/// <summary>
/// Initialises a new instance of the
/// </summary>
/// <param name="expression">String representing the Mathematical expression</param>
public Expression(string expression)
{
expressionTree = new ExpressionTree(expression);
}
private Expression(ExpressionTree tree)
{
expressionTree = tree;
}
public Expression Evaluate()
{
return new Expression(expressionTree.Evaluate());
}
}
}

View File

@@ -1,20 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<EnforceCodeStyleInBuild>True</EnforceCodeStyleInBuild>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Parser\Nodes\NumericIntegerNode.cs" />
<Compile Remove="Parser\Nodes\TreeNode.cs" />
</ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>EngineTests</_Parameter1>
</AssemblyAttribute>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="Microsoft.TestPlatform.TestHost" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@@ -1,90 +0,0 @@
using MathEngine.Parser.Parser;
using MathEngine.Parser.Parser.Nodes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MathEngine.Parser.Nodes
{
internal class NumericIntegerNode: BaseNode
{
private readonly decimal Value;
/// <summary>
/// Initialises a new instance of a NumericNode with a given Token
/// </summary>
/// <param name="value">The token for the nodes value</param>
public NumericIntegerNode(decimal value)
{
this.Children = null;
this.Value = value;
}
protected override BaseNode Add(BaseNode otherNode)
{
if (otherNode is NumericIntegerNode)
{
NumericIntegerNode rhs = (NumericIntegerNode)otherNode;
return new NumericIntegerNode(Value + rhs.Value);
}
throw new InvalidOperationException("Attempted Invalid operation");
}
protected override BaseNode Subtract(BaseNode otherNode)
{
if (otherNode is NumericIntegerNode)
{
NumericIntegerNode rhs = (NumericIntegerNode)otherNode;
return new NumericIntegerNode(Value - rhs.Value);
}
throw new InvalidOperationException("Attempted Invalid operation");
}
protected override BaseNode Multiply(BaseNode otherNode)
{
if (otherNode is NumericIntegerNode)
{
NumericIntegerNode rhs = (NumericIntegerNode)otherNode;
return new NumericIntegerNode(Value * rhs.Value);
}
throw new InvalidOperationException("Attempted Invalid operation");
}
protected override BaseNode Divide(BaseNode otherNode)
{
if (otherNode is NumericIntegerNode)
{
NumericIntegerNode rhs = (NumericIntegerNode)otherNode;
return new NumericIntegerNode(Value / rhs.Value);
}
throw new InvalidOperationException("Attempted Invalid operation");
}
protected override BaseNode Exponentiate(BaseNode otherNode)
{
if (otherNode is NumericIntegerNode)
{
NumericIntegerNode rhs = (NumericIntegerNode)otherNode;
return new NumericIntegerNode((Int64)(Math.Pow((double)(Value), (double)rhs.Value)));
}
throw new InvalidOperationException("Attempted Invalid operation");
}
/// <summary>
/// Evaluates the Numeric Node by simply returning the current instance
/// </summary>
/// <returns></returns>
public override BaseNode Evaluate()
{
return this;
}
public override string ToString()
{
return Value.ToString();
}
}
}

View File

@@ -0,0 +1,218 @@
using MathEngine.Tokenizer;
using System.Security;
using static MathEngine.Tokenizer.Token;
namespace MathEngine.Parser
{
/// <summary>
/// Represents the conversion from a list of Tokens representing Mathematical expression to List in Reverse Polish Notation form
/// </summary>
internal class Parser
{
/// <summary>
/// Return the Precdence of a given token operator
/// </summary>
/// <param name="X">Token to get Precdence of</param>
/// <returns>An integer value that represent the precedence value of the token</returns>
private static int OperatorPrecedence(Token X)
{
switch (X.Type)
{
case TokenType.Addition:
case TokenType.Subtraction:
{
return 1;
}
case TokenType.Multiplication:
case TokenType.Division:
{
return 2;
}
case TokenType.UnaryPlus:
case TokenType.UnaryMinus:
{
return 3;
}
case TokenType.Exponentiation:
{
return 4;
}
case TokenType.LeftParenthesis:
case TokenType.RightParenthesis:
{
return 5;
}
default:
{
throw new Exception("Unknown operator precedence" + X.Value);
}
}
}
/// <summary>
/// Is the operation left associative
/// </summary>
/// <param name="X">Operation to check</param>
/// <returns>True if the token is left associative, False otherwise</returns>
private static bool IsLeftAssociatve(Token X)
{
switch (X.Type)
{
case TokenType.Addition:
case TokenType.Subtraction:
case TokenType.Multiplication:
case TokenType.Division:
case TokenType.LeftParenthesis:
case TokenType.RightParenthesis:
{
return true;
}
case TokenType.Exponentiation:
case TokenType.UnaryMinus:
{
return false;
}
default:
{
throw new Exception("If you are seeing this something went wrong when trying to determine if a token was Left Associatve");
}
}
}
/// <summary>
/// Pops operators from the operator stack to the output queue based on precedence and associativity rules.
/// This method is part of the Shunting Yard algorithm, ensuring that operators with higher or equal precedence
/// (when left-associative) are correctly ordered in the resulting Reverse Polish Notation (RPN).
/// </summary>
/// <param name="operatorStack">The stack holding operators and functions that are yet to be added to the output.</param>
/// <param name="outputQueue">The queue that accumulates tokens in RPN order.</param>
/// <param name="current">The current operator token being processed from the input expression.</param>
private static void PopOperatorsToQueue(Stack<Token> operatorStack, Queue<Token> outputQueue, Token current)
{
while (operatorStack.Count > 0)
{
Token top = operatorStack.Peek();
bool isFunction = top.Type == TokenType.Function;
bool isHigherPrecedence = OperatorPrecedence(top) > OperatorPrecedence(current);
bool isEqualPrecedenceAndLeftAssociative = ((OperatorPrecedence(top) == OperatorPrecedence(current)) && IsLeftAssociatve(current));
bool isNotLeftParenthesis = top.Type != TokenType.LeftParenthesis;
if ((isFunction || isHigherPrecedence || isEqualPrecedenceAndLeftAssociative) && isNotLeftParenthesis)
{
outputQueue.Enqueue(operatorStack.Pop());
}
else
{
break;
}
}
}
/// <summary>
/// Handles pushing either an addition or subtraction operator token onto the operator stack, distinguishing
/// between unary and binary addition or subtract operators based on the previous token context.
/// </summary>
/// <param name="operatorStack">The stack holding operators during parsing.</param>
/// <param name="outputQueue">The output queue representing the RPN expression.</param>
/// <param name="current">The current operator token being processed.</param>
/// <param name="previous">The previous token processed, used to determine if the current operator is unary.</param>
private static void PushAdditionSubtractionWithUnaryCheck(Stack<Token> operatorStack, Queue<Token> outputQueue, Token current, Token previous)
{
bool isUnary = previous.Type == TokenType.None ||
previous.Type == TokenType.LeftParenthesis ||
previous.Type == TokenType.Addition ||
previous.Type == TokenType.Subtraction ||
previous.Type == TokenType.Multiplication ||
previous.Type == TokenType.Division ||
previous.Type == TokenType.Exponentiation;
if (isUnary)
{
var unaryToken = current.Type == TokenType.Subtraction ? Token.UnaryMinus : Token.UnaryPlus;
PopOperatorsToQueue(operatorStack, outputQueue, unaryToken);
operatorStack.Push(unaryToken);
}
else
{
PopOperatorsToQueue(operatorStack, outputQueue, current);
operatorStack.Push(current);
}
}
/// <summary>
/// Parses a list of tokens into a valid RPN expression
/// </summary>
/// <param name="Expression">List of tokens to parse</param>
/// <returns>Returns a <see cref="Queue{Token}"/> representing the Reverse polish notation form of the expression</returns>
internal static Queue<Token> Parse(List<Token> Expression)
{
//Temp holding stack for operators
Stack<Token> OperatorStack = new(Expression.Count/2);
//The final stack to return
Queue<Token> rpnQueue = new(Expression.Count);
//Stack used to hold the number of input params to a function
//Stack<uint> ArityStack = new Stack<uint>();
Token PreviousToken = Token.None;
Token CurrentToken;
for (int i = 0; i < Expression.Count; i++)
{
CurrentToken = Expression[i];
switch (CurrentToken.Type)
{
case TokenType.Numeric:
rpnQueue.Enqueue(CurrentToken);
break;
case TokenType.Addition:
case TokenType.Subtraction:
PushAdditionSubtractionWithUnaryCheck(OperatorStack, rpnQueue, CurrentToken, PreviousToken);
break;
case TokenType.Multiplication:
case TokenType.Division:
case TokenType.Exponentiation:
PopOperatorsToQueue(OperatorStack, rpnQueue, CurrentToken);
OperatorStack.Push(CurrentToken);
break;
case TokenType.Function:
OperatorStack.Push(CurrentToken);
break;
case TokenType.LeftParenthesis:
OperatorStack.Push(CurrentToken);
break;
case TokenType.RightParenthesis:
while (OperatorStack.Count != 0 && (OperatorStack.Peek() != Token.LeftParenthesis))
{
rpnQueue.Enqueue(OperatorStack.Pop());
}
if (OperatorStack.Count == 0)
{
throw new ParserException("Mismatched parentheses: Missing left parenthesis");
}
OperatorStack.Pop(); // Discard the left parenthesis
if (OperatorStack.Count > 0 && (OperatorStack.Peek().Type == TokenType.Function))
{
rpnQueue.Enqueue(OperatorStack.Pop());
}
break;
}
PreviousToken = CurrentToken;
}
while ((OperatorStack.Count > 0))
{
if (OperatorStack.Peek().Type == TokenType.LeftParenthesis || OperatorStack.Peek().Type == TokenType.RightParenthesis)
throw new Exception("Mismatched parentheses; Expected (");
else
rpnQueue.Enqueue(OperatorStack.Pop());
}
return rpnQueue;
}
}
}

View File

@@ -1,85 +0,0 @@
using MathEngine.Parser.Tokeniser;
namespace MathEngine.Parser.Parser
{
/// <summary>
/// Represents an Abstract Syntax tree for expresison evaluation
/// </summary>
public class ExpressionTree
{
/// <summary>
/// The root node of the expression tree;
/// </summary>
private readonly BaseNode rootNode;
private readonly BaseNode? evaluated_expression;
/// <summary>
/// Initialises a new instance of the MathEngine.Parser.Parser.Node class with a given Token
/// <param name="value">The token for the nodes value</param>
/// </summary>
/// <param name="Expression">String representing the </param>
public ExpressionTree(string Expression)
{
List<Token> tokens = Tokeniser.Tokeniser.Tokenise(Expression);
Stack<Token> rpnForm = Parser.Parse(tokens);
rootNode = TreeGenerator.TreeFromRPN(rpnForm);
evaluated_expression = rootNode.Evaluate();
}
private ExpressionTree(BaseNode rootNode)
{
this.rootNode = rootNode;
}
/* /// <summary>
/// Evaluates the current instance of ExpressionTree
/// </summary>
/// <returns>Returns an update of the current instance which the expression Evaluated</returns>
public ExpressionTree Evaluate()
{
return new ExpressionTree(rootNode.Evaluate());
}*/
/// <summary>
/// Returns a value indicating if the given object is equal to the current instance of ExpressionTree
/// </summary>
/// <param name="other">The object to compare to the current instance</param>
/// <returns>True if they are equal, False otherwise</returns>
//public override bool Equals(object? other)
//{
// if (other is BaseNode)
// {
// ExpressionTree otherTree = new((BaseNode)other);
// return this.Equals(otherTree);
//}
// return false;
//}
/// <summary>
/// Compares the current ExpressionTree instance for equality with the given ExpressionTree
/// </summary>
/// <param name="other">The ExpressionTree to compare to the current instance</param>
/// <returns>True if the expression trees are equal and False otherwise</returns>
// public bool Equals(ExpressionTree other)
//{
// if (this == null || other == null)
// {
// return false;
// }
// if (this.rootNode == null || other.rootNode == null)
// {
// return false;
// }
// return this.rootNode.Equals(other.rootNode);
//}
public override int GetHashCode()
{
return System.HashCode.Combine(rootNode, evaluated_expression);
}
public override string ToString()
{
return evaluated_expression.ToString();
}
}
}

View File

@@ -1,161 +0,0 @@
using MathEngine.Parser.Tokeniser;
namespace MathEngine.Parser.Parser
{
/// <summary>
/// Represents the conversion from a list of Tokens representing Mathematical expression to List in Reverse Polish Notation form
/// </summary>
internal class Parser
{
/// <summary>
/// Return the Precdence of a given token operator
/// </summary>
/// <param name="X">Token to get Precdence of</param>
/// <returns>An integer value that represent the precedence value of the token</returns>
private static int OperatorPrecedence(Token X)
{
switch (X.Token_Type)
{
case Token.Type.Addition:
case Token.Type.Subtraction:
{
return 1;
}
case Token.Type.Multiplication:
case Token.Type.Division:
{
return 2;
}
case Token.Type.UnaryPlus:
case Token.Type.UnaryMinus:
{
return 3;
}
case Token.Type.Exponentiation:
{
return 4;
}
case Token.Type.LeftParenthesis:
case Token.Type.RightParenthesis:
{
return 5;
}
default:
{
throw new Exception("Unknown operator precedence" + X.TokenValue);
}
}
}
/// <summary>
/// Is the operation left associative
/// </summary>
/// <param name="X">Operation to check</param>
/// <returns>True if the token is left associative, False otherwise</returns>
private static bool IsLeftAssociatve(Token X)
{
switch (X.Token_Type)
{
case Token.Type.Addition:
case Token.Type.Subtraction:
case Token.Type.Multiplication:
case Token.Type.Division:
case Token.Type.LeftParenthesis:
case Token.Type.RightParenthesis:
{
return true;
}
case Token.Type.Exponentiation:
case Token.Type.UnaryMinus:
{
return false;
}
default:
{
throw new Exception("If you are seeing this something went wrong when trying to determine if a token was Left Associatve");
}
}
}
/// <summary>
/// Reverse the order of a given stack of Tokens
/// </summary>
/// <param name="Stack">Stack to reverse</param>
/// <returns>The input stack in the reserve order</returns>
private static Stack<Token> ReverseStackTok(Stack<Token> Stack)
{
Stack<Token> OutputStack = new (Stack.Count);
while ((Stack.Count != 0))
OutputStack.Push(Stack.Pop());
return OutputStack;
}
/// <summary>
/// Parses a list of tokens into a valid RPN expression stack
/// </summary>
/// <param name="Expression">List of tokens to parse</param>
/// <returns>Returns the Reverse polish notation form of the expression</returns>
internal static Stack<Token> Parse(List<Token> Expression)
{
//Temp holding stack for operators
Stack<Token> OperatorStack = new(Expression.Count/2);
//The final stack to return
Stack<Token> OutputStack = new(Expression.Count);
//Stack used to hold the number of input params to a function
//Stack<uint> ArityStack = new Stack<uint>();
Token CurrentToken;
for (int i = 0; i < Expression.Count; i++)
{
CurrentToken = Expression[i];
switch (CurrentToken.Token_Type)
{
case Token.Type.Numeric:
OutputStack.Push(CurrentToken);
break;
case Token.Type.Addition:
case Token.Type.Subtraction:
case Token.Type.Multiplication:
case Token.Type.Division:
case Token.Type.Exponentiation:
while ((OperatorStack.Count != 0 && ((((OperatorStack.Peek().Token_Type == Token.Type.Function) | (OperatorPrecedence(OperatorStack.Peek()) > OperatorPrecedence(CurrentToken)) | ((OperatorPrecedence(OperatorStack.Peek()) == OperatorPrecedence(CurrentToken)) & (IsLeftAssociatve(CurrentToken)))) && !(OperatorStack.Peek().Token_Type == Token.Type.LeftParenthesis)))))
{
OutputStack.Push(OperatorStack.Pop());
}
OperatorStack.Push(CurrentToken);
break;
case Token.Type.Function:
OperatorStack.Push(CurrentToken);
break;
case Token.Type.LeftParenthesis:
OperatorStack.Push(CurrentToken);
break;
case Token.Type.RightParenthesis:
while (OperatorStack.Peek() != Token.LeftParenthesis && OperatorStack.Count != 0)
{
OutputStack.Push(OperatorStack.Pop());
}
OperatorStack.Pop(); // Discard the left parenthesis
if (OperatorStack.Peek().Token_Type == Token.Type.Function)
{
OutputStack.Push(OperatorStack.Pop());
}
break;
}
}
while ((OperatorStack.Count > 0))
{
if (OperatorStack.Peek().Token_Type == Token.Type.LeftParenthesis || OperatorStack.Peek().Token_Type == Token.Type.RightParenthesis)
throw new Exception("Mismatched parentheses; Expected (");
else
OutputStack.Push(OperatorStack.Pop());
}
return ReverseStackTok(OutputStack);
}
}
}

View File

@@ -0,0 +1,37 @@
namespace MathEngine.Parser
{
/// <summary>
/// Represents an error that occured during the Parsing process
/// </summary>
internal class ParserException : Exception
{
/// <summary>
/// Initializes A new instance of the <see cref="TokenizerException"></see> class
/// </summary>
public ParserException()
{
}
/// <summary>
/// Initializes A new instance of the <see cref="TokenizerException"></see> class with a specified error message
/// </summary>
/// <param name="message">The message that describes the error</param>
public ParserException(string message)
: base(message)
{
}
/// <summary>
/// Initializes A new instance of the <see cref="TokenizerException"></see> class with a specified error message and a reference to the inner exception that is the cause of this exception
/// </summary>
/// <param name="message">The message that describes the error</param>
/// <param name="innerException">The exception that is the cause of the current excepton</param>
public ParserException(string message, Exception innerException)
: base(message, innerException)
{
}
}
}

View File

@@ -1,285 +0,0 @@
namespace MathEngine.Parser.Tokeniser
{
/// <summary>
/// Defines the Token Type. The base for all manipulations
/// </summary>
internal readonly struct Token
{
/// <summary>
/// Represents the token for +
/// </summary>
public static readonly Token Plus = new("+",Type.Addition,NumericType.NaN,0);
/// <summary>
/// Represents the token for -
/// </summary>
public static readonly Token Minus = new("-", Type.Subtraction, NumericType.NaN, 0);
/// <summary>
/// Represents the token for *
/// </summary>
public static readonly Token Multiply = new("*", Type.Multiplication, NumericType.NaN, 0);
/// <summary>
/// Represents the token for /
/// </summary>
public static readonly Token Divide = new("/", Type.Division, NumericType.NaN, 0);
/// <summary>
/// Represents the token for (
/// </summary>
public static readonly Token LeftParenthesis = new Token("(", Token.Type.LeftParenthesis, Token.NumericType.NaN, 0);
/// <summary>
/// Represents the token for )
/// </summary>
public readonly static Token RightParenthesis = new Token(")", Token.Type.RightParenthesis, Token.NumericType.NaN, 0);
/// Represents the token for ^
/// </summary>
public static readonly Token Exponentiation = new("^", Type.Exponentiation, NumericType.NaN, 0);
/// <summary>
/// Enum representing the token type
/// </summary>
internal enum Type
{
Numeric,
DecimalPoint,
Addition,
Subtraction,
Multiplication,
Division,
Exponentiation,
UnaryPlus,
UnaryMinus,
Operator,
Variable,
Function,
FunctionArgumentSeparator,
LeftParenthesis,
RightParenthesis,
OpenBracket,
CloseBracket,
OpenBrace,
CloseBrace
}
/// <summary>
/// Enum representing the numerical type of the token
/// </summary>
internal enum NumericType
{
Integer,
Decimal,
Complex,
NaN
}
/// <summary>
/// String representing the value of the token
/// </summary>
private readonly string Value;
/// <summary>
/// The type of token
/// </summary>
private readonly Type TokenType;
/// <summary>
/// The numeric type of the token
/// </summary>
private readonly NumericType Numeric_Type;
/// <summary>
/// The arity of the token
/// </summary>
private readonly uint Arity;
#region "Properties"
/// <summary>
/// Returns the value of the Token
/// </summary>
public string TokenValue
{
get { return Value; }
}
/// <summary>
/// Returns the type of the token
/// </summary>
public readonly Type Token_Type
{
get { return TokenType; }
}
/// <summary>
/// Returns the numerical type of the token
/// </summary>
public readonly NumericType NumericalType
{
get { return Numeric_Type; }
}
/// <summary>
/// Returns the arity of the token
/// </summary>
public uint FunctionArity
{
get { return Arity; }
}
#endregion
/// <summary>
/// Initializes a new instance of the Tokeniser.Token structure with a given TokenValue, TokenType, TokenNumericType and Arity
/// </summary>
/// <param name="TokenValue">String representing the value of the token</param>
/// <param name="TokenType">The type that the token instance represents</param>
/// <param name="TokenNumericType">The numeric type of the token</param>
/// <param name="FunctionArity">The token Arity</param>
public Token(string TokenValue, Type TokenType, NumericType TokenNumericType, uint FunctionArity = 0)
{
this.Value = TokenValue;
this.TokenType = TokenType;
this.Numeric_Type = TokenNumericType;
this.Arity = FunctionArity;
}
/// <summary>
/// Initializes a new instance of the Tokeniser.Token structure with a given TokenValue, TokenType, TokenNumericType and Arity
/// </summary>
/// <param name="TokenValue">Char representing the value of the token</param>
/// <param name="TokenType">The type that the token instance represents</param>
/// <param name="TokenNumericType">The numeric type of the token</param>
/// <param name="FunctionArity">The token Arity</param>
public Token(char TokenValue, Type TokenType, NumericType TokenNumericType, uint FunctionArity = 0)
{
this.Value = TokenValue.ToString();
this.TokenType = TokenType;
this.Numeric_Type = TokenNumericType;
this.Arity = FunctionArity;
}
#if DEBUG
/// <summary>
/// Debug String; Used to give a string representation of a token
/// </summary>
/// <returns>Returns a string representation of the current token instance</returns>
public new string ToString()
{
return Value + "," + TokenType.ToString() + "," + Numeric_Type.ToString() + "," + Arity.ToString();
}
#endif
/// <summary>
/// Returns a value that indicates whether two Tokens are equal.
/// </summary>
/// <param name="X">First Token to compare</param>
/// <param name="Y">Second Token to compare</param>
/// <returns>Returns true if the two Tokens are equal and false otherwise</returns>
public static bool operator ==(Token X, Token Y)
{
if (X.TokenValue != Y.TokenValue)
{
return false;
}
if (X.TokenType != Y.TokenType)
{
return false;
}
if (X.NumericalType != Y.NumericalType)
{
return false;
}
if (X.Arity != Y.Arity)
{
return false;
}
return true;
}
/// <summary>
/// Returns a value that indicates whether two Tokens are not equal.
/// </summary>
/// <param name="X">First Token to compare</param>
/// <param name="Y">Second Token to compare</param>
/// <returns>Returns true if the two Tokens are not equal and false otherwise</returns>
public static bool operator !=(Token X, Token Y)
{
if (X.TokenValue != Y.TokenValue)
{
return true;
}
if (X.TokenType != Y.TokenType)
{
return true;
}
if (X.NumericalType != Y.NumericalType)
{
return true;
}
if (X.Arity != Y.Arity)
{
return true;
}
return false;
}
/// <summary>
/// Returns a value that indicates whether the given Token is equal to the current Token instance.
/// </summary>
/// <param name="other">Token to compare</param>
/// <returns>Returns true if the two Tokens are equal and false otherwise</returns>
public bool Equals(Token other)
{
if (this.TokenValue != other.TokenValue)
{
return false;
}
if (this.TokenType != other.TokenType)
{
return false;
}
if (this.NumericalType != other.NumericalType)
{
return false;
}
if (this.Arity != other.Arity)
{
return false;
}
return true;
}
/// <summary>
/// Returns a value that indicates whether an object is equal to the current Token instance.
/// </summary>
/// <param name="obj">The object to compare to the Token instance</param>
/// <returns>Returns true if the object is equal to the Token instance, false otherwise</returns>
public override bool Equals(object? obj)
{
if (obj is Token other)
{
return this.Equals(other);
}
else
{
return false;
}
}
/// <summary>
/// Calculates the HashCode for the current Token Instance
/// </summary>
/// <returns></returns>
public override int GetHashCode()
{
return HashCode.Combine(Value, TokenType, Numeric_Type, Arity);
}
}
}

View File

@@ -1,136 +0,0 @@
namespace MathEngine.Parser.Tokeniser
{
/// <summary>
/// Represents the conversion of a Mathematical expression in string form to a List of Tokens
/// </summary>
internal static class Tokeniser
{
private static readonly List<char> Operators = new(new char[] { '+', '-', '*', '/', '^' });
private static readonly List<char> Parenthesis = new(new char[] { '(', ')'});
/// <summary>
/// Gets the next non-whitespace char or returns the null terminator is at EOS (End of stream)
/// </summary>
static private char GetNextChar(string Expresison, ref Int32 currentIndex)
{
char curChar;
currentIndex++;
curChar = currentIndex >= Expresison.Length ? '\0' : Expresison[currentIndex];
return curChar;
}
/// <summary>
/// Returns the token that represents the current character
/// </summary>
/// <param name="curChar">The character to get the token of</param>
/// <returns>A token representing the current character, otherwise an exception is thrown</returns>
static private Token GetOperatorToken(char curChar)
{
return curChar switch
{
'+' => Token.Plus,
'-' => Token.Minus,
'*' => Token.Multiply,
'/' => Token.Divide,
'^' => Token.Exponentiation,
_ => throw new Exception(String.Format("Character {0} is not a defined operator", curChar)),
};
}
static private Token GetParenthesisToken(char curChar)
{
return curChar switch
{
'(' => Token.LeftParenthesis,
')' => Token.RightParenthesis,
_ => throw new Exception(String.Format("Character {0} is not Parenthesis", curChar)),
};
}
/// <summary>
/// Tokenises a given Mathematical expression given as a string to a list of tokens
/// </summary>
/// <param name="Expression">Expression to tokenise</param>
/// <returns>A list of tokens representing the given expression, if the expression string is null or empty then an empty list is returned</returns>
static internal List<Token> Tokenise(string Expression)
{
if (string.IsNullOrEmpty(Expression))
{
return new List<Token> { };
}
else
{
//Cleanup whitespace
Expression = String.Concat(Expression.Where(c => !Char.IsWhiteSpace(c)));
Int32 currentIndex = -1;
List<Token> Tokenstack = new(Expression.Length); //Nearly always is overallocated to the true number of tokens but avoids the need to kkeep reallocating for a growing stack
char curChar;
while (currentIndex < Expression.Length)
{
curChar = GetNextChar(Expression,ref currentIndex);
//Switch on special characters
if (Operators.Contains(curChar))
{
Tokenstack.Add(Tokeniser.GetOperatorToken(curChar));
continue; //Next loop interation
}
if (Parenthesis.Contains(curChar))
{
Tokenstack.Add(Tokeniser.GetParenthesisToken(curChar));
continue;
}
//Number, two cases two consider, case 1) number is something like 142.2; case 2 .5 which is clearly 0.5.
//Case 1
if (Char.IsDigit(curChar))
{
bool hasDecimalPlace = false;
Int32 tempIndex = currentIndex;
char tempChar;
do
{
tempChar = GetNextChar(Expression, ref tempIndex);
if (tempChar == '.' && !hasDecimalPlace)
{
hasDecimalPlace = true;
}
else if (tempChar == '.' && hasDecimalPlace)
{
while (Char.IsDigit(tempChar) || tempChar == '.')
{
tempChar = GetNextChar(Expression, ref tempIndex);
}
string errString = Expression[currentIndex..tempIndex];
throw new Exception(String.Format("Syntax error: {0} has multiple decimal point when at most one is allowed", errString));
}
} while (Char.IsDigit(tempChar) || tempChar == '.');
//Iterate until we hit the next special character or EOS
//Token is a number so add this to the stack
Tokenstack.Add(new Token(Expression[currentIndex..tempIndex], Token.Type.Numeric, Token.NumericType.Decimal, 0));
currentIndex = tempIndex - 1; //Sets the index variable to just before the non numeric char
}
else if (Char.IsAsciiLetter(curChar)) // Keep reading until a parenthesis
{
Int32 tempIndex = currentIndex;
char tempChar;
do
{
tempChar = GetNextChar(Expression, ref tempIndex);
} while (Char.IsAscii(tempChar) && tempChar != '(');
//Token is a potential function so add this to the stack
Tokenstack.Add(new Token(Expression[currentIndex..tempIndex], Token.Type.Function, Token.NumericType.NaN, 0));
currentIndex = tempIndex - 1; //Sets the index variable to just before the end of the function
}
}
//return the stack after triming
Tokenstack.TrimExcess();
return Tokenstack;
}
}
}
}

View File

@@ -0,0 +1,144 @@
using static MathEngine.Tokenizer.Token;
namespace MathEngine.Tokenizer
{
/// <summary>
/// Defines the Token type. The base for all manipulations.
/// </summary>
internal readonly record struct Token(
string Value,
TokenType Type,
uint Arity = 0
)
{
/// <summary>
/// Enum representing the token type.
/// </summary>
internal enum TokenType
{
None,
Numeric,
DecimalPoint,
Addition,
Subtraction,
Multiplication,
Division,
Exponentiation,
UnaryPlus,
UnaryMinus,
Operator,
Variable,
Function,
FunctionArgumentSeparator,
LeftParenthesis,
RightParenthesis,
OpenBracket,
CloseBracket,
OpenBrace,
CloseBrace
}
// Predefined tokens
public static readonly Token None = new("", TokenType.None);
public static readonly Token Plus = new("+", TokenType.Addition);
public static readonly Token UnaryPlus = new("-", TokenType.UnaryPlus);
public static readonly Token Minus = new("-", TokenType.Subtraction);
public static readonly Token UnaryMinus = new("-", TokenType.UnaryMinus);
public static readonly Token Multiply = new("*", TokenType.Multiplication);
public static readonly Token Divide = new("/", TokenType.Division);
public static readonly Token Exponentiation = new("^", TokenType.Exponentiation);
public static readonly Token LeftParenthesis = new("(", TokenType.LeftParenthesis);
public static readonly Token RightParenthesis = new(")", TokenType.RightParenthesis);
/// <summary>
/// Returns true if the token represents any arithmetic operator.
/// </summary>
public bool IsOperator
{
get
{
return Type is TokenType.Addition
or TokenType.Subtraction
or TokenType.Multiplication
or TokenType.Division
or TokenType.Exponentiation
or TokenType.UnaryPlus
or TokenType.UnaryMinus;
}
}
/// <summary>
/// Returns true if the token represents a numeric literal.
/// </summary>
public bool IsNumeric
{
get
{
return Type is TokenType.Numeric;
}
}
/// <summary>
/// Returns true if the token is a variable
/// </summary>
public bool IsVariable
{
get
{
return Type is TokenType.Variable;
}
}
/// <summary>
/// Returns true if the token represents a function
/// </summary>
public bool IsFunction
{
get
{
return Type is TokenType.Function;
}
}
/// <summary>
/// Returns true if the token is a left or right parenthesis.
/// </summary>
public bool IsParenthesis
{
get
{
return Type is TokenType.LeftParenthesis or TokenType.RightParenthesis;
}
}
/// <summary>
/// Returns true if the token is a grouping symbol of any kind.
/// </summary>
public bool IsGrouping
{
get
{
return Type is TokenType.LeftParenthesis
or TokenType.RightParenthesis
or TokenType.OpenBracket
or TokenType.CloseBracket
or TokenType.OpenBrace
or TokenType.CloseBrace;
}
}
/// <summary>
/// Returns true if the token separates function arguments.
/// </summary>
public bool IsArgumentSeparator
{
get
{
return Type is TokenType.FunctionArgumentSeparator;
}
}
}
}

View File

@@ -0,0 +1,172 @@
using MathEngine.Tokenizer;
using static MathEngine.Tokenizer.Token;
namespace MathEngine.Tokenizer
{
/// <summary>
/// Represents the conversion of a Mathematical expression in string form to a List of Tokens
/// </summary>
internal static class ExpressionTokenizer
{
private static readonly HashSet<char> Operators = new() { '+', '-', '*', '/', '^' };
private static readonly HashSet<char> Parenthesis = new() { '(', ')' };
/// <summary>
/// Checks if a character represents an ASCII numerical digit
/// </summary>
/// <param name="c">The character to check</param>
/// <returns>True if <paramref name="c"/> represents an ASCII numerical digit, false otherwise</returns>
private static bool IsAsciiDigit(char c)
{
return c >= '0' && c <= '9';
}
/// <summary>
/// Checks if a character represents an ASCII whitespace character
/// </summary>
/// <param name="c">The character to check</param>
/// <returns>True if <paramref name="c"/> represents an ASCII whitespace character, false otherwise</returns>
private static bool IsWhitespace(char c)
{
return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\v' || c == '\f';
}
/// <summary>
/// Checks if a character represents an ASCII dot character
/// </summary>
/// <param name="c">The character to check</param>
/// <returns>True if <paramref name="c"/> represents an ASCII dot character, false otherwise</returns>
private static bool IsDecimalPoint(char c)
{
return c == '.';
}
/// <summary>
/// Parses the <paramref name="expression"/> to read a numeric <typeparamref name="Token"/>
/// </summary>
/// <param name="expression">The expression being parsed</param>
/// <param name="at_decimal_point">Indicates if the character read before calling this method was a decimal point or not</param>
/// <param name="current_index">The current index the parser is at</param>
/// <returns>A <typeparamref name="Token"/> representing the numeric value read</returns>
private static Token ReadNumericToken(ReadOnlySpan<char> expression, bool at_decimal_point, ref Int32 currentIndex)
{
Int32 tempIndex = currentIndex + 1;
bool hasDecimalPlace = at_decimal_point;
char tempChar;
while (tempIndex < expression.Length)
{
tempChar = expression[tempIndex];
if (tempChar == '.' && !hasDecimalPlace)
{
hasDecimalPlace = true;
}
else if (tempChar == '.' && hasDecimalPlace)
{
int errIndex = tempIndex + 1;
while (errIndex < expression.Length && (IsAsciiDigit(tempChar) || IsDecimalPoint(tempChar)))
{
errIndex++;
}
ReadOnlySpan<char> errSpan = expression.Slice(currentIndex, errIndex - currentIndex);
throw new TokenizerException($"Syntax error: The number {errSpan.ToString()} has multiple decimal point when at most one is allowed.");
}
else if (!IsAsciiDigit(tempChar) && tempChar != '.')
{
break; // end of numeric token
}
tempIndex++;
}
ReadOnlySpan<char> numberSpan = expression.Slice(currentIndex, tempIndex - currentIndex);
currentIndex = tempIndex - 1; //Sets the index variable to just before the non numeric char
//Token is a number so add this to the stack
return new Token(numberSpan.ToString(), TokenType.Numeric);
}
static private Token GetOperatorToken(char curChar)
{
return curChar switch
{
'+' => Token.Plus,
'-' => Token.Minus,
'*' => Token.Multiply,
'/' => Token.Divide,
'^' => Token.Exponentiation,
_ => throw new TokenizerException($"Recieved unknown character '{curChar}' when attempting to parse an operator.")
};
}
static private Token GetParenthesisToken(char curChar)
{
return curChar switch
{
'(' => Token.LeftParenthesis,
')' => Token.RightParenthesis,
_ => throw new TokenizerException($"Recieved unknown character '{curChar}' when attempting to parse a parenthesis."),
};
}
/// <summary>
/// Tokenizes a given Mathematical expression given as a string to a list of tokens
/// </summary>
/// <param name="expression">Expression to tokenise</param>
/// <returns>A list of tokens representing the given expression, if the expression string is null or empty then an empty list is returned</returns>
static internal List<Token> Tokenize(ReadOnlySpan<char> expression)
{
if (expression.IsEmpty)
{
return new List<Token> { };
}
List<Token> Tokenstack = new(expression.Length); //Nearly always is overallocated to the true number of tokens but avoids the need to keep reallocating for a growing stack
char curChar;
for (int i = 0; i < expression.Length; i++)
{
curChar = expression[i];
//Cleanup whitespace
if (IsWhitespace(curChar))
{
continue;
}
//Switch on special characters
if (Operators.Contains(curChar))
{
Tokenstack.Add(GetOperatorToken(curChar));
continue; //Next loop interation
}
if (Parenthesis.Contains(curChar))
{
Tokenstack.Add(GetParenthesisToken(curChar));
continue;
}
//Number, two cases two consider, case 1) number is something like 142.2; case 2 .5 which is clearly 0.5.
//Case 1
if (IsAsciiDigit(curChar))
{
Tokenstack.Add(ReadNumericToken(expression, false, ref i));
continue;
}
else if (IsDecimalPoint(curChar))
{
Tokenstack.Add(ReadNumericToken(expression, true, ref i));
continue;
}
}
//return the stack after triming
Tokenstack.TrimExcess();
return Tokenstack;
}
}
}

View File

@@ -0,0 +1,37 @@
namespace MathEngine.Tokenizer
{
/// <summary>
/// Represents an error that occured during the Tokenization process
/// </summary>
internal class TokenizerException : Exception
{
/// <summary>
/// Initializes A new instance of the <see cref="TokenizerException"></see> class
/// </summary>
public TokenizerException()
{
}
/// <summary>
/// Initializes A new instance of the <see cref="TokenizerException"></see> class with a specified error message
/// </summary>
/// <param name="message">The message that describes the error</param>
public TokenizerException(string message)
: base(message)
{
}
/// <summary>
/// Initializes A new instance of the <see cref="TokenizerException"></see> class with a specified error message and a reference to the inner exception that is the cause of this exception
/// </summary>
/// <param name="message">The message that describes the error</param>
/// <param name="innerException">The exception that is the cause of the current excepton</param>
public TokenizerException(string message, Exception innerException)
: base(message, innerException)
{
}
}
}

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MathEngine\MathEngine.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,15 @@
using MathEngine.Expression;
namespace MathRunner
{
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine("Evaluting expression...");
Expression exp = new("54+2+1");
Console.WriteLine(exp.Evaluate());
Console.WriteLine("Hello, World!");
}
}
}