Welcome, guest | Sign In | My Account | Store | Cart

One of the signs that you love Python is when you start to use it as a simple calculator. The problem with that is beyond the usefulness of 'sum' the interactive interpreter is not optimal for any calculations beyond a few numbers. This mostly seems to stem from the numbers not being formatted in a nice fashion; 2345634+2894756-2345823 is not the easiest thing to read. That's where an accountant's calculator comes in handy; the tape presents numbers in a column view that is very uncluttered. And thanks to the decimal package a very simple one can be implemented quickly.

To use this recipe you input the number, an optional space, and then the operator (/, *, -, or +; everything you would find on the numeric keypad on your keyboard) and then press return. This will apply the number to the running total using the operator. To output the total just enter a blank line. To quit enter the letter 'q' and press return. This simple interface matches the output of a typical accountant's calculator, removing the need to have some other form of output.

Python, 40 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
"""A *very* simple command-line accountant's calculator.

Uses the decimal package (introduced in Python 2.4) for calculation accuracy.
Input should be a number (decimal point is optional), an optional space, and an
operator (one of /, *, -, or +).  A blank line will output the total.
Inputting just 'q' will output the total and quit the program.

"""
import decimal
import re

parse_input = re.compile(r'(?P<num_text>\d*(\.\d*)?)\s*(?P<op>[/*\-+])')

total = decimal.Decimal('0')

total_line = lambda val: ''.join(('=' * 5, '\n', str(val)))

while True:
    tape_line = raw_input()
    if not tape_line:
        print total_line(total)
        continue
    elif tape_line is 'q':
        print total_line(total)
        break
    try:
        num_text, space, op = parse_input.match(tape_line).groups()
    except AttributeError:
        raise ValueError("invalid input")
    num = decimal.Decimal(num_text)
    if op is '/':
        total /= num
    elif op is '*':
        total *= num
    elif op is '-':
        total -= num
    elif op is '+':
        total += num
    else:
        raise ValueError("unsupported operator: %s" % op)

This recipe is only possible thanks to the decimal package introduced in Python 2.4 . It allows for very high-precision decimal arithmetic that is just not possible using binary floating point numbers; no more lost pennies thanks to binary floating point not being able to represent exact values.

A nice improvement would be to remove the need to press return after each operator. That would require having stdin be unbuffered and reading in every character to detect when it is entered. Also a GUI version that visually looked like a calculator would be nice.

1 comment

Jason Whitlark 19 years, 4 months ago  # | flag

Improvements - no return needed, clear function, arbitrary decimal places. I was very pleased with this recipe, but wanted it to do a little more. Here is my version, which only works on windows; you'll need to override getchar for other operating systems. See Recipe 134892 for more on how to do that.

from decimal import *
import msvcrt     #Windows only!
import sys

class TenKey:
    def __init__(self):
        #set up for fixed decimal places (change .01)
        getcontext().rounding = ROUND_HALF_UP
        self.zero = Decimal('0').quantize(Decimal('.01'))

        self.total = self.zero
        self.EnteredOnce = False
        self.currLine = ''

    def clear(self):
        self.total = self.zero
        self.currLine = ''
        print "c"

    def onOperator(self, char):
        print char
        self.EnteredOnce = False
        self.currLine = ''

    def getchar(self):
        """Override or replace this function for Unix or MacOS
           see http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/134892
           for example"""
        self.lineDelim = '\r'
        return msvcrt.getch()

    def run(self):
        while True:
            char = self.getchar()
            if char == 'q':
                break
            elif char == 'c':
                self.clear()
            elif char == self.lineDelim:
                if self.EnteredOnce:
                    self.clear()
                else:
                    sys.stdout.write('=======\n' + str(self.total) + '\n')
                    self.EnteredOnce = True
                    continue
            elif char == '+':
                self.total += Decimal(self.currLine)
                self.onOperator(char)
            elif char == '-':
                self.total -= Decimal(self.currLine)
                self.onOperator(char)
            elif char == '*':
                self.total *= Decimal(self.currLine)
                self.onOperator(char)
            elif char == '/':
                self.total /= Decimal(self.currLine)
                self.onOperator(char)
            elif char in '0123456789':
                self.EnteredOnce = False
                sys.stdout.write(char)
                self.currLine += char
            elif char == '.':
                if char not in self.currLine:
                    self.currLine += char
                    sys.stdout.write(char)

if __name__ == "__main__":
    calc = TenKey()
    calc.run()