Calculator

by Josh Blake (Y12 age ~17)

Josh is a highly talented self-taught programmer. He recommends Think Python - How to think like a computer scientist

The code on this page is available for download in a text file.

This Python calculator uses a dictionary in which each value is a tuple comprising a function and an integer. This simpler example of the dictionary in use should help you to understand the code.

import operator #A dictionary storing each mathematical operator symbol as a key and a #tuple (comprising a function and an integer) as its value. The integer is used #by the calculator as a measure of the precedence of the operator. operators = { '+': (operator.add, 5), '-': (operator.sub, 5), '*': (operator.mul, 10), '/': (operator.truediv, 10), '^': (operator.pow, 15), } #Access the second item in the (zero-based) tuple. print (operators['+'][1]) #Pass the integers 35 and 7 to the add function (the first item in the tuple). print (operators['+'][0](35, 17)) #Pass the integers 6 and 7 to the multiply function print (operators['*'][0](6, 7)) #Pass the integers 2 and 5 to the power function print (operators['^'][0](2, 5))

The output is:

Output

The Code

Note the excellent use of error trapping throughout this program.

# Copyright (c) 2014 Josh Blake # # Licensed under the Apache License, Version 2.0 (the "License"); you may not # use this file except in compliance with the License, as described at # http://www.apache.org/licenses/ and http://www.pp4s.co.uk/licenses/ import operator, decimal from string import digits from sys import exit class UserException(Exception): pass OPERATORS = { '+': (operator.add, 5), '-': (operator.sub, 5), '*': (operator.mul, 10), '/': (operator.truediv, 10), '^': (operator.pow, 15), } def get_operation(calc, pos): """Get operation of a character Params: calc: a string pos: position in string of desired character Returns: operation corresponding to character calc[pos] Throws: UserException: there is no operation """ try: return OPERATORS[calc[pos]] except KeyError: raise UserException('unknown operation ' + calc[pos]) def subexpression(calc, pos): """Extract and calculate subexpression starting at pos. Params: calc: string to extract from pos: where to start within string Returns tuple of (result, length): result: the result of the subexpression length: length of subexpression (in the string) """ to_process = calc[pos+1:] expression_end = to_process.find(')') if expression_end == -1: raise UserException('unclosed expression') to_process = to_process[:expression_end] result = main(to_process, True) return (result, expression_end+2) def tokenise(calc): """Take a string and convert to a list of tokens. Params: calc: a string representing a calculation Returns: a list of tuples of the format (item, priority) where: item: either a Decimal or a function that accepts two decimals priority: higher should be calculated first (a Decimal is 0) Throws: UserException: error in format of calc """ def num(calc, pos): """Extract number or bracketed expression. Params: calc: string to extract from pos: where to start within string Returns tuple of (number, length): number: the number extracted length: length of number (in the string) """ neg = False # number is +ive unles specified startPos = pos # check if number is positive or negative try: if calc[pos] == '(': return subexpression(calc, pos) while calc[pos] == '+' or calc[pos] == '-': if calc[pos] == '-': neg = not neg pos += 1 except IndexError as err: raise UserException('invalid number ' + str(err)) firstDigitPos = pos try: while calc[pos] in (digits + '.'): pos += 1 except IndexError: pass # number is at endof string try: number = decimal.Decimal(calc[firstDigitPos:pos]) except decimal.InvalidOperation as err: # cannot convert to decimal raise UserException('invalid number ' + str(err)) if neg: number *= -1 return (number, pos - startPos) i = 0 tokens = [] while True: number, length = num(calc, i) tokens.append(number) i += length try: tokens.append(get_operation(calc, i)) except IndexError: return tokens i += 1 def get_highest_priority(tokens): """Find the next operation to perform Param: tokens: list of tokens (same format as returned by tokenise) Return: tuple of two values(position, operation) position: position of operation that should be performed next operation: function for operation to perform """ orderVal = 0 pos = 0 for i, token in enumerate(tokens): try: operator = tokens[i] if operator[1] > orderVal: pos = i operation = tokens[i][0] orderVal = tokens[i][1] except TypeError: continue # token isn't an operator return pos, operation def process(tokens): """Perform calculation represented by tokens. Params: tokens: list of tokens as returned by tokenise Returns: result of operation(s) (type decimal.Decimal) Works by finding the highest priority token (BIDMAS) then performing that operation on the numberes either side of the operator. Eg if tokens represents the following [0, '+', 2, '*', 5] the * will be found and passed 2 and 5. This slice of the list will then become 10. So after this operation it tokens will be [0, '+', 10] """ while len(tokens) > 1: pos, operation = get_highest_priority(tokens) try: result = operation(tokens[pos-1], tokens[pos+1]) except Exception: raise UserException(Exception) tokens[pos-1:pos+2] = [result] return tokens[0] def main(calc, internal=False): """Work out a calculation in a string""" try: tokens = tokenise(calc) result = process(tokens) except UserException as err: if internal: raise else: return 'Error: '+str(err) if internal: return result return float(result) if __name__ == "__main__": while True: input_ = raw_input("Enter calculation or 'q' to quit:\n") if input_ == "q": break print(main(input_))

Test Program

Josh impressed us by submitting this test program with Calculator. You can introducing an erroneous assertion to the code of the test program in order to see error messages when the expected results are not obtained.

from calculator import main def error(toRun): actual_result = main(toRun) if not actual_result.startswith('Error:'): print('Expected error running ' + toRun) print('Received: %s' % actual_result) print('') def check(toRun, expected_result): try: actual_result = main(toRun) except Exception as err: print('Uncaught exception running: %s' % toRun) raise if not main(toRun) == expected_result: print('Incorrect output running: ' + toRun) print('Expected: %s' % expected_result) print('Recieved: %s' % actual_result) print('') ##number in, number out check('1', 1.0) check('2', 2.0) ##shouldn't be able to enter non-numbers error('s') error('sdf10') error('d') #operations alone as well error('+') error('-') ##add and subtract check('1+2', 3.0) check('5+6+2', 13.0) check('3-1', 2.0) check('3-1+1', 3.0) check('10-1-3', 6.0) #how about floats? check('0.3+0.1', 0.4) check('1-0.5', 0.5) ##can't add these! error('1+2+') error('-1+7-4+') ##positive and negative check('-3', -3.0) check('+5', 5.0) check('3+-5', -2.0) check('7-+4', 3.0) check('-4+11', 7.0) check('-6+2-10', -14.0) ##multiply and divide #simple, no order of operations check('2*3', 6.0) check('5*10+3', 53.0) check('10/2', 5.0) check('15/5+4', 7.0) check('15/3*5', 25.0) #let's try some floats check('6/4', 1.5) check('0.7*3', 2.1) #And powers! check('3^2', 9.0) check('4^3', 64.0) check('4^0.5', 2.0) check('2^-1', 0.5) #now BIDMAS comes into play... check('3+3*2', 9.0) check('5+20/4', 10.0) check('4/4+1+2*3', 8.0) check('4+3*2+4', 14.0) check('4+3^2*2', 22.0) #fun with brackets check('(3+2)*2', 10.0) check('1/(1+1)', 0.5) check('(2*3)+2', 8.0) check('2-(3-1)', 0.0) #some invalid ones error('5/(2-3+1)') error('5+2)') error('5+2*(2+') print('Finished')

Link to Relevant Section

Getting Started with Python