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