Evaluate mathematical expression
https://www.codewars.com/kata/52a78825cdfc2cfc87000005/solutions/python
This commit is contained in:
parent
4f3056c84b
commit
498dcb79ad
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)
|
Loading…
Reference in New Issue
Block a user