""" This Calculator holds the logic for the calculator. """ import pytest from calculator.operators import Operator, STANDARD_OPERATORS from calculator.expression import Token, Term, Expression, TermExpression, OperatorExpression class Calculator: """ Calculator class is a simple calculator that can parse and evaluate expressions of the form: expression ::= term | expression operator expression operator ::= + | - | * | / with the usual precedence rules. """ def __init__(self, operators=None): if operators is None: operators = STANDARD_OPERATORS self.operators = operators def tokenize(self, line: str) -> list[Token]: """ Tokenize an expression into a list of tokens. """ tokens = [] for token in line.split(): if token in self.operators: tokens.append(self.operators[token]) else: try: term = float(token) tokens.append(term) except ValueError as exc: raise ValueError(f"Invalid token {token}") from exc return tokens def parse(self, tokens: list[Token]) -> Expression: """ Parse a list of tokens into an ordered expression. """ if not tokens: raise ValueError("Empty expression") if len(tokens) == 1: if isinstance(tokens[0], Term): return TermExpression(tokens[0]) raise ValueError(f"Expected a term, got {tokens[0]}") if len(tokens) == 2: raise ValueError("Invalid expression") # Find the rightest operator with the lowest precedence operator = None for i, token in enumerate(tokens): if isinstance(token, Operator): if operator is None or token.precedence <= operator.precedence: operator = token operator_index = i # Split the expression into two parts left = tokens[:operator_index] right = tokens[operator_index + 1:] # Parse the left and right parts recursively return OperatorExpression(operator, self.parse(left), self.parse(right)) def __call__(self, expression: str) -> Term: return self.parse(self.tokenize(expression))() @pytest.fixture(scope="module", name="setup") def fixture_setup(): """ Setup the test suite, by instantiating the calculator and the operators. """ plus = Operator('+', 1, lambda a, b: a + b) minus = Operator('-', 1, lambda a, b: a - b) times = Operator('*', 2, lambda a, b: a * b) divide = Operator('/', 2, lambda a, b: a / b) calculator = Calculator( operators={'+': plus, '-': minus, '*': times, '/': divide}) yield plus, minus, times, divide, calculator def test_tokenizer(setup): """ Test the tokenizer. """ plus, minus, times, divide, calc = setup assert calc.tokenize("1 + 2") == [1.0, plus, 2.0] assert calc.tokenize("1 + 2 * 3") == [1.0, plus, 2.0, times, 3.0] assert calc.tokenize( "1 + 2 * 3 / 4") == [1.0, plus, 2.0, times, 3.0, divide, 4.0] assert calc.tokenize( "1 + 2 * 3 / 4 - 5") == [1.0, plus, 2.0, times, 3.0, divide, 4.0, minus, 5.0] def test_parser(setup): """ Test the parser. """ _, _, _, _, calc = setup assert repr(calc.parse(calc.tokenize("1 + 2"))) == '(1.0 + 2.0)' assert repr(calc.parse(calc.tokenize("1 + 2 * 3")) ) == '(1.0 + (2.0 * 3.0))' assert repr(calc.parse(calc.tokenize( "1 + 2 * 3 / 4"))) == '(1.0 + ((2.0 * 3.0) / 4.0))' assert repr(calc.parse(calc.tokenize( "1 + 2 * 3 / 4 - 5"))) == '((1.0 + ((2.0 * 3.0) / 4.0)) - 5.0)' def test_evaluation(setup): """ Test the evaluation. """ _, _, _, _, calc = setup assert calc("1 + 2") == 3 assert calc("1 + 2 * 3") == 7 assert calc("1 + 2 * 3 / 4") == 2.5 assert calc("1 + 2 * 3 / 4 - 5") == -2.5