From b5c318f8659cb2fbcc916d602efa6cfa4a9ad48a Mon Sep 17 00:00:00 2001 From: Jim <112640460+0xJ1M@users.noreply.github.com> Date: Tue, 9 May 2023 20:31:11 +0100 Subject: [PATCH] Inital Commit --- MathEngine/EngineTests/EngineTests.csproj | 22 ++ .../Parser Tests/ExpressionTreeTests.cs | 59 +++++ .../EngineTests/Parser Tests/ParserTests.cs | 92 +++++++ .../Parser Tests/TokenIserTests.cs | 65 +++++ .../EngineTests/Parser Tests/TreeNodeTests.cs | 30 +++ MathEngine/EngineTests/Usings.cs | 1 + MathEngine/MathEngine.sln | 31 +++ MathEngine/MathEngine/MathEngine.csproj | 14 + .../Parser/Parser/ExpressionTree.cs | 211 +++++++++++++++ .../MathEngine/Parser/Parser/Node/TreeNode.cs | 153 +++++++++++ MathEngine/MathEngine/Parser/Parser/Parser.cs | 142 ++++++++++ .../MathEngine/Parser/Tokeniser/Token.cs | 244 ++++++++++++++++++ .../MathEngine/Parser/Tokeniser/Tokeniser.cs | 110 ++++++++ 13 files changed, 1174 insertions(+) create mode 100644 MathEngine/EngineTests/EngineTests.csproj create mode 100644 MathEngine/EngineTests/Parser Tests/ExpressionTreeTests.cs create mode 100644 MathEngine/EngineTests/Parser Tests/ParserTests.cs create mode 100644 MathEngine/EngineTests/Parser Tests/TokenIserTests.cs create mode 100644 MathEngine/EngineTests/Parser Tests/TreeNodeTests.cs create mode 100644 MathEngine/EngineTests/Usings.cs create mode 100644 MathEngine/MathEngine.sln create mode 100644 MathEngine/MathEngine/MathEngine.csproj create mode 100644 MathEngine/MathEngine/Parser/Parser/ExpressionTree.cs create mode 100644 MathEngine/MathEngine/Parser/Parser/Node/TreeNode.cs create mode 100644 MathEngine/MathEngine/Parser/Parser/Parser.cs create mode 100644 MathEngine/MathEngine/Parser/Tokeniser/Token.cs create mode 100644 MathEngine/MathEngine/Parser/Tokeniser/Tokeniser.cs diff --git a/MathEngine/EngineTests/EngineTests.csproj b/MathEngine/EngineTests/EngineTests.csproj new file mode 100644 index 0000000..5217795 --- /dev/null +++ b/MathEngine/EngineTests/EngineTests.csproj @@ -0,0 +1,22 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + + + + diff --git a/MathEngine/EngineTests/Parser Tests/ExpressionTreeTests.cs b/MathEngine/EngineTests/Parser Tests/ExpressionTreeTests.cs new file mode 100644 index 0000000..08a1862 --- /dev/null +++ b/MathEngine/EngineTests/Parser Tests/ExpressionTreeTests.cs @@ -0,0 +1,59 @@ +using MathEngine.Parser.Tokeniser; +using MathEngine.Parser.Parser; +namespace EngineTests +{ + /// + /// Class for testing the ExpressionTree Class + /// + [TestClass] + public class ExpressionTreeTests + { + + /// + /// Test to see if a simple expression is constructed correctly + /// + [TestMethod] + public void TestExpressionTreeSimpleExpression() + { + 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 ExpressionTree(testExp); + Assert.IsTrue(returnedTree.Equals(exptectedTree)); + } + + /// + /// Test to see if a simple expression is evaluated correctly + /// + [TestMethod] + public void TestExpressionTreeSimpleExpressionEvaluation() + { + string testExp = "3+4*7"; + Token tok31 = new("31", Token.Type.Numeric, Token.NumericType.Decimal, 0); + TreeNode exptectedTree = new(tok31); + ExpressionTree returnedTree = new ExpressionTree(testExp); + ExpressionTree evaluatedTree = returnedTree.Evaluate(); + Assert.IsTrue(evaluatedTree.Equals(exptectedTree)); + } + + /// + /// Test to see if a simple expression using all base operators (+,-,*,/) is evaluated correctly + /// + [TestMethod] + public void TestExpressionTreeSimpleExpressionAllBaseOperatorsEvaluation() + { + string testExp = "3+4*7-8/7"; + decimal testValue = decimal.Divide(209 , 7); + Token tok31 = new(testValue.ToString(), Token.Type.Numeric, Token.NumericType.Decimal, 0); + TreeNode exptectedTree = new(tok31); + ExpressionTree returnedTree = new ExpressionTree(testExp); + ExpressionTree evaluatedTree = returnedTree.Evaluate(); + Assert.IsTrue(evaluatedTree.Equals(exptectedTree)); + } + } +} \ No newline at end of file diff --git a/MathEngine/EngineTests/Parser Tests/ParserTests.cs b/MathEngine/EngineTests/Parser Tests/ParserTests.cs new file mode 100644 index 0000000..4975f79 --- /dev/null +++ b/MathEngine/EngineTests/Parser Tests/ParserTests.cs @@ -0,0 +1,92 @@ +using MathEngine.Parser.Tokeniser; +using MathEngine.Parser.Parser; +namespace EngineTests +{ + /// + /// Class for testing the Parser + /// + [TestClass] + public class ParserTests + { + /// + /// Test the Parser on a basic List of tokens + /// + [TestMethod] + public void TestParserBasicExpression() + { + //Arrange + string testString = "3+4"; + List 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 expectedStack = new(); + expectedStack.Push(Token.Plus); + expectedStack.Push(four); + expectedStack.Push(three); + //Act + Stack 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(); + } + } + } + } + + /// + /// Test the Parser on a more compilicated basic expression to see if operator precedence is respected + /// + [TestMethod] + public void TestParserBasicExpressionAllOperators() + { + //Arrange + string testString = "3+4*8-47.2/9"; + List 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 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 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(); + } + } + } + } + + + } +} diff --git a/MathEngine/EngineTests/Parser Tests/TokenIserTests.cs b/MathEngine/EngineTests/Parser Tests/TokenIserTests.cs new file mode 100644 index 0000000..e0c2911 --- /dev/null +++ b/MathEngine/EngineTests/Parser Tests/TokenIserTests.cs @@ -0,0 +1,65 @@ +using MathEngine.Parser.Tokeniser; + +namespace EngineTests +{ + /// + /// Class for testing the Tokeniser + /// + [TestClass] + public class TokeniserTests + { + /// + /// Test the tokeniser on a basic string + /// + [TestMethod] + public void TestTokeniseBasicString() + { + //Arrange + string testString = "1+1"; + Token one = new("1", Token.Type.Numeric, Token.NumericType.Decimal, 0); + List expectedValue = new() + { + one, + Token.Plus, + one + }; + //Act + List returnedValue = Tokeniser.Tokenise(testString); + //Assert + Assert.IsTrue(expectedValue.SequenceEqual(returnedValue)); + } + + /// + /// Test the tokeniser on a basic string, but with significant ammounts of whitespace + /// + [TestMethod] + public void TestTokeniseBasicStringWithWhiteSpace() + { + //Arrange + string testString = " 1 + 1 "; + Token one = new("1", Token.Type.Numeric, Token.NumericType.Decimal, 0); + List expectedValue = new() + { + one, + Token.Plus, + one + }; + //Act + List returnedValue = Tokeniser.Tokenise(testString); + //Assert + Assert.IsTrue(expectedValue.SequenceEqual(returnedValue)); + } + + /// + /// Test the tokeniser on a string which contains a number which is not formatted correctly + /// + [TestMethod] + public void TestTokeniseStringWithInvalidNumbr() + { + //Arrange + string testString = "1+11.2.5"; + //Act and Assert + Assert.ThrowsException(() => Tokeniser.Tokenise(testString)); + } + } +} \ No newline at end of file diff --git a/MathEngine/EngineTests/Parser Tests/TreeNodeTests.cs b/MathEngine/EngineTests/Parser Tests/TreeNodeTests.cs new file mode 100644 index 0000000..7bca15a --- /dev/null +++ b/MathEngine/EngineTests/Parser Tests/TreeNodeTests.cs @@ -0,0 +1,30 @@ +using MathEngine.Parser.Tokeniser; +using MathEngine.Parser.Parser; +namespace EngineTests +{ + /// + /// Class for testing the TreeNode class + /// + [TestClass] + public class TreeNodeTests + { + + /// + /// Test to see if a simple expression is constructed correctly + /// + [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)); + } + } +} diff --git a/MathEngine/EngineTests/Usings.cs b/MathEngine/EngineTests/Usings.cs new file mode 100644 index 0000000..ab67c7e --- /dev/null +++ b/MathEngine/EngineTests/Usings.cs @@ -0,0 +1 @@ +global using Microsoft.VisualStudio.TestTools.UnitTesting; \ No newline at end of file diff --git a/MathEngine/MathEngine.sln b/MathEngine/MathEngine.sln new file mode 100644 index 0000000..3c30a4b --- /dev/null +++ b/MathEngine/MathEngine.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32616.157 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MathEngine", "MathEngine\MathEngine.csproj", "{E4A483AB-44FC-4386-A509-C612FE6E6C8A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EngineTests", "EngineTests\EngineTests.csproj", "{096BD3DE-E398-42AD-875F-6BEA469ED78F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E4A483AB-44FC-4386-A509-C612FE6E6C8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E4A483AB-44FC-4386-A509-C612FE6E6C8A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E4A483AB-44FC-4386-A509-C612FE6E6C8A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E4A483AB-44FC-4386-A509-C612FE6E6C8A}.Release|Any CPU.Build.0 = Release|Any CPU + {096BD3DE-E398-42AD-875F-6BEA469ED78F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {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 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {DF90889C-36A5-4730-82C6-91E0B39FDF91} + EndGlobalSection +EndGlobal diff --git a/MathEngine/MathEngine/MathEngine.csproj b/MathEngine/MathEngine/MathEngine.csproj new file mode 100644 index 0000000..b9af703 --- /dev/null +++ b/MathEngine/MathEngine/MathEngine.csproj @@ -0,0 +1,14 @@ + + + + net6.0 + enable + enable + + + + <_Parameter1>EngineTests + + + + diff --git a/MathEngine/MathEngine/Parser/Parser/ExpressionTree.cs b/MathEngine/MathEngine/Parser/Parser/ExpressionTree.cs new file mode 100644 index 0000000..9c09c54 --- /dev/null +++ b/MathEngine/MathEngine/Parser/Parser/ExpressionTree.cs @@ -0,0 +1,211 @@ +using System.Collections.Generic; +using MathEngine.Parser.Tokeniser; +namespace MathEngine.Parser.Parser +{ + /// + /// Represents an Abstract Syntax tree for expresison evaluation + /// + internal class ExpressionTree + { + /// + /// The root node of the expression tree; + /// + private readonly TreeNode? rootNode; + + /// + /// Initialises a new instance of the MathEngine.Parser.Parser.Node class with a given Token + /// The token for the nodes value + /// + /// + public ExpressionTree(string Expression) + { + List tokens = Tokeniser.Tokeniser.Tokenise(Expression); + Stack rpnForm = Parser.Parse(tokens); + rootNode = GenerateExpressionTree(rpnForm); + } + + private ExpressionTree(TreeNode rootNode) + { + this.rootNode = rootNode; + } + + /// + /// Creates a binary TreeNode, that is a node with a root value and two children + /// + /// The token to be the root node of the TreeNode + /// TreeNode that is the left branch of the current node + /// TreeNode that is the right branch of the current node + /// A TreeNode with CurrentToken as the root value and LeftBranch and RightBranch as Children + private static TreeNode CreateBinaryNode(Token CurrentToken, TreeNode LeftBranch, TreeNode RightBranch) + { + TreeNode root = new(CurrentToken); + root.AddChildNode(LeftBranch); + root.AddChildNode(RightBranch); + return root; + } + + /// + /// Creates a unary TreeNode, that is a node with a root value and two children + /// + /// The token to be the root node of the TreeNode + /// TreeNode that is the child of the current node + /// A TreeNode with CurrentToken as the root value and ChildNode as the sole child node + private static TreeNode CreateUnaryNode(Token CurrentToken, TreeNode ChildNode) + { + TreeNode root = new(CurrentToken); + root.AddChildNode(ChildNode); + return root; + } + + /// + /// Generates the full expression tree given an RPN expression stack + /// + /// RPN expression stack to generate an expression tree from + /// An expression Tree that represents the Mathematical expression given by rpnExpression + private static TreeNode? GenerateExpressionTree(Stack rpnExpression) + { + Stack OutputStack = new(rpnExpression.Count); + TreeNode Node; + Token CurrentToken; + if (rpnExpression.Count == 0) + { + return null; + } + else + { + while (rpnExpression.Count != 0) + { + CurrentToken = rpnExpression.Pop(); + switch (CurrentToken.Token_Type) + { + case Token.Type.Numeric: + Node = new TreeNode(CurrentToken); + OutputStack.Push(Node); + break; + case Token.Type.Addition: // We need to preserve "Left handness" 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.Subtraction: + case Token.Type.Multiplication: + case Token.Type.Division: + case Token.Type.Exponentiation: + TreeNode Right = OutputStack.Pop(); + TreeNode Left = OutputStack.Pop(); + Node = CreateBinaryNode(CurrentToken, Left, Right); + OutputStack.Push(Node); + break; + case Token.Type.UnaryPlus: + case Token.Type.UnaryMinus: + Node = CreateUnaryNode(CurrentToken, OutputStack.Pop()); + OutputStack.Push(Node); + break; + } + } + return OutputStack.Pop(); + } + } + + + /// + /// Evaluates branches of a given tree + /// + /// + /// + private static TreeNode Evaluate_Tree_Branch(TreeNode Branch) + { + TreeNode Root, LeftBranch, RightBranch; + if (Branch.NodeValue.Token_Type == Token.Type.Numeric) + return Branch; + else + { + LeftBranch = Evaluate_Tree_Branch(Branch.GetChildNode(0)); + RightBranch = Evaluate_Tree_Branch(Branch.GetChildNode(1)); + // We finally combine the computed branches with the operator that links them and return the result + Root = Evaluate_Operator(Branch.NodeValue, LeftBranch, RightBranch); + return Root; + } + } + + /// + /// Evlautes a binary node where the root node is an operator and given two branches the left and the right + /// + /// + /// + /// + /// + private static TreeNode Evaluate_Operator(Token Operator_Token, TreeNode Left_Branch, TreeNode Right_Branch) + { + decimal lhs = decimal.Parse(Left_Branch.NodeValue.TokenValue); + decimal rhs = decimal.Parse(Right_Branch.NodeValue.TokenValue); + return Operator_Token.Token_Type switch + { + Token.Type.Addition => new TreeNode(new Token((lhs + rhs).ToString(), Token.Type.Numeric, Token.NumericType.Decimal, 0)), + Token.Type.Subtraction => new TreeNode(new Token((lhs - rhs).ToString(), Token.Type.Numeric, Token.NumericType.Decimal, 0)), + Token.Type.Multiplication => new TreeNode(new Token(((decimal)(lhs * rhs)).ToString(), Token.Type.Numeric, Token.NumericType.Decimal, 0)), + Token.Type.Division => new TreeNode(new Token(((decimal)(lhs / rhs)).ToString(), Token.Type.Numeric, Token.NumericType.Decimal, 0)), + _ => throw new Exception("Potentially invalid token?"), + }; + } + + /// + /// Evaluates the current instance of ExpressionTree + /// + /// Returns an update of the current instance which the expression Evaluated + public ExpressionTree? Evaluate() + { + if (rootNode == null) + { + return null; + } + else // To evaluate we go anti-clockwise around the tree + { + TreeNode? Root, LeftBranch, RightBranch; + if (rootNode.NodeValue.Token_Type == Token.Type.Numeric) + return this; + else + { + LeftBranch = Evaluate_Tree_Branch(rootNode.GetChildNode(0)); + RightBranch = Evaluate_Tree_Branch(rootNode.GetChildNode(1)); + Root = Evaluate_Operator(rootNode.NodeValue, LeftBranch, RightBranch); + return new ExpressionTree(Root); + } + } + } + + /// + /// Returns a value indicating if the given object is equal to the current instance of ExpressionTree + /// + /// The object to compare to the current instance + /// + public override bool Equals(object? other) + { + if (other is TreeNode) + { + ExpressionTree otherTree = new((TreeNode)other); + return this.Equals(otherTree); + } + return false; + } + + /// + /// Compares the current ExpressionTree instance for equality with the given ExpressionTree + /// + /// The ExpressionTree to compare to the current instance + /// True if the expression trees are equal and False otherwise + 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(this.rootNode); + } + } +} diff --git a/MathEngine/MathEngine/Parser/Parser/Node/TreeNode.cs b/MathEngine/MathEngine/Parser/Parser/Node/TreeNode.cs new file mode 100644 index 0000000..bc78b6d --- /dev/null +++ b/MathEngine/MathEngine/Parser/Parser/Node/TreeNode.cs @@ -0,0 +1,153 @@ +using MathEngine.Parser.Tokeniser; +namespace MathEngine.Parser.Parser +{ + /// + /// Represents a node in a Tree structure + /// + internal class TreeNode + { + private TreeNode? Parent; + private List? Children; + private readonly Token Value; + + /// + /// Initialises a new instance of the MathEngine.Parser.Parser.Node class with a given Token + /// + /// The token for the nodes value + public TreeNode(Token value) + { + Parent = null; + Children = null; + Value = value; + } + + /// + /// Returns the value of the node + /// + public Token NodeValue + { + get { return Value; } + } + + /// + /// Returns the parent node of the current node, or null if it does not exist + /// + public TreeNode? ParentNode + { + get + { + if (Parent == null) + { + return null; + } + else + { + return Parent; + } + } + } + + /// + /// Returns all of the child nodes of the current node, or null if it odes not exist + /// + public List? GetChildrenNodes + { + get + { + if (Children == null) + { + return null; + } + else + { + return Children; + } + } + } + + /// + /// Returns the child node specified by the index, if there are no children nodes or if the index is out of bounds than null is returned + /// + /// + /// + public TreeNode? GetChildNode(int index) + { + if (Children == null) + { + return null; + } + if (index < 0 || index >= Children.Count) + { + return null; + } + + return Children[index]; + } + + /// + /// Adds a child node to the current root node, if there are no children nodes a list is created + /// + /// The value for the child node that is to be added + public void AddChildNode(TreeNode Node) + { + if (Children == null) + { + Children = new() + { + Node + }; + } + else + { + Children.Add(Node); + } + } + + /// + /// Returns a value that indicates if the given object is equal to the current instance of TreeNode + /// + /// The object to compare to the current instance of TreeNode + /// + public override bool Equals(object? other) + { + if (other is ExpressionTree) + { + return other.Equals(this); + } + return false; + } + + public bool Equals(TreeNode other) + { + if (this.Value != other.Value) //If the root values are not equal we are done + { + return false; + } + // otherwise, + if (this.Children != null && other.Children != null) // If both children are NOT null then we reursively check the child nodes + { + //Covered all nullable cases, we now need to recursively check the child nodes + if (Children.Count != other.Children.Count) + { + return false; + } + for (int childNodeIndex = 0; childNodeIndex < Children.Count; childNodeIndex++) + { + if (!Children[childNodeIndex].Equals(other.Children[childNodeIndex])) + { + return false; + } + } + return true; + } + else if (this.Children == null && other.Children == null) //Special case is if both children lists are null then the TreeNodes are equal + { + return true; + } + else // otherwise at least one is null and the other is not so they can't be equal + { + return false; // if both children are not null than at least one is null so they can't be equal + } + } + } +} diff --git a/MathEngine/MathEngine/Parser/Parser/Parser.cs b/MathEngine/MathEngine/Parser/Parser/Parser.cs new file mode 100644 index 0000000..f4b7a26 --- /dev/null +++ b/MathEngine/MathEngine/Parser/Parser/Parser.cs @@ -0,0 +1,142 @@ +using MathEngine.Parser.Tokeniser; +namespace MathEngine.Parser.Parser +{ + /// + /// Represents the conversion from a list of Tokens representing Mathematical expression to List in Reverse Polish Notation form + /// + internal class Parser + { + + /// + /// Return the Precdence of a given token operator + /// + /// Token to get Precdence of + /// + 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); + } + } + } + + /// + /// Is the operation left associative + /// + /// Operation to check + /// + 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"); + + } + } + } + + /// + /// ''' Reverse the order of a given stack of Tokens + /// ''' + /// ''' Stack to reverse + /// ''' + private static Stack ReverseStackTok(Stack Stack) + { + Stack OutputStack = new (Stack.Count); + while ((Stack.Count != 0)) + OutputStack.Push(Stack.Pop()); + return OutputStack; + } + + /// + /// Parses a list of tokens into a valid RPN expression stack + /// + /// List of tokens to parse + /// Returns the Reverse polish notation form of the expression + static internal Stack Parse(List Expression) + { + //Temp holding stack for operators + Stack OperatorStack = new(Expression.Count/2); + //The final stack to return + Stack OutputStack = new(Expression.Count); + //Stack used to hold the number of input params to a function + //Stack ArityStack = new Stack(); + 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: + 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; + } + } + + 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); + } + } +} diff --git a/MathEngine/MathEngine/Parser/Tokeniser/Token.cs b/MathEngine/MathEngine/Parser/Tokeniser/Token.cs new file mode 100644 index 0000000..52f3ce3 --- /dev/null +++ b/MathEngine/MathEngine/Parser/Tokeniser/Token.cs @@ -0,0 +1,244 @@ +namespace MathEngine.Parser.Tokeniser +{ + /// + /// Defines the Token Type. The base for all manipulations + /// + internal struct Token + { + /// + /// Represents the token for + + /// + public static readonly Token Plus = new("+",Type.Addition,NumericType.NaN,0); + + /// + /// Represents the token for - + /// + public static readonly Token Minus = new("-", Type.Subtraction, NumericType.NaN, 0); + + /// + /// Represents the token for * + /// + public static readonly Token Multiply = new("*", Type.Multiplication, NumericType.NaN, 0); + + /// + /// Represents the token for / + /// + public static readonly Token Divide = new("/", Type.Division, NumericType.NaN, 0); + + /// + /// Enum representing the token type + /// + internal enum Type + { + Numeric, + DecimalPoint, + Addition, + Subtraction, + Multiplication, + Division, + Exponentiation, + UnaryPlus, + UnaryMinus, + Operator, + Variable, + Function, + FunctionArgumentSeparator, + LeftParenthesis, + RightParenthesis, + OpenBracket, + CloseBracket, + OpenBrace, + CloseBrace + } + + /// + /// Enum representing the numerical type of the token + /// + internal enum NumericType + { + Integer, + Decimal, + Complex, + NaN + } + + /// + /// String representing the value of the token + /// + private readonly string Value; + + /// + /// The type of token + /// + private readonly Type TokenType; + + /// + /// The numeric type of the token + /// + private readonly NumericType Numeric_Type; + + /// + /// The arity of the token + /// + private readonly uint Arity; + + #region "Properties" + + /// + /// Returns the value of the Token + /// + public string TokenValue + { + get { return Value; } + } + + /// + /// Returns the type of the token + /// + public readonly Type Token_Type + { + get { return TokenType; } + } + + /// + /// Returns the numerical type of the token + /// + public readonly NumericType NumericalType + { + get { return Numeric_Type; } + } + + /// + /// Returns the arity of the token + /// + public uint FunctionArity + { + get { return Arity; } + } + + #endregion + + /// + /// Initializes a new instance of the Tokeniser.Token structure with a given TokenValue, TokenType, TokenNumericType and Arity + /// + /// String representing the value of the token + /// The type that the token instance represents + /// The numeric type of the token + /// The token Arity + public Token(string TokenValue, Type TokenType, NumericType TokenNumericType, uint FunctionArity = 0) + { + this.Value = TokenValue; + this.TokenType = TokenType; + this.Numeric_Type = TokenNumericType; + this.Arity = FunctionArity; + } + +#if DEBUG + /// + /// Debug String; Used to give a string representation of a token + /// + /// + public new string ToString() + { + return Value + "," + TokenType.ToString() + "," + Numeric_Type.ToString() + "," + Arity.ToString(); + } +#endif + + /// + /// Returns a value that indicates whether a two Tokens are equal. + /// + /// First Token to compare + /// Second Token to compare + /// Returns true if the two Tokens are equal and false otherwise + 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; + } + + /// + /// Returns a value that indicates whether a two Tokens are not equal. + /// + /// First Token to compare + /// Second Token to compare + /// Returns true if the two Tokens are not equal and false otherwise + 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; + } + + 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; + } + + public override bool Equals(object? obj) + { + if (obj is Token) + { + Token other = (Token)obj; + return this.Equals(other); + } + else + { + return false; + } + } + + /// + /// Calculates the HashCode for the current Token Instance + /// + /// + public override int GetHashCode() + { + return HashCode.Combine(Value, TokenType, Numeric_Type, Arity); + } + } +} \ No newline at end of file diff --git a/MathEngine/MathEngine/Parser/Tokeniser/Tokeniser.cs b/MathEngine/MathEngine/Parser/Tokeniser/Tokeniser.cs new file mode 100644 index 0000000..46de9f2 --- /dev/null +++ b/MathEngine/MathEngine/Parser/Tokeniser/Tokeniser.cs @@ -0,0 +1,110 @@ +namespace MathEngine.Parser.Tokeniser +{ + /// + /// Represents the conversion of a Mathematical expression in string form to a List of Tokens + /// + static internal class Tokeniser + { + private static readonly List Operators = new(new char[] { '+', '-', '*', '/', '^' }); + + /// + /// Gets the next non-whitespace char or returns the null terminator is at EOS (End of stream) + /// + static private char GetNextChar(string Expresison, ref Int32 currentIndex) + { + char curChar; + do + { + currentIndex++; + curChar = currentIndex >= Expresison.Length ? '\0' : Expresison[currentIndex]; + + } while (char.IsWhiteSpace(curChar)); + return curChar; + } + + /// + /// Returns the token that represents the current character + /// + /// The character to get the token of + /// A token representing the current character, otherwise an exception is thrown + static private Token GetOperatorToken(char curChar) + { + return curChar switch + { + '+' => Token.Plus, + '-' => Token.Minus, + '*' => Token.Multiply, + '/' => Token.Divide, + _ => throw new Exception(String.Format("Character {0} is not a defined operator", curChar)), + }; + } + + /// + /// Tokenises a given Mathematical expression given as a string to a list of tokens + /// + /// Expression to tokenise + /// A list of tokens representing the given expression, if the expression string is null or empty then an empty list is returned + static internal List Tokenise(string Expression) + { + if (string.IsNullOrEmpty(Expression)) + { + return new List { }; + } + else + { + //Cleanup whitespace + Expression = String.Concat(Expression.Where(c => !Char.IsWhiteSpace(c))); + Int32 currentIndex = -1; + //Example expression 1+1+Sin[x] + List 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 + } + //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 + } + } + + //return the stack after triming + Tokenstack.TrimExcess(); + + return Tokenstack; + } + } + } +}