diff --git a/HW2/HW2.sln b/HW2/HW2.sln new file mode 100644 index 0000000..c5d8b5c --- /dev/null +++ b/HW2/HW2.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SyntaxTree", "SyntaxTree\SyntaxTree.csproj", "{6C1B7790-DBF9-4146-A87B-83D95A0D5C54}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SyntaxTree.Tests", "SyntaxTree.Tests\SyntaxTree.Tests.csproj", "{29D5F19D-35BC-49BE-8724-369258B5717D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6C1B7790-DBF9-4146-A87B-83D95A0D5C54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6C1B7790-DBF9-4146-A87B-83D95A0D5C54}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6C1B7790-DBF9-4146-A87B-83D95A0D5C54}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6C1B7790-DBF9-4146-A87B-83D95A0D5C54}.Release|Any CPU.Build.0 = Release|Any CPU + {29D5F19D-35BC-49BE-8724-369258B5717D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {29D5F19D-35BC-49BE-8724-369258B5717D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {29D5F19D-35BC-49BE-8724-369258B5717D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {29D5F19D-35BC-49BE-8724-369258B5717D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/HW2/SyntaxTree.Tests/SyntaxTree.Tests.csproj b/HW2/SyntaxTree.Tests/SyntaxTree.Tests.csproj new file mode 100644 index 0000000..699997e --- /dev/null +++ b/HW2/SyntaxTree.Tests/SyntaxTree.Tests.csproj @@ -0,0 +1,44 @@ + + + + net9.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + Always + + + diff --git a/HW2/SyntaxTree.Tests/SyntaxTreeTests.cs b/HW2/SyntaxTree.Tests/SyntaxTreeTests.cs new file mode 100644 index 0000000..b92e73f --- /dev/null +++ b/HW2/SyntaxTree.Tests/SyntaxTreeTests.cs @@ -0,0 +1,155 @@ +// +// Copyright (c) khusainovilas. All rights reserved. +// + +namespace SyntaxTree.Tests; + +/// +/// tests for syntax tree. +/// +public class SyntaxTreeTests +{ + private StringWriter consoleOutput; + + [SetUp] + public void SetUp() + { + this.consoleOutput = new StringWriter(); + Console.SetOut(this.consoleOutput); + } + + [TearDown] + public void TearDown() + { + this.consoleOutput.Dispose(); + Console.SetOut(new StreamWriter(Console.OpenStandardOutput()) { AutoFlush = true }); + } + + /// + /// Test parsing a valid expression with multiplication and addition. + /// + [Test] + public void Parser_Parse_ValidExpression_ReturnsCorrectTreeAndResult() + { + const string input = "(* (+ 1 1) 2)"; + var tree = Parser.Parse(input); + Assert.Multiple(() => + { + Assert.That(tree.ToStringRepresentation(), Is.EqualTo("(* (+ 1 1) 2)")); + Assert.That(tree.Calculate(), Is.EqualTo(4)); + }); + } + + /// + /// Test parsing an expression with negative numbers. + /// + [Test] + public void Parser_Parse_NegativeNumber_ReturnsCorrectResult() + { + const string input = "(+ -5 3)"; + var tree = Parser.Parse(input); + Assert.Multiple(() => + { + Assert.That(tree.ToStringRepresentation(), Is.EqualTo("(+ -5 3)")); + Assert.That(tree.Calculate(), Is.EqualTo(-2)); + }); + } + + /// + /// Test parsing an empty input throws ArgumentException. + /// + [Test] + public void Parser_Parse_EmptyInput_ThrowsArgumentException() + { + Assert.Throws(() => Parser.Parse(string.Empty)); + } + + /// + /// Test parsing an invalid token throws ArgumentException. + /// + [Test] + public void Parser_Parse_InvalidToken_ThrowsArgumentException() + { + Assert.Throws(() => Parser.Parse("(+ 1 a)")); + } + + /// + /// Test parsing unbalanced parentheses throws ArgumentException. + /// + [Test] + public void Parser_Parse_UnbalancedParentheses_ThrowsArgumentException() + { + Assert.Throws(() => Parser.Parse("(( 1 2)")); + } + + /// + /// Test parsing division by zero throws DivideByZeroException in Calculate. + /// + [Test] + public void Parser_Parse_DivisionByZero_ThrowsDivideByZeroException() + { + const string input = "(/ 10 0)"; + var tree = Parser.Parse(input); + Assert.That(tree.ToStringRepresentation(), Is.EqualTo("(/ 10 0)")); + Assert.Throws(() => tree.Calculate()); + } + + /// + /// Test NumberNode calculation and string representation. + /// + [Test] + public void NumberNode_CalculateAndToString_ReturnsCorrectValues() + { + var node = new NumberNode(-5); + Assert.Multiple(() => + { + Assert.That(node.Calculate(), Is.EqualTo(-5)); + Assert.That(node.ToStringRepresentation(), Is.EqualTo("-5")); + }); + } + + /// + /// Test that Add node calculates and prints correctly. + /// + [Test] + public void Add_CalculateAndToString_ReturnsCorrectValues() + { + var node = new Add(new NumberNode(1), new NumberNode(2)); + + Assert.Multiple(() => + { + Assert.That(node.Calculate(), Is.EqualTo(3)); + Assert.That(node.ToStringRepresentation(), Is.EqualTo("(+ 1 2)")); + }); + } + + /// + /// Test subtraction (non-commutative operation) works correctly. + /// + [Test] + public void Parser_Parse_Subtraction_ReturnsCorrectResult() + { + const string input = "(- 10 4)"; + var tree = Parser.Parse(input); + Assert.Multiple(() => + { + Assert.That(tree.ToStringRepresentation(), Is.EqualTo("(- 10 4)")); + Assert.That(tree.Calculate(), Is.EqualTo(6)); + }); + } + + /// + /// Test division (non-commutative operation) works correctly and doesn't swap operands. + /// + [Test] + public void Parser_Parse_Division_ReturnsCorrectResult() + { + const string input = "(/ 10 2)"; + var tree = Parser.Parse(input); + Assert.Multiple(() => + { + Assert.That(tree.ToStringRepresentation(), Is.EqualTo("(/ 10 2)")); + Assert.That(tree.Calculate(), Is.EqualTo(5)); + }); + } +} \ No newline at end of file diff --git a/HW2/SyntaxTree.Tests/stylecop.json b/HW2/SyntaxTree.Tests/stylecop.json new file mode 100644 index 0000000..76c8e76 --- /dev/null +++ b/HW2/SyntaxTree.Tests/stylecop.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "documentationRules": { + "companyName": "khusainovilas", + "copyrightText": "Copyright (c) {companyName}. All rights reserved." + } + } +} \ No newline at end of file diff --git a/HW2/SyntaxTree.Tests/testFile.txt b/HW2/SyntaxTree.Tests/testFile.txt new file mode 100644 index 0000000..fbd556d --- /dev/null +++ b/HW2/SyntaxTree.Tests/testFile.txt @@ -0,0 +1 @@ +(+ 1 2) \ No newline at end of file diff --git a/HW2/SyntaxTree/Add.cs b/HW2/SyntaxTree/Add.cs new file mode 100644 index 0000000..61fb0f2 --- /dev/null +++ b/HW2/SyntaxTree/Add.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) khusainovilas. All rights reserved. +// + +namespace SyntaxTree; + +/// +/// Add operation. +/// +public class Add : Operation +{ + /// + /// Initializes a new instance of the class. + /// + /// Left operand of the operation. + /// Right operand of the operation. + public Add(IAbstractNode left, IAbstractNode right) + : base(left, right) + { + } + + /// + protected override char Symbol => '+'; + + /// + public override int Calculate() => this.Left.Calculate() + this.Right.Calculate(); +} \ No newline at end of file diff --git a/HW2/SyntaxTree/Divide.cs b/HW2/SyntaxTree/Divide.cs new file mode 100644 index 0000000..41bb302 --- /dev/null +++ b/HW2/SyntaxTree/Divide.cs @@ -0,0 +1,36 @@ +// +// Copyright (c) khusainovilas. All rights reserved. +// + +namespace SyntaxTree; + +/// +/// Divide operation. +/// +public class Divide : Operation +{ + /// + /// Initializes a new instance of the class. + /// + /// Left operand of the operation. + /// Right operand of the operation. + public Divide(IAbstractNode left, IAbstractNode right) + : base(left, right) + { + } + + /// + protected override char Symbol => '/'; + + /// + public override int Calculate() + { + var divisor = this.Right.Calculate(); + if (divisor == 0) + { + throw new DivideByZeroException("Division by zero."); + } + + return this.Left.Calculate() / divisor; + } +} \ No newline at end of file diff --git a/HW2/SyntaxTree/IAbstractNode.cs b/HW2/SyntaxTree/IAbstractNode.cs new file mode 100644 index 0000000..e84656f --- /dev/null +++ b/HW2/SyntaxTree/IAbstractNode.cs @@ -0,0 +1,23 @@ +// +// Copyright (c) khusainovilas. All rights reserved. +// + +namespace SyntaxTree; + +/// +/// Represents a node in the syntax tree. +/// +public interface IAbstractNode +{ + /// + /// Calculates the value of the node. + /// + /// The calculated result. + int Calculate(); + + /// + /// Returns the string representation of the node. + /// + /// String representation of the node. + string ToStringRepresentation(); +} \ No newline at end of file diff --git a/HW2/SyntaxTree/Multiply.cs b/HW2/SyntaxTree/Multiply.cs new file mode 100644 index 0000000..af84374 --- /dev/null +++ b/HW2/SyntaxTree/Multiply.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) khusainovilas. All rights reserved. +// + +namespace SyntaxTree; + +/// +/// Multiply operation. +/// +public class Multiply : Operation +{ + /// + /// Initializes a new instance of the class. + /// + /// Left operand of the operation. + /// Right operand of the operation. + public Multiply(IAbstractNode left, IAbstractNode right) + : base(left, right) + { + } + + /// + protected override char Symbol => '*'; + + /// + public override int Calculate() => this.Left.Calculate() * this.Right.Calculate(); +} \ No newline at end of file diff --git a/HW2/SyntaxTree/NumberNode.cs b/HW2/SyntaxTree/NumberNode.cs new file mode 100644 index 0000000..2a9586e --- /dev/null +++ b/HW2/SyntaxTree/NumberNode.cs @@ -0,0 +1,17 @@ +// +// Copyright (c) khusainovilas. All rights reserved. +// + +namespace SyntaxTree; + +/// +/// Represents a number node in the syntax tree. +/// +public class NumberNode(int value) : IAbstractNode +{ + /// + public int Calculate() => value; + + /// + public string ToStringRepresentation() => value.ToString(); +} \ No newline at end of file diff --git a/HW2/SyntaxTree/Operation.cs b/HW2/SyntaxTree/Operation.cs new file mode 100644 index 0000000..efae6d9 --- /dev/null +++ b/HW2/SyntaxTree/Operation.cs @@ -0,0 +1,44 @@ +// +// Copyright (c) khusainovilas. All rights reserved. +// + +namespace SyntaxTree; + +/// +/// Base class for all binary operations. +/// +public abstract class Operation : IAbstractNode +{ + /// + /// Initializes a new instance of the class. + /// + /// Left operand of the operation. + /// Right operand of the operation. + protected Operation(IAbstractNode left, IAbstractNode right) + { + this.Left = left; + this.Right = right; + } + + /// + /// Gets left child of operator. + /// + protected IAbstractNode Left { get; } + + /// + /// Gets right child of operator. + /// + protected IAbstractNode Right { get; } + + /// + /// Gets symbol of operation. + /// + protected abstract char Symbol { get; } + + /// + public abstract int Calculate(); + + /// + public string ToStringRepresentation() + => $"({this.Symbol} {this.Left.ToStringRepresentation()} {this.Right.ToStringRepresentation()})"; +} \ No newline at end of file diff --git a/HW2/SyntaxTree/Parser.cs b/HW2/SyntaxTree/Parser.cs new file mode 100644 index 0000000..075b079 --- /dev/null +++ b/HW2/SyntaxTree/Parser.cs @@ -0,0 +1,100 @@ +// +// Copyright (c) khusainovilas. All rights reserved. +// + +namespace SyntaxTree; + +/// +/// Parser for building the syntax tree from a string representation. +/// +public static class Parser +{ + /// + /// Parses the input string into a syntax tree node. + /// + /// The input string in prefix notation. + /// The root node of the syntax tree. + public static IAbstractNode Parse(string input) + { + if (string.IsNullOrWhiteSpace(input)) + { + throw new ArgumentException("Input cannot be empty or whitespace."); + } + + if (!AreParenthesesBalanced(input)) + { + throw new ArgumentException("Unbalanced parentheses."); + } + + var tokens = input + .Replace('(', ' ') + .Replace(')', ' ') + .Split(' ', StringSplitOptions.RemoveEmptyEntries); + + var pos = 0; + var node = ParseExpression(tokens, ref pos); + + if (pos != tokens.Length) + { + throw new ArgumentException("Extra tokens after parsing complete expression."); + } + + return node; + } + + private static IAbstractNode ParseExpression(string[] tokens, ref int pos) + { + if (pos >= tokens.Length) + { + throw new ArgumentException("Unexpected end of input."); + } + + var token = tokens[pos++]; + if (int.TryParse(token, out var value)) + { + return new NumberNode(value); + } + + if (token.Length != 1 || !"+-*/".Contains(token[0])) + { + throw new ArgumentException($"Invalid operator: {token}"); + } + + var op = token[0]; + var left = ParseExpression(tokens, ref pos); + var right = ParseExpression(tokens, ref pos); + + return op switch + { + '+' => new Add(left, right), + '-' => new Subtract(left, right), + '*' => new Multiply(left, right), + '/' => new Divide(left, right), + _ => throw new ArgumentException($"Unknown operator: {op}"), + }; + } + + private static bool AreParenthesesBalanced(string s) + { + var balance = 0; + foreach (var c in s) + { + if (c == '(') + { + balance++; + } + + if (c == ')') + { + balance--; + } + + if (balance < 0) + { + return false; + } + } + + return balance == 0; + } +} diff --git a/HW2/SyntaxTree/Program.cs b/HW2/SyntaxTree/Program.cs new file mode 100644 index 0000000..71cd4be --- /dev/null +++ b/HW2/SyntaxTree/Program.cs @@ -0,0 +1,42 @@ +// +// Copyright (c) khusainovilas. All rights reserved. +// + +using SyntaxTree; + +try +{ + if (args.Length != 1) + { + throw new ArgumentException("Exactly one argument is expected - the file path to the file."); + } + + var filePath = args[0]; + + if (!File.Exists(filePath)) + { + throw new FileNotFoundException("File not found.", filePath); + } + + var input = File.ReadAllText(filePath).Trim(); + var tree = Parser.Parse(input); + + Console.WriteLine($"The tree: {tree.ToStringRepresentation()}"); + Console.WriteLine($"The result: {tree.Calculate()}"); +} +catch (FileNotFoundException ex) +{ + Console.WriteLine($"Error: {ex.Message}"); +} +catch (ArgumentException ex) +{ + Console.WriteLine($"Error: Incorrect expression. {ex.Message}"); +} +catch (DivideByZeroException) +{ + Console.WriteLine("Mistake: Division by zero."); +} +catch (Exception) +{ + Console.WriteLine("Error: An unknown error has occurred."); +} \ No newline at end of file diff --git a/HW2/SyntaxTree/Subtract.cs b/HW2/SyntaxTree/Subtract.cs new file mode 100644 index 0000000..4c01f0c --- /dev/null +++ b/HW2/SyntaxTree/Subtract.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) khusainovilas. All rights reserved. +// + +namespace SyntaxTree; + +/// +/// Subtract operation. +/// +public class Subtract : Operation +{ + /// + /// Initializes a new instance of the class. + /// + /// Left operand of the operation. + /// Right operand of the operation. + public Subtract(IAbstractNode left, IAbstractNode right) + : base(left, right) + { + } + + /// + protected override char Symbol => '-'; + + /// + public override int Calculate() => this.Left.Calculate() - this.Right.Calculate(); +} \ No newline at end of file diff --git a/HW2/SyntaxTree/SyntaxTree.csproj b/HW2/SyntaxTree/SyntaxTree.csproj new file mode 100644 index 0000000..f3becf9 --- /dev/null +++ b/HW2/SyntaxTree/SyntaxTree.csproj @@ -0,0 +1,22 @@ + + + + Exe + net9.0 + enable + enable + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/HW2/SyntaxTree/stylecop.json b/HW2/SyntaxTree/stylecop.json new file mode 100644 index 0000000..76c8e76 --- /dev/null +++ b/HW2/SyntaxTree/stylecop.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "documentationRules": { + "companyName": "khusainovilas", + "copyrightText": "Copyright (c) {companyName}. All rights reserved." + } + } +} \ No newline at end of file