From a555a131af3b967b4e765eb35e6d67f6c574ad7c Mon Sep 17 00:00:00 2001 From: 0xJ1M <112640460+0xJ1M@users.noreply.github.com> Date: Fri, 8 Sep 2023 17:53:13 +0100 Subject: [PATCH] Feature/basic evaluator (#2) * Refactor * Updated TreeNode system to use abstract base class and inheritence * Updated unit test coverage * Improved code coverage --- MathEngine/EngineTests/EngineTests.csproj | 8 +- .../Parser Tests/ExpressionTreeTests.cs | 28 ++- .../Parser Tests/TokenIserTests.cs | 65 ------- MathEngine/MathEngine/Evaluator/Evaluator.cs | 74 ++++++++ MathEngine/MathEngine/MathEngine.csproj | 7 +- .../Parser/Parser/ExpressionTree.cs | 162 +++--------------- .../MathEngine/Parser/Parser/Node/TreeNode.cs | 158 ----------------- .../MathEngine/Parser/Parser/TreeGenerator.cs | 52 ++++++ .../MathEngine/Parser/Tokeniser/Token.cs | 18 +- 9 files changed, 180 insertions(+), 392 deletions(-) delete mode 100644 MathEngine/EngineTests/Parser Tests/TokenIserTests.cs create mode 100644 MathEngine/MathEngine/Evaluator/Evaluator.cs delete mode 100644 MathEngine/MathEngine/Parser/Parser/Node/TreeNode.cs create mode 100644 MathEngine/MathEngine/Parser/Parser/TreeGenerator.cs diff --git a/MathEngine/EngineTests/EngineTests.csproj b/MathEngine/EngineTests/EngineTests.csproj index 5217795..8f8f0fc 100644 --- a/MathEngine/EngineTests/EngineTests.csproj +++ b/MathEngine/EngineTests/EngineTests.csproj @@ -1,13 +1,19 @@ - net6.0 + net7.0 enable enable false + + True + + + + diff --git a/MathEngine/EngineTests/Parser Tests/ExpressionTreeTests.cs b/MathEngine/EngineTests/Parser Tests/ExpressionTreeTests.cs index a51bb01..e42709b 100644 --- a/MathEngine/EngineTests/Parser Tests/ExpressionTreeTests.cs +++ b/MathEngine/EngineTests/Parser Tests/ExpressionTreeTests.cs @@ -16,15 +16,8 @@ namespace EngineTests 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(testExp); - Assert.IsTrue(returnedTree.Equals(exptectedTree)); + Assert.IsTrue(returnedTree.ToString() == "7"); } /// @@ -34,11 +27,8 @@ namespace EngineTests 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(testExp); - ExpressionTree? evaluatedTree = returnedTree.Evaluate(); - Assert.IsTrue(evaluatedTree.Equals(exptectedTree)); + Assert.IsTrue(returnedTree.ToString() == "31"); } /// @@ -49,11 +39,17 @@ namespace EngineTests { 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(testExp); - ExpressionTree? evaluatedTree = returnedTree.Evaluate(); - Assert.IsTrue(evaluatedTree.Equals(exptectedTree)); + 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)); } } } \ No newline at end of file diff --git a/MathEngine/EngineTests/Parser Tests/TokenIserTests.cs b/MathEngine/EngineTests/Parser Tests/TokenIserTests.cs deleted file mode 100644 index e0c2911..0000000 --- a/MathEngine/EngineTests/Parser Tests/TokenIserTests.cs +++ /dev/null @@ -1,65 +0,0 @@ -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/MathEngine/Evaluator/Evaluator.cs b/MathEngine/MathEngine/Evaluator/Evaluator.cs new file mode 100644 index 0000000..7496f40 --- /dev/null +++ b/MathEngine/MathEngine/Evaluator/Evaluator.cs @@ -0,0 +1,74 @@ +using MathEngine.Parser.Parser; +using MathEngine.Parser.Tokeniser; + +namespace MathEngine.Parser.Evaluator +{ + /// + /// Class that evaluate TreeNodes + /// + internal class Evaluator + { + /*public static TreeNode? Evaluate(TreeNode rootNode) + { + 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 rootNode; + else + { //For now these can't be null, this will need to be updated in the future + LeftBranch = Evaluate_Tree_Branch(rootNode.GetChildNode(0)); + RightBranch = Evaluate_Tree_Branch(rootNode.GetChildNode(1)); + Root = Evaluate_Operator(rootNode.NodeValue, LeftBranch, RightBranch); + return Root; + } + } + } + + + /// + /// Evaluates branches of a given tree + /// + /// The TreeNode branch to evaluate + /// Returns the Evaluation of the Branch + 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 + /// + /// + /// + /// + /// Returns the evaluated value of the operator as a TreeNode + 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?"), + }; + }*/ + } +} diff --git a/MathEngine/MathEngine/MathEngine.csproj b/MathEngine/MathEngine/MathEngine.csproj index b9af703..7824531 100644 --- a/MathEngine/MathEngine/MathEngine.csproj +++ b/MathEngine/MathEngine/MathEngine.csproj @@ -1,10 +1,15 @@  - net6.0 + net7.0 enable enable + True + + + + <_Parameter1>EngineTests diff --git a/MathEngine/MathEngine/Parser/Parser/ExpressionTree.cs b/MathEngine/MathEngine/Parser/Parser/ExpressionTree.cs index 18ba17a..91f3e80 100644 --- a/MathEngine/MathEngine/Parser/Parser/ExpressionTree.cs +++ b/MathEngine/MathEngine/Parser/Parser/ExpressionTree.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using MathEngine.Parser.Tokeniser; +using MathEngine.Parser.Tokeniser; namespace MathEngine.Parser.Parser { /// @@ -10,7 +9,8 @@ namespace MathEngine.Parser.Parser /// /// The root node of the expression tree; /// - private readonly TreeNode rootNode; + private readonly BaseNode rootNode; + private readonly BaseNode? evaluated_expression; /// /// Initialises a new instance of the MathEngine.Parser.Parser.Node class with a given Token @@ -21,161 +21,34 @@ namespace MathEngine.Parser.Parser { List tokens = Tokeniser.Tokeniser.Tokenise(Expression); Stack rpnForm = Parser.Parse(tokens); - rootNode = GenerateExpressionTree(rpnForm); + rootNode = TreeGenerator.TreeFromRPN(rpnForm); + evaluated_expression = rootNode.Evaluate(); } - private ExpressionTree(TreeNode rootNode) + private ExpressionTree(BaseNode 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; - while (rpnExpression.Count != 0) - { - CurrentToken = rpnExpression.Pop(); - switch (CurrentToken.Token_Type) - { - case Token.Type.Numeric: - Node = new TreeNode(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: - 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 - /// - /// The TreeNode branch to evaluate - /// Returns the Evaluation of the Branch - 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 - /// - /// - /// - /// - /// Returns the evaluated value of the operator as a TreeNode - 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() + 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 - { //For now these can't be null, this will need to be updated in the future - 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); - } - } - } + return new ExpressionTree(rootNode.Evaluate()); + }*/ - /// + /// /// Returns a value indicating if the given object is equal to the current instance of ExpressionTree /// /// The object to compare to the current instance /// True if they are equal, False otherwise - public override bool Equals(object? other) + public override bool Equals(object? other) { - if (other is TreeNode) + if (other is BaseNode) { - ExpressionTree otherTree = new((TreeNode)other); + ExpressionTree otherTree = new((BaseNode)other); return this.Equals(otherTree); } return false; @@ -201,7 +74,12 @@ namespace MathEngine.Parser.Parser public override int GetHashCode() { - return System.HashCode.Combine(this.rootNode); + return System.HashCode.Combine(rootNode, evaluated_expression); + } + + public override string ToString() + { + return evaluated_expression.ToString(); } } } diff --git a/MathEngine/MathEngine/Parser/Parser/Node/TreeNode.cs b/MathEngine/MathEngine/Parser/Parser/Node/TreeNode.cs deleted file mode 100644 index da26994..0000000 --- a/MathEngine/MathEngine/Parser/Parser/Node/TreeNode.cs +++ /dev/null @@ -1,158 +0,0 @@ -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 - /// - /// The index of the child node to get - /// The ChildNode at the specified index, null if it is null or the index is out-of-bounds - 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 - /// True if the object innstace is equal to the current ExpressionTree instance, False otherwise - public override bool Equals(object? other) - { - if (other is ExpressionTree) - { - return other.Equals(this); - } - return false; - } - - /// - /// Returns a value that indicates if the given TreeNode instance is equal to another - /// - /// The TreeNode to check for equality - /// True if they are equal, False otherwise - 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/TreeGenerator.cs b/MathEngine/MathEngine/Parser/Parser/TreeGenerator.cs new file mode 100644 index 0000000..11da6f6 --- /dev/null +++ b/MathEngine/MathEngine/Parser/Parser/TreeGenerator.cs @@ -0,0 +1,52 @@ +using MathEngine.Parser.Tokeniser; + +namespace MathEngine.Parser.Parser +{ + /// + /// Class for converting from RPN form to an expression tree + /// + internal class TreeGenerator + { + /// + /// 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 + public static BaseNode TreeFromRPN(Stack rpnExpression) + { + Stack OutputStack = new(rpnExpression.Count); + BaseNode Node; + Token CurrentToken; + while (rpnExpression.Count != 0) + { + CurrentToken = rpnExpression.Pop(); + switch (CurrentToken.Token_Type) + { + case Token.Type.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: + 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: + //Node = NodeFactory.CreateUnaryNode(CurrentToken, OutputStack.Pop()); + //OutputStack.Push(Node); + break; + } + } + return OutputStack.Pop(); + } + } +} diff --git a/MathEngine/MathEngine/Parser/Tokeniser/Token.cs b/MathEngine/MathEngine/Parser/Tokeniser/Token.cs index 0e05502..52ae344 100644 --- a/MathEngine/MathEngine/Parser/Tokeniser/Token.cs +++ b/MathEngine/MathEngine/Parser/Tokeniser/Token.cs @@ -179,23 +179,23 @@ /// 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) + if (X.TokenValue != Y.TokenValue) { - return false; + return true; } - if (X.TokenType == Y.TokenType) + if (X.TokenType != Y.TokenType) { - return false; + return true; } - if (X.NumericalType == Y.NumericalType) + if (X.NumericalType != Y.NumericalType) { - return false; + return true; } - if (X.Arity == Y.Arity) + if (X.Arity != Y.Arity) { - return false; + return true; } - return true; + return false; } ///