Evaluate mathematical expression
https://www.codewars.com/kata/52a78825cdfc2cfc87000005/solutions/python
This commit is contained in:
		
							
								
								
									
										81
									
								
								evaluate-mathematical-expressions/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								evaluate-mathematical-expressions/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -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. | ||||
| ``` | ||||
							
								
								
									
										105
									
								
								evaluate-mathematical-expressions/solution.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								evaluate-mathematical-expressions/solution.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
|      | ||||
							
								
								
									
										47
									
								
								evaluate-mathematical-expressions/test.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								evaluate-mathematical-expressions/test.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
		Reference in New Issue
	
	Block a user