From 498dcb79adf6acc2e2dd4224e50d30eb4e66f9aa Mon Sep 17 00:00:00 2001 From: "GASSER Thibaud (PRESTA EXT)" Date: Wed, 6 Mar 2024 16:40:24 +0100 Subject: [PATCH] Evaluate mathematical expression https://www.codewars.com/kata/52a78825cdfc2cfc87000005/solutions/python --- evaluate-mathematical-expressions/README.md | 81 ++++++++++++++ evaluate-mathematical-expressions/solution.py | 105 ++++++++++++++++++ evaluate-mathematical-expressions/test.py | 47 ++++++++ 3 files changed, 233 insertions(+) create mode 100644 evaluate-mathematical-expressions/README.md create mode 100644 evaluate-mathematical-expressions/solution.py create mode 100644 evaluate-mathematical-expressions/test.py diff --git a/evaluate-mathematical-expressions/README.md b/evaluate-mathematical-expressions/README.md new file mode 100644 index 0000000..6c7d664 --- /dev/null +++ b/evaluate-mathematical-expressions/README.md @@ -0,0 +1,81 @@ +https://www.codewars.com/kata/52a78825cdfc2cfc87000005/fork/python + +# Instructions + +Given a mathematical expression as a string you must return the result as a number. + +## Numbers + +Number may be both whole numbers and/or decimal numbers. The same goes for the returned result. + +## Operators + +You need to support the following mathematical operators: + +* Multiplication `*` +* Division `/` (as floating point division) +* Addition `+` +* Subtraction `-` + +Operators are always evaluated from left-to-right, and `*` and `/` must be evaluated before `+` and `-`. + +## Parentheses + +You need to support multiple levels of nested parentheses, ex. `(2 / (2 + 3.33) * 4) - -6` + +## Whitespace + +There may or may not be whitespace between numbers and operators. + +An addition to this rule is that the minus sign (`-`) used for negating numbers and parentheses will *never* be separated by whitespace. I.e all of the following are **valid** expressions. + +``` +1-1 // 0 +1 -1 // 0 +1- 1 // 0 +1 - 1 // 0 +1- -1 // 2 +1 - -1 // 2 +1--1 // 2 + +6 + -(4) // 2 +6 + -( -4) // 10 +``` + +And the following are **invalid** expressions + +``` +1 - - 1 // Invalid +1- - 1 // Invalid +6 + - (4) // Invalid +6 + -(- 4) // Invalid +``` + +## Validation + +You do not need to worry about validation - you will only receive **valid** mathematical expressions following the above rules. + +## Restricted APIs + +```if:javascript +NOTE: Both `eval` and `Function` are disabled. +``` + +```if:php +NOTE: `eval` is disallowed in your solution. +``` + +```if:python +NOTE: `eval` and `exec` are disallowed in your solution. +``` +```if:clojure +NOTE: `eval` and `import` are disallowed in your solution. +``` + +```if:java +NOTE: To keep up the difficulty of the kata, use of some classes and functions is disallowed. Their names cannot appear in the solution file, even in comments and variable names. +``` + +```if:rust +NOTE: `std::process::Command` is disallowed in your solution. +``` diff --git a/evaluate-mathematical-expressions/solution.py b/evaluate-mathematical-expressions/solution.py new file mode 100644 index 0000000..b71266a --- /dev/null +++ b/evaluate-mathematical-expressions/solution.py @@ -0,0 +1,105 @@ +import re +from enum import Enum +from dataclasses import dataclass + +class TokenType(Enum): + INTEGER = 1 + FLOATING = 2 + PLUS = 3, + MINUS = 4, + MULT = 5, + DIV = 6, + OPEN_PAREN = 7, + CLOSE_PAREN = 8 + + +@dataclass +class Token: + type: TokenType + value: str | int + + +def tokenize(expr): + scanner = re.Scanner(( + (r"\d+", lambda scanner,token: Token(TokenType.INTEGER, token)), + (r"\d+\.\d", lambda scanner,token: Token(TokenType.FLOATING, token)), + (r"\+", lambda scanner,token: Token(TokenType.PLUS, token)), + (r"\-", lambda scanner,token: Token(TokenType.MINUS, token)), + (r"\*", lambda scanner,token: Token(TokenType.MULT, token)), + (r"\/", lambda scanner,token: Token(TokenType.DIV, token)), + (r"\(", lambda scanner,token: Token(TokenType.OPEN_PAREN, token)), + (r"\)", lambda scanner,token: Token(TokenType.CLOSE_PAREN, token)), + (r"\s+", None) + )) + + tokens, unknown = scanner.scan(expr) + assert len(unknown) == 0, f"Unknown token: {unknown}" + return tokens + + +class Parser: + def __init__(self, tokens): + self.tokens = tokens + self.pos = 0 + + def _accept(self, token_type) -> Token | None: + if self.pos < len(self.tokens) and self.tokens[self.pos].type == token_type: + token = self.tokens[self.pos] + self.pos += 1 + return token + return None + + def parse_expr(self): + tree = self.parse_expr_plus() + assert self.pos == len(self.tokens), "Not all tokens where consumed!" + return tree + + def parse_expr_plus(self): + lhs = self.parse_expr_mult() + while ((token := self._accept(TokenType.PLUS)) is not None + or (token := self._accept(TokenType.MINUS)) is not None): + rhs = self.parse_expr_mult() + if token.type == TokenType.PLUS: + lhs += rhs + elif token.type == TokenType.MINUS: + lhs -= rhs + else: + raise SyntaxError(f"Unexpected token {token[0]}") + return lhs + + def parse_expr_mult(self): + lhs = self.parse_primary() + while ((token := self._accept(TokenType.MULT)) is not None + or (token := self._accept(TokenType.DIV)) is not None): + if token.type == TokenType.MULT: + lhs *= self.parse_primary() + elif token.type == TokenType.DIV: + lhs /= self.parse_primary() + else: + raise SyntaxError(f"Unexpected token {token[0]}") + return lhs + + def parse_primary(self): + assert self.pos <= len(self.tokens) + sign = 1 + if self._accept(TokenType.MINUS) is not None: + sign *= -1 + if (token := self._accept(TokenType.INTEGER)) is not None: + return sign * int(token.value) + elif (token := self._accept(TokenType.OPEN_PAREN)) is not None: + expr = self.parse_expr_plus() + if self._accept(TokenType.CLOSE_PAREN) is None: + raise SyntaxError(f"Expected token {TokenType.CLOSE_PAREN}") + return sign * expr + assert False, f"Unexpected token type for parse_primary: {self.tokens[self.pos][0]}" + + +def calc(expression): + p = Parser(tokenize(expression)) + try: + return p.parse_expr() + except: + print(expression) + print(p.tokens) + raise + diff --git a/evaluate-mathematical-expressions/test.py b/evaluate-mathematical-expressions/test.py new file mode 100644 index 0000000..4157f11 --- /dev/null +++ b/evaluate-mathematical-expressions/test.py @@ -0,0 +1,47 @@ +from solution import calc +import codewars_test as test +import random + + +@test.describe("Fixed tests") +def _(): + @test.it("Tests") + def __(): + cases = ( + ("1 + 1", 2), + ("8/16", 0.5), + ("3 -(-1)", 4), + ("2 + -2", 0), + ("10- 2- -5", 13), + ("(((10)))", 10), + ("3 * 5", 15), + ("-7 * -(6 / 3)", 14) + ) + + for x, y in cases: + test.assert_equals(calc(x), y) + +@test.describe("Random tests") +def _(): + choice = random.choice + randint = random.randint + + @test.it("Tests") + def __(): + for i in range(100): + try: + s = "{}{} {} {}{} {} {}{} {} {}{} {} {}{} {} {}{} {} {}{} {} {}{}".format(choice(["-", ""]), randint(1, 100), choice(["+", "-", "*", "/"]), choice(["-", ""]), randint(1, 100), choice(["+", "-", "*", "/"]), choice(["-", ""]), randint(1, 100), choice(["+", "-", "*", "/"]), choice(["-", ""]), randint(1, 100), choice(["+", "-", "*", "/"]), choice(["-", ""]), randint(1, 100), choice(["+", "-", "*", "/"]), choice(["-", ""]), randint(1, 100), choice(["+", "-", "*", "/"]), choice(["-", ""]), randint(1, 100), choice(["+", "-", "*", "/"]), choice(["-", ""]), randint(1, 100)) + test.assert_approx_equals(calc(s), eval(s), 1e-6, s) + except ZeroDivisionError: + test.pass_() + + for i in range(100): + try: + s = "{}({}{}) {} ({}{} {} {}{} {} {}({})) {} ({}{} {} {}((({}({}{} {} {}{})))) {} {}{})".format(choice(["-", ""]), choice(["-", ""]), randint(1, 100), choice(["+", "-", "*", "/"]), choice(["-", ""]), randint(1, 100), choice(["+", "-", "*", "/"]), choice(["-", ""]), randint(1, 100), choice(["+", "-", "*", "/"]), choice(["-", ""]), randint(1, 100), choice(["+", "-", "*", "/"]), choice(["-", ""]), randint(1, 100), choice(["+", "-", "*", "/"]), choice(["-", ""]), choice(["-", ""]), choice(["-", ""]), randint(1, 100), choice(["+", "-", "*", "/"]), choice(["-", ""]), randint(1, 100), choice(["+", "-", "*", "/"]), choice(["-", ""]), randint(1, 100)) + test.assert_approx_equals(calc(s), eval(s), 1e-6, s) + except ZeroDivisionError: + test.pass_() + + for i in range(100): + s = "{}{}- {}{}- {}{}- {}{}".format(choice(["-", ""]), randint(1, 100), choice(["-", ""]), randint(1, 100), choice(["-", ""]), randint(1, 100), choice(["-", ""]), randint(1, 100)) + test.assert_approx_equals(calc(s), eval(s), 1e-6, s)