Let me start off by saying that I have several years of experience in Java, but now I need to learn Python, so I decided to make a calculator as it also is a community challenge.
Please review the code looking for beginner mistakes, though I do intend the code to look professional. This is my first real program made in Python and two days ago I knew nothing about Python.
My calculator currently has the following abilities:
- It evaluates expressions, so you cannot interact with it.
- The supported operators are
+
,-
,*
and/
. - Functions are not supported.
- It uses the Shunting-yard algorithm.
- It uses Reverse Polish Notation.
calculator/tokens.py
from decimal import Decimal
__author__ = 'Frank van Heeswijk'
class Token:
pass
class ValueToken(Token):
def __init__(self, value: Decimal):
self.value = value
def __repr__(self):
return "VT(" + str(self.value) + ")"
def __hash__(self):
return hash(self.value)
def __eq__(self, other):
if type(self) != type(other):
return False
return self.value == other.value
def __ne__(self, other):
return not self == other
class OperatorToken(Token):
def __init__(self, operator: str):
self.operator = operator
def __repr__(self):
return "OT(" + self.operator + ")"
def __hash__(self):
return hash(str)
def __eq__(self, other):
if type(self) != type(other):
return False
return self.operator == other.operator
def __ne__(self, other):
return not self == other
class LeftParenthesesToken(Token):
def __repr__(self):
return "LPT"
def __hash__(self):
return 0
def __eq__(self, other):
if type(self) != type(other):
return False
return True
def __ne__(self, other):
return not self == other
class RightParenthesesToken(Token):
def __repr__(self):
return "RPT"
def __hash__(self):
return 0
def __eq__(self, other):
if type(self) != type(other):
return False
return True
def __ne__(self, other):
return not self == other
calculator/calculator.py
from decimal import Decimal
from enum import Enum
import inspect
import re
from calculator.tokens import OperatorToken, ValueToken, LeftParenthesesToken, RightParenthesesToken
__author__ = 'Frank van Heeswijk'
class Associativity(Enum):
left = 1
right = 2
class Calculator:
__operators = {
# reference: http://en.wikipedia.org/wiki/Operators_in_C_and_C%2B%2B#Operator_precedence
# operator: (precedence, associativity, function)
"u+": (-3, Associativity.right, lambda op: op),
"u-": (-3, Associativity.right, lambda op: -op),
"*": (-5, Associativity.left, lambda op1, op2: op1 * op2),
"/": (-5, Associativity.left, lambda op1, op2: op1 / op2),
"+": (-6, Associativity.left, lambda op1, op2: op1 + op2),
"-": (-6, Associativity.left, lambda op1, op2: op1 - op2)
}
def __init__(self):
self.operators = Calculator.__operators
def evaluate(self, expression: str) -> Decimal:
"""
Evaluates an expression and returns its result.
:param expression: The input expression
:return: The output of evaluating the expression
"""
tokens = self.to_rpn(self.tokenize(expression))
stack = []
for token in tokens:
if isinstance(token, ValueToken):
stack.append(token.value)
elif isinstance(token, OperatorToken):
function = self.operators[token.operator][2]
argspec = inspect.getargspec(function)
argument_count = len(argspec.args)
if len(stack) < argument_count:
raise RuntimeError("not enough tokens for: " + str(token) + ", expected: " + str(argument_count) + ", actual: " + str(len(tokens)))
values = [stack.pop() for x in range(argument_count)]
values.reverse()
result = function(*values)
stack.append(result)
else:
raise RuntimeError("unexpected token: " + token)
return stack.pop()
def tokenize(self, expression: str) -> list:
"""
Tokenizes an expression and produces an output list of tokens.
:rtype: list of [Token]
:param expression: The input expression
"""
tokens = []
stripped_expression = expression.replace(' ', '')
value_regex = re.compile(r"\d+(\.\d+)?")
operator_regex = re.compile(r"[^\d\.\(\)]")
left_parentheses_regex = re.compile(r"\(")
right_parentheses_regex = re.compile(r"\)")
regexps = [value_regex, operator_regex, left_parentheses_regex, right_parentheses_regex]
raw_patterns = "|".join(map(lambda regex: regex.pattern, regexps))
capture_regex = re.compile("(" + raw_patterns + ")")
for raw_token, something_else in capture_regex.findall(stripped_expression):
if value_regex.match(raw_token):
tokens.append(ValueToken(Decimal(raw_token)))
elif operator_regex.match(raw_token):
if raw_token not in self.__operators:
raise RuntimeError("unsupported operator: " + raw_token)
tokens.append(OperatorToken(raw_token))
elif left_parentheses_regex.match(raw_token):
tokens.append(LeftParenthesesToken())
elif right_parentheses_regex.match(raw_token):
tokens.append(RightParenthesesToken())
else:
raise RuntimeError("token " + raw_token + " does not match any regex")
# resolve unary plus and minus operators
for index, token in enumerate(tokens):
if isinstance(token, OperatorToken) and token.operator == '-':
if index == 0\
or isinstance(tokens[index - 1], LeftParenthesesToken)\
or isinstance(tokens[index - 1], OperatorToken):
tokens[index] = OperatorToken('u-')
elif isinstance(token, OperatorToken) and token.operator == '+':
if index == 0\
or isinstance(tokens[index - 1], LeftParenthesesToken)\
or isinstance(tokens[index - 1], OperatorToken):
tokens[index] = OperatorToken('u+')
return tokens
def to_rpn(self, tokens: list) -> list:
"""
Converts a list of tokens to an output list in Reverse Polish Notation form.
:rtype: list of [Token]
:type tokens: list of [Token]
:param tokens: The input tokens
:raise RuntimeError: If the parentheses are mismatched
"""
output_queue = []
stack = []
for token in tokens:
if isinstance(token, ValueToken):
output_queue.append(token)
elif isinstance(token, LeftParenthesesToken):
stack.append(token)
elif isinstance(token, RightParenthesesToken):
while len(stack) > 0:
pop_token = stack.pop()
if isinstance(pop_token, LeftParenthesesToken):
break
output_queue.append(pop_token)
# todo implement function support
else:
raise RuntimeError("mismatched parentheses")
elif isinstance(token, OperatorToken):
while len(stack) > 0:
pop_token = stack.pop()
if isinstance(pop_token, OperatorToken) and self.__has_lower_precedence(token, pop_token):
output_queue.append(pop_token)
else:
stack.append(pop_token)
break
stack.append(token)
else:
raise RuntimeError("unexpected token: " + token)
while len(stack) > 0:
pop_token = stack.pop()
if isinstance(pop_token, LeftParenthesesToken):
raise RuntimeError("mismatched parentheses")
output_queue.append(pop_token)
return output_queue
def __has_lower_precedence(self, operatortoken1: OperatorToken, operatortoken2: OperatorToken) -> bool:
operator1 = operatortoken1.operator
operator2 = operatortoken2.operator
if operator1 not in self.operators:
raise RuntimeError("Unsupported operator token: " + operator1)
if operator2 not in self.operators:
raise RuntimeError("Unsupported operator token: " + operator2)
operator1_tuple = self.operators[operator1]
operator2_tuple = self.operators[operator2]
return (operator1_tuple[1] == Associativity.left and operator1_tuple[0] <= operator2_tuple[0]) \
or (operator1_tuple[1] == Associativity.right and operator1_tuple[0] < operator2_tuple[0])
calculator_application.py
import sys
from calculator.calculator import Calculator
__author__ = 'Frank van Heeswijk'
if __name__ == '__main__':
if len(sys.argv) < 2:
print("Usage: calculator_application \"<expression>\"")
sys.exit(1)
calculator = Calculator()
expression = " ".join(sys.argv[1:])
result = calculator.evaluate(expression)
print(result)
Small sample of the unit tests to give you a feel about its usage:
def test_evaluate(self):
calculator = Calculator()
self.assertEqual(Decimal(4), calculator.evaluate("4"))
self.assertEqual(Decimal(21), calculator.evaluate("7 * 3"))
self.assertEqual(Decimal(11), calculator.evaluate("2 * 4 + 3"))
self.assertEqual(Decimal(45), calculator.evaluate("(3 * (2 + 5)) + 6 * (4)"))
self.assertEqual(Decimal("25.92"), calculator.evaluate("2.7 * (3.2 + 6.4)"))
self.assertEqual(Decimal(1), calculator.evaluate("-2 * -4 + -7"))
def test_evaluate_operators(self):
calculator = Calculator()
self.assertEqual(Decimal(3), calculator.evaluate("+3"))
self.assertEqual(Decimal(-3), calculator.evaluate("-3"))
self.assertEqual(Decimal(6), calculator.evaluate("2 * 3"))
self.assertEqual(Decimal(2), calculator.evaluate("6 / 3"))
self.assertEqual(Decimal(5), calculator.evaluate("2 + 3"))
self.assertEqual(Decimal(3), calculator.evaluate("7 - 4"))
def test_evaluate_operator_precedences(self):
calculator = Calculator()
self.assertEqual(Decimal(-14), calculator.evaluate("-3 * 5 + +1"))
self.assertEqual(Decimal("6.5"), calculator.evaluate("8 / -16 - -7"))
self.assertEqual(Decimal(30), calculator.evaluate("5 * 3 * 8 / 4 / 2 * 6 / 3"))
self.assertEqual(Decimal(-3), calculator.evaluate("2 + 3 + 4 - 5 - 8 + 6 + 4 - 9"))
If you are interested in the other unit tests, then the full project can be seen at my GitHub repository.