From 2ac7ce05206b7e79b4be90de0d2f0988f4a155fd Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 30 Nov 2019 18:06:07 +0100 Subject: [PATCH] calculator: add infix parser --- calculator/calculator/parse/__init__.py | 1 + calculator/calculator/parse/infix.py | 80 +++++++++++++++++++++++ calculator/calculator/parse/test_infix.py | 42 ++++++++++++ 3 files changed, 123 insertions(+) create mode 100644 calculator/calculator/parse/infix.py create mode 100644 calculator/calculator/parse/test_infix.py diff --git a/calculator/calculator/parse/__init__.py b/calculator/calculator/parse/__init__.py index 630a813..c28704b 100644 --- a/calculator/calculator/parse/__init__.py +++ b/calculator/calculator/parse/__init__.py @@ -1,2 +1,3 @@ +from .infix import parse_infix from .postfix import parse_postfix from .prefix import parse_prefix diff --git a/calculator/calculator/parse/infix.py b/calculator/calculator/parse/infix.py new file mode 100644 index 0000000..0057f3f --- /dev/null +++ b/calculator/calculator/parse/infix.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, List, Union, cast + +from calculator.ast import BinOp, Constant, UnaryOp +from calculator.core import operations + +from .parsed_string import ParsedString + +if TYPE_CHECKING: + from calculator.ast import Node + + +""" +E : T [ (+|-) T ]* + +T : F [ (*|/) F ]* + +F : [ (-|+) ]* P + +P : G [ (^) F ]* + +G : '(' E ')' | CONSTANT +""" + + +def parse_g(l: List[Union[int, str]]) -> Node: + top = l.pop(0) + if top == "(": + ans = parse_e(l) + assert l.pop(0) == ")" + return ans + return Constant(top) + + +def parse_p(l: List[Union[int, str]]) -> Node: + lhs = parse_g(l) + while len(l) and l[0] == "^": + op = l.pop(0) + rhs = parse_f(l) + lhs = BinOp(operations.STR_TO_BIN[op], lhs, rhs) + return lhs + + +def parse_f(l: List[Union[int, str]]) -> Node: + str_to_unop = { + "+": operations.identity, + "-": operations.negate, + } + if l[0] in str_to_unop: + op = l.pop(0) + return UnaryOp(str_to_unop[op], parse_f(l)) + return parse_p(l) + + +def parse_t(l: List[Union[int, str]]) -> Node: + lhs = parse_f(l) + while len(l) and l[0] in ["*", "/"]: + op = cast(str, l.pop(0)) + rhs = parse_t(l) + lhs = BinOp(operations.STR_TO_BIN[op], lhs, rhs) + return lhs + + +def parse_e(l: List[Union[int, str]]) -> Node: + lhs = parse_t(l) + while len(l) and l[0] in ["+", "-"]: + op = cast(str, l.pop(0)) + rhs = parse_t(l) + lhs = BinOp(operations.STR_TO_BIN[op], lhs, rhs) + return lhs + + +def parse_infix(input: str) -> Node: + """ + Parses the given string in infix notation. + """ + parsed = ParsedString(input).tokenize() + ans = parse_e(parsed) + return ans diff --git a/calculator/calculator/parse/test_infix.py b/calculator/calculator/parse/test_infix.py new file mode 100644 index 0000000..1a3aed4 --- /dev/null +++ b/calculator/calculator/parse/test_infix.py @@ -0,0 +1,42 @@ +from calculator.ast import BinOp, Constant, UnaryOp +from calculator.core import operations + +from .infix import parse_infix + + +def test_parse_constant(): + assert parse_infix("42") == Constant(42) + + +def test_parse_negated_constant(): + assert parse_infix("-42") == UnaryOp(operations.negate, Constant(42)) + + +def test_parse_doubly_negated_constant(): + assert parse_infix("--42") == UnaryOp( + operations.negate, UnaryOp(operations.negate, Constant(42)) + ) + + +def test_parse_binary_operation(): + assert parse_infix("12 + 27") == BinOp(operations.plus, Constant(12), Constant(27)) + + +def test_parse_complete_expression_tree(): + assert parse_infix("(12 + 27 )* (42 - 51)") == BinOp( + operations.times, + BinOp(operations.plus, Constant(12), Constant(27)), + BinOp(operations.minus, Constant(42), Constant(51)), + ) + + +def test_power_binds_to_the_right(): + assert parse_infix("12 ^ 27 ^ 42") == BinOp( + operations.pow, Constant(12), BinOp(operations.pow, Constant(27), Constant(42)) + ) + + +def test_negate_and_pow_dont_mix(): + assert parse_infix("-12 ^ 27") == UnaryOp( + operations.negate, BinOp(operations.pow, Constant(12), Constant(27)) + )