|
import operator |
|
|
|
from base_parser import BaseParser |
|
from calculator_exceptions import UnexpectedCharacterError, UnexpectedEndError |
|
|
|
|
|
class Parser(BaseParser): |
|
"""Parser for tokenized calculator inputs.""" |
|
|
|
def parse(self): |
|
"""Parse calculator input and return the result of evaluating it. |
|
|
|
>>> Parser([1, '*', '(', 2, '+', 3, ')']).parse() |
|
5 |
|
""" |
|
return self._parse_expression() |
|
|
|
def _parse_expression(self): |
|
"""Parse an Expression and return the result of evaluating it. |
|
|
|
>>> Parser([1, '+', 2])._parse_expression() |
|
3 |
|
""" |
|
|
|
# Parse the first (required) Term |
|
terms = [self._parse_term()] |
|
|
|
# Parse any following: ("*"|"/") Factor |
|
op = lambda t: t in '+-' |
|
terms += flatten((op, self._parse_term()) for op in self._take(op)) |
|
|
|
return evaluate(terms) |
|
|
|
def _parse_term(self): |
|
"""Parse a Term and return the result of evaluating it. |
|
|
|
>>> Parser([1, '*', 2])._parse_term() |
|
2 |
|
""" |
|
|
|
# Parse the first (required) Factor |
|
factors = [self._parse_factor()] |
|
|
|
# Parse any following: ("*"|"/") Factor |
|
op = lambda t: t in '*/' |
|
factors += flatten((op, self._parse_factor()) for op in self._take(op)) |
|
|
|
return evaluate(factors) |
|
|
|
def _parse_factor(self): |
|
"""Parse a Factor and return the result of evaluating it. |
|
|
|
>>> Parser([1])._parse_factor() |
|
1 |
|
|
|
>>> Parser(['(', 1, '+', 2, '*', 3, ')'])._parse_factor() |
|
7 |
|
""" |
|
|
|
# NOTE: Here's where Python gets a little cumbersome. This isn't really |
|
# a for, we're just using it to handle the case where it doesn't |
|
# find a number (gracefully skip). If it finds one, we return the |
|
# number. |
|
for n in self._take(lambda t: isinstance(t, float)): |
|
return n |
|
|
|
# If we failed to parse a number, then try to find a '(' |
|
for _ in self._take(lambda t: t == '('): |
|
# If we found a '(', parse the subexpression |
|
value = self._parse_expression() |
|
# Make sure the subexpression is followed by a ')' |
|
self._expect(')') |
|
|
|
# Both parsing the number and subexpresion failed |
|
raise self._unexpected('number', '(') |
|
|
|
def _expect(self, char): |
|
"""Expect a certain character, or raise if it is not next.""" |
|
for _ in self._take(lambda t: t == char): |
|
return |
|
|
|
raise self._unexpected(char) |
|
|
|
def _unexpected(self, *expected): |
|
"""Create an exception for an unexpected character.""" |
|
try: |
|
return UnexpectedCharacterError(self._items.peek(), expected) |
|
except StopIteration: |
|
return UnexpectedEndError(expected) |
|
|
|
|
|
def evaluate(items): |
|
""" |
|
Evaluate a list of floats separated by operators (at the same level of |
|
precedence). Returns the result. |
|
|
|
>>> evaluate([3, '*', 4, '/', 2]) |
|
6 |
|
""" |
|
|
|
assert items, 'cannot evaluate empty list' |
|
# x, x + x, x + x + x, etc. all have an odd number of tokens |
|
assert len(items) % 2 == 1, 'list must be of odd length' |
|
|
|
while len(items) > 1: |
|
items[-3:] = [_evaluate_binary(*items[-3:])] |
|
|
|
return items[0] |
|
|
|
|
|
def _evaluate_binary(lhs, op, rhs): |
|
"""Evalutates a single binary operation op where lhs and rhs are floats.""" |
|
ops = {'+': operator.add, |
|
'-': operator.sub, |
|
'*': operator.mul, |
|
'/': operator.truediv} |
|
|
|
return ops[op](lhs, rhs) |
|
|
|
|
|
def flatten(iterable): |
|
"""Flattens a nested iterable by one nesting layer. |
|
|
|
>>> flatten([[1,2], [3]]) |
|
[1, 2, 3] |
|
""" |
|
return [x for l in iterable for x in l] |