4
\$\begingroup\$

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
Jamal
35.2k13 gold badges134 silver badges238 bronze badges
asked Feb 6, 2015 at 16:40
\$\endgroup\$

3 Answers 3

4
\$\begingroup\$
  • In Python, list is designed to grow efficiently from the end. Implementing a stack using append and pop at the end is therefore a better approach.
  • I would expect is_number to return False instead of the implicit None 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.
answered Feb 7, 2015 at 11:23
\$\endgroup\$
1
\$\begingroup\$

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.

answered Feb 6, 2015 at 18:22
\$\endgroup\$
1
\$\begingroup\$

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()
answered Oct 5, 2024 at 15:11
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.