After reading the definition, I made a simple Reverse Polish Notation (RPN) calculator in Python.
Originally it had just 4 operators (using import operator
and a lookup table) and only did integers. After looking at some example calculations, I amended it to work on floats and added a raising to powers. Then I saw some more advanced operations here and added math functions.
Owing to the large amount of math functions available, is there an easier way to include them without having to have math.<function>
in the operators lookup?
# Reverse Polish Notation calculator
# based on http://en.wikipedia.org/wiki/Reverse_Polish_notation
import math
import operator
ops = {'+':operator.add,
'-':operator.sub,
'*':operator.mul,
'/':operator.div,
'^':operator.pow,
'sin':math.sin,
'tan':math.tan,
'cos':math.cos,
'pi':math.pi}
def is_number(s):
try:
float(s)
return True
except ValueError:
pass
def calculate(equation):
stack = []
result = 0
for i in equation:
if is_number(i):
stack.insert(0,i)
else:
if len(stack) < 2:
print 'Error: insufficient values in expression'
break
else:
print 'stack: %s' % stack
if len(i) == 1:
n1 = float(stack.pop(1))
n2 = float(stack.pop(0))
result = ops[i](n1,n2)
stack.insert(0,str(result))
else:
n1 = float(stack.pop(0))
result = ops[i](math.radians(n1))
stack.insert(0,str(result))
return result
def main():
running = True
while running:
equation = raw_input('enter the equation: ').split(' ')
answer = calculate(equation)
print 'RESULT: %f' % answer
again = raw_input('\nEnter another? ')[0].upper()
if again != 'Y':
running = False
if __name__ == '__main__':
main()
Simple/straightforward test:
enter the equation: 6 4 5 + * 25 2 3 + / - stack: ['5', '4', '6'] stack: ['9.0', '6'] stack: ['3', '2', '25', '54.0'] stack: ['5.0', '25', '54.0'] stack: ['5.0', '54.0'] RESULT: 49.000000
Math functions test:
enter the equation: 5 8 2 15 * sin * + 2 45 tan + / stack: ['15', '2', '8', '5'] stack: ['30.0', '8', '5'] stack: ['0.5', '8', '5'] stack: ['4.0', '5'] stack: ['45', '2', '9.0'] stack: ['1.0', '2', '9.0'] stack: ['3.0', '9.0'] RESULT: 3.000000
3 Answers 3
- In Python,
list
is designed to grow efficiently from the end. Implementing a stack usingappend
andpop
at the end is therefore a better approach. - I would expect
is_number
to returnFalse
instead of the implicitNone
which usually stands for a missing value. - An explicit
math.
in front of functions makes clear where they come from. If you want to support more functions you have other things to worry about. For example, the forced conversion from degrees to radians only makes sense with certain functions. - You need to store explicitly the number of arguments each operator expects. Your current solution is to infer that from the length of the operator's name. That is not an extensible approach.
You could try something like this:
import math
x = 5
methodToCall = getattr(math, 'sin')
result = methodToCall(x)
So you basically evaluate the string and get the corresponding method.
For the pure calculator not many functions are really needed, just like the ones that can be found in a cientific calculator. So we can implement these "primitive functions" that need direct access to the stack and extend to other functions with known number of parameters 0, 1, 2. As done in the following code
''' RPN calculator with function library extension
'''
import math
''' example of specific function libraries
'''
sample_lib = {
"par0": {
# f()
'avog': lambda: 6.02214076e+23, # Avogadro number
},
"par1": {
# f(x)
"sinc": lambda x: math.sin(x) / x,
},
"par2": {
# f(x,y)
"parallelR": lambda R1, R2: (R1 * R2) / (R1 + R2),
},
}
def any2float(number, defaultVal = 0.):
try:
retv = float(number)
except:
return defaultVal, False
return retv, True
def calcRPN (expRPN, library = sample_lib):
stack = []
RET_ERR = None
def logerror (text):
nonlocal RET_ERR
print (f"**** ERROR: {text}")
RET_ERR = "?"
return 0
def popS (pos = -1):
if not stack or pos >= 0 or -pos > len(stack):
logerror (f"something wrong with the stack at operation <{tok}> {stack}")
return 0.
return float (stack.pop (pos))
'''
define primitives (e.g. keys in a cientific calculator)
functions or lambda's can be used
'''
def powerf ():
return math.pow (popS (-2), popS ())
primitives = {
'+': lambda: popS () + popS (),
'-': lambda: -popS () + popS (),
'*': lambda: popS () * popS (),
'/': lambda: popS (-2) / popS (),
'^': powerf,
'pow': powerf,
'sqrt': lambda: math.sqrt (popS ()),
'sqr': lambda: math.pow (popS (), 2),
'sin': lambda: math.sin (popS ()),
'atan2': lambda: math.atan2 (popS (-2), popS ()),
# ...
# physical constants
'pi': lambda: math.pi,
'e': lambda: math.exp (1),
# ...
}
''' check RPN expression, accept only list or string
'''
if isinstance (expRPN, str):
onespace = expRPN.strip ()
expRPN = onespace.split (" ")
elif not isinstance (expRPN, list):
logerror (f"argument of calcRPN <{expRPN}> is not a list or a string (neither/nor)")
return RET_ERR
''' calc loop
'''
for tok in expRPN:
flo, isnum = any2float (tok)
try:
if isnum:
stack.append (flo)
elif tok in primitives:
stack.append (primitives[tok] ())
elif "par0" in library and tok in library["par0"]:
stack.append (library["par0"][tok] ())
elif "par1" in library and tok in library["par1"]:
stack.append (library["par1"][tok] (popS ()))
elif "par2" in library and tok in library["par2"]:
stack.append (library["par2"][tok] (popS (-2), popS ()))
else:
logerror (f"operation [{tok}] not implemented yet")
except ValueError as txt:
logerror (f"some numerical error at operation [{tok}] {txt}")
if RET_ERR:
return RET_ERR
if len(stack) != 1:
logerror (f"stack len = {len(stack)}, it should be just 1. Stack {stack}")
return RET_ERR
return stack.pop ()
def main():
while True:
rpnTxt = input ("enter a RPN expression: ")
if rpnTxt == "":
return
print (f"RESULT: {calcRPN (rpnTxt)}")
if __name__ == '__main__':
main()
Explore related questions
See similar questions with these tags.