RPNCalc

An RPN calculator in Python

Discussion | Program Notes | Licensing, Source
Revision History | Program Listing

(double-click any word to see its definition)

Discussion

This is a coding example program, an RPN caculator — it's meant to show some Python techniques and capabilities.

A Reverse Polish Notation (RPN) system is a parenthesis-free way to perform calculations. The basic idea is that one places the required arguments onto a stack, then invokes an operation that needs the arguments. Like this (user entries are blue):
• Initial State
Stack:
empty
• 80 Enter
Stack:
80
• 81 Enter
Stack:
80
81
• /
Stack:
0.9876543209876543

The basic idea is that an arithmetic operator or function takes its arguments from the stack starting with the bottom, and if you enter enough arguments, you can enter lots of operators. RPNCalc display the four lowest stack entries to assist in keeping track of things (as RPN calculators do).

This means operators follow arguments (postfix), rather than appearing between them (infix). This happens to be an easy kind of scheme to design, but it also makes relatively complex calculations easy to manage, after one becomes familiar with the entry method. The Hewlett-Packard company has a successful product line of RPN calculators favored by scientists and engineers (there is no connection between Hewlett-Packard and either the author or arachnoid.com).

RPNCalc uses the RPN system, and it also accepts multiple entries on a single line, so one doesn't actually have to press "Enter" after each entry — a space will do.

Here's an example problem. We have a room that is 8 meters long, 12 meters wide and 2.5 meters high. We need to know the diagonal distance from one corner at the floor, to the diagonally opposite corner at the ceiling.

• With an algebraic calculator, I would enter "sqrt, (, 8, x^2, +, 12, x^2, +, 2.5, x^2, ), =", more or less (I don't have such a calculator to test with).

• With RPNCalc, to get the same result I would enter a single line and press Enter once: "8 2 ^ 12 2 ^ 2.5 2 ^ + + sqrt" for a result of 14.637 meters.

Program Notes

RPNCalc shows one of Python's advantages - one can use a function name as an argument or data element. This means one can create a cross-reference list of keyboard entry tokens and associated functions, and quickly associate entries with functions.

• The "com2args" list (line 50) contains all the names and functions that require two stack arguments.
• The "com1arg" list (line 60) contains all the names and functions that require one stack argument.
• The "coms" list (line 86) contains names and functions that either don't care about the stack or that insert a value into it.
• In operation, the user types a line and presses Enter.
• The user's line is broken up into tokens (line 119), and each token is examined separately.
• First, each token is tested to see if it's a number (line 121). If true, the number is placed on the stack.
• If the token isn't a number, it is tested against the three lists described above (lines 125-129).
• If a match is found, the relevant list entry contains a function reference, from the Python operator module (addition, subtraction, etc.) or the math module's collection of functions (sine, cosine, etc.) or a small set of stack operations usually defined as "lambda functions".
• Lambda functions are small, anonymous functions that are useful anywhere a small operation is needed that doesn't merit a formal function definition.
• Once a matching function is found, it can be executed by simply using an appropriate syntax:
• To read the value of a numeric variable, once can write "x = myvar". In this case, x receives the value of "myvar".
• But if the variable contains a reference to a function, one can write "y = myvar()". Notice the parentheses after the variable name. In this case, y receives the return value of the function.
• Here is an example using Python's interactive mode:
```>>> from math import *
>>> myvar = sqrt
>>> print myvar
<built-in function sqrt>
>>> print myvar(2)
1.41421356237
>>>
```
• There are a few less obvious aspects to this program. One is that I needed an index for the stack label string containing "X,Y,Z,T" in the stack display. Normally if you write "for item in list:", you will get a series of items, returned according their order in the list. But if you want an index along with the list items, you write "for i,item in enumerate(list):" (line 104). This allowed me an index without having to explicitly create and increment one.

• Another aspect is that I needed to get a subset of the stack contents, but in reverse order. The stack can grow without bound, and that's a "good thing™" because a user might have a complex calculation requiring more than four stack entries. But I ony wanted to display the lowest four entries, and I needed them in the reverse order that they appear in the stack. My solution is "stack[3::-1]" (also line 104). This says, "create a copy of the four lowest-numbered members from the original list, but in reverse order."

• By importing the readine module and using the "raw_input()" function for input (line 118), I acquired a history capability, which means the user can press the up-arrow key to recover and reuse previous entries.

• Each command tuple has three fields — a token to match keyboard entries, a function, and an explanatory string. If the user presses "h", a help function (line 33) prints out all the command tokens and their explanatory strings.

Licensing, Source

RPNCalc is released under the GNU General Public License. Here is the plain-text source file without line numbers.

Revision History

• Version 1.1 12/05/2010. UPgraded to Python 2.7, added erf(x) and erfc(x) functions.
• Version 1.0 12/01/2010. Initial Public Release.

Program Listing

```  1: #!/usr/bin/env python
2: # -*- coding: utf-8 -*-
3:
4: # Version 1.1 12/05/2010
5:
6: # ***************************************************************************
7: # *   Copyright (C) 2010, Paul Lutus                                        *
8: # *                                                                         *
9: # *   This program is free software; you can redistribute it and/or modify  *
10: # *   it under the terms of the GNU General Public License as published by  *
11: # *   the Free Software Foundation; either version 2 of the License, or     *
12: # *   (at your option) any later version.                                   *
13: # *                                                                         *
14: # *   This program is distributed in the hope that it will be useful,       *
15: # *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
16: # *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
17: # *   GNU General Public License for more details.                          *
18: # *                                                                         *
19: # *   You should have received a copy of the GNU General Public License     *
20: # *   along with this program; if not, write to the                         *
21: # *   Free Software Foundation, Inc.,                                       *
22: # *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
23: # ***************************************************************************
24:
25: import sys, re, readline, types
26: from math import *
27: from operator import *
28:
29: class RPNCalc:
30:
31:   def fmtprint(self,a,b): print("%6s | %s" % (a,b))
32:
33:   def helptxt(self):
34:     self.fmtprint("Enter","For")
35:     print("-" * 20)
36:     for tup in (self.com2args,self.com1arg,self.coms):
37:       for item in tup:
38:         self.fmtprint(item[0],item[2])
39:
40:   def found(self,s,tup):
41:     for item in tup:
42:       if(s == item[0]):
43:         self.node = item
44:         return True
45:     return False
46:
47:   def __init__(self):
48:     self.stack = []
49:
50:     self.com2args = (
51:       ("+"  , add,"y + x"),
52:       ("-"  , sub,"y - x"),
53:       ("*"  , mul,"y * x"),
54:       ("/"  , div,"y / x"),
55:       ("%"  , fmod,"y % x"),
56:       ("^"  , pow,"y ^ x"),
57:       ("hyp", hypot,"hypot(x,y)"),
58:     )
59:
60:     self.com1arg = (
61:       ("sin"  , sin,       "sin(x)"),
62:       ("cos"  , cos,       "cos(x)"),
63:       ("tan"  , tan,       "tan(x)"),
64:       ("asin" , asin,     "asin(x)"),
65:       ("acos" , acos,     "acos(x)"),
66:       ("atan" , atan,     "atan(x)"),
67:       ("sinh" , sinh,     "sinh(x)"),
68:       ("cosh" , cosh,     "cosh(x)"),
69:       ("tanh" , tanh,     "tanh(x)"),
70:       ("asinh", asinh,   "asinh(x)"),
71:       ("acosh", acosh,   "acosh(x)"),
72:       ("atanh", atanh,   "atanh(x)"),
73:       ("sqrt" , sqrt,     "sqrt(x)"),
74:       ("log"  , log,       "log(x)"),
75:       ("exp"  , exp,       "exp(x)"),
76:       ("ceil" , ceil,     "ceil(x)"),
77:       ("floor", floor,   "floor(x)"),
78:       ("erf"  , erf,       "erf(x)"),
79:       ("erfc" , erfc,     "erfc(x)"),
80:       ("!"    , factorial,     "x!"),
81:       ("abs"  , fabs,         "|x|"),
82:       ("deg"  , degrees,"degree(x)"),
84:     )
85:
86:     self.coms = (
87:       ("pi", lambda: self.stack.insert(0,pi),"Pi"),
88:       ("e" , lambda: self.stack.insert(0,e),"e (base of natural logarithms)"),
89:       ("d" , lambda: self.stack.pop(0),"Drop x"),
90:       ("x" , lambda: self.stack.insert(0,self.stack[0]),"Enter x"),
91:       (""  , lambda: self.stack.insert(0,self.stack[0]),"Enter x (press Enter)"),
92:       ("s" , lambda: self.stack.insert(0,self.stack.pop(1)),"Swap x <-> y"),
93:       ("h" , self.helptxt,"Help"),
94:       ("q" , lambda: 0,"Quit"),
95:     )
96:
97:   def process(self):
98:     spregex = re.compile("\s+")
99:     stacklbl = "tzyx"
100:     line = ""
101:
102:     while (line != "q"):
103:       while(len(self.stack) < 4): self.stack.append(0.0)
104:       for i, n in enumerate(self.stack[3::-1]):
105:         if(type(n) == int):
106:           try:
107:             n = float(n)
108:           except:
109:             n = float('inf')
110:         s = "f" # default display format
111:         an  = abs(n)
112:         if(an > 1e10 or an < 1e-10 and an != 0.0):
113:           s = "e" # switch to scientific notation
114:         fs  = ("%.16" + s) % n
115:         # line up decimal points
116:         p = 10 + len(fs) - fs.find('.')
117:         print("%s: %*s" % (stacklbl[i],p,fs))
118:       line = input("Entry (h = help, q = quit): ")
119:       for tok in spregex.split(line):
120:         try: # parsing a number
121:           self.stack.insert(0,float(tok))
122:         except: # it's not a number
123:           try: # look for command
124:             if (self.found(tok, self.com2args)):
125:               self.stack.insert(0,self.node[1](self.stack.pop(1),self.stack.pop(0)))
126:             elif (self.found(tok, self.com1arg)):
127:               self.stack.insert(0,self.node[1](self.stack.pop(0)))
128:             elif (self.found(tok, self.coms)):
129:               self.node[1]()
130:             else: