Python sol'n to QOTW #25 "RPN calculator"

Andrew Dalke <dalke-DxsMES/F/[email protected]> Sat, 2 Oct 2004 02:56:55 -0600
Newsgroups gmane.comp.lang.perl.qotw.discuss
Message-ID <[email protected]>
Here's my Python solution to the RPN calculator.  The implementation
uses a dispatch table.  All handlers get access to the calculator,
in case they want to manipulate the stack.  I have an adapter class
to make it easy to add normal functions (unary, binary, and one
ternary -- pow(x,y,z) == x**y % z) .  My tokenizer is easier than
the perl ones because I can let Python do the work for me.  An
advantage in this case to strong type checking.  ;)

What's unusual about mine is that I support strings ('+ is the string
"+" instead of the command "+", 'cos is a string, etc.  But note
that the word cannot contain whitespace.

I also support simple macro-like functions and an if/then/else
statement.  Someone asked why roll/rolld were useful.  I have
a few functions which use them.  They are needed to get a number
from further up the stack into operating range.

To make the 'if' and 'def'ine statements easier to input, I
introduced a simple input scheme which disables interpretation
while those are being defined.  Otherwise I would need to quote
the words in the middle and that looked ugly.

Here it is in action.

Simple RPN calculator.
Use '?' to get a list of commands, 'quit' to exit
 > ?
'!' '%' '&' '*' '**' '+' '-' '/' '//' '<' '<<' '<=' '==' '>' '>=' '>>'  
'?' '^' 'abs' 'acos' 'asin' 'atan2' 'bin' 'ceil' 'clear' 'cos' 'cosh'  
'dec' 'def' 'degrees' 'drop' 'dup' 'e' 'enddef' 'endif' 'exec' 'exp'  
'fib' 'float' 'floor' 'hex' 'hypot' 'if' 'int' 'log' 'log10' 'nop'  
'oct' 'pi' 'pow' 'quit' 'radians' 'roll' 'rolld' 'sin' 'sinh' 'sqrt'  
'swap' 'troff' 'tron' '|' '~'


 > 2 4 5 ** **
0:  
179769313486231590772930519078902473361797697894230657273430081157732675 
805500963132708477322407536021120113879871393357658789768814416622492847 
430639474124377767893424865485276302219601246094119453082952085005768838 
150682342462881473913110540827237163350510684586298239947245938479716304 
835356329624224137216

 > hex
0:  
0x1000000000000000000000000000000000000000000000000000000000000000000000 
000000000000000000000000000000000000000000000000000000000000000000000000 
000000000000000000000000000000000000000000000000000000000000000000000000 
0000000000000000000000000000000000000000000

 > 169 ! /
0: 4210.97328754

 > dec
0: 4210.97328754

 > dup 0xf swap 1 -
2: 4210.97328754
1: 15
0: 4209.97328754

 > sqrt
2: 4210.97328754
1: 15
0: 64.8843069435

 > clear e
0: 2.71828182846

 > log
0: 1.0

 > exp
0: 2.71828182846

 > drop 81 sqrt
0: 9.0

 > 3 4 atan2
1: 9.0
0: 0.643501108793

 > degrees
1: 9.0
0: 36.8698976458

 > 0b100001 | 0b101001
Processing stopped with '|' because: unsupported operand type(s) for |:  
'float' and 'int'
2: 9.0
1: 36.8698976458
0: 33

 > clear 0b100001 0b101001 |
0: 41

 > bin
0: 0b101001

 > drop 0b010001 0b101001 &
0: 0b1

 > drop dec

             Here's an example if an if statement and using a string

 > clear 25 if dup sqrt int 2 ** == then 'square else 'not-square endif
0: square

 > clear 80 if dup sqrt int 2 ** == then 'square else 'not-square endif
0: not-square

 > clear

            Turn that into a function

 > def is-square if dup sqrt int 2 ** == then 'square else 'not-square  
endif enddef

 > 80 is-square
0: not-square

 > clear 81 is-square
0: square

 > clear 10000 is-square
0: square


             No loops, but I can do functional programming

 > # recusive solution to drop the top N elements

 > def dropN if dup 0 > then 1 - swap drop dropN else drop endif enddef

 > 101 102 103 104 105 106 107 108 109 110
9: 101
8: 102
7: 103
6: 104
5: 105
4: 106
3: 107
2: 108
1: 109
0: 110

 > 5 dropN
4: 101
3: 102
2: 103
1: 104
0: 105

These new functions are included in the available functions list
 > clear

 > ?
'!' '%' '&' '*' '**' '+' '-' '/' '//' '<' '<<' '<=' '==' '>' '>=' '>>'  
'?' '^' 'abs' 'acos' 'asin' 'atan2' 'bin' 'ceil' 'clear' 'cos' 'cosh'  
'dec' 'def' 'degrees' 'drop' 'dropN' 'dup' 'e' 'enddef' 'endif' 'exec'  
'exp' 'fib' 'float' 'floor' 'hex' 'hypot' 'if' 'int' 'is-square' 'log'  
'log10' 'nop' 'oct' 'pi' 'pow' 'quit' 'radians' 'roll' 'rolld' 'sin'  
'sinh' 'sqrt' 'swap' 'troff' 'tron' '|' '~'

      Here's a more complicated program (shown without the prompts)
      It finds the roots of a quadratic equation given a b c on the stack

# Compute the discriminant.  Stack must have a b c
def discriminant
   swap dup 2 ** 3 roll dup 4 rolld 3
   roll dup 3 rolld 4 * * -
enddef

# Given a b c, compute both roots.
# If it is complex, put the string 'imaginary' on the stack
# but do not touch a b c .  Otherwise consume a b c
# and replace them with the two solutions.
def roots
   discriminant
   if dup 0 < then drop 'imaginary else
     sqrt                # we have a b c d
     swap drop           # a b d
     2 roll 2 * dup      # b d 2*a 2*a
     2 roll swap / dup   # b 2*a d/(2*a) d/(2*a)
     3 rolld 3 rolld     # d/(2*a) d/(2*a) b 2*a
     / -1 * dup 3 rolld  #  -b/(2*a) d/(2*a) d/(2*a) -b/(2*a)
     + 2 rolld -         # (-b+d)/(2*a)   (-b-d)/(2*a)
   endif
enddef


 > -1 2 3 roots
1: -1.0
0: 3.0

 > clear 1 2 3 roots
3: 1
2: 2
1: 3
0: imaginary

 > clear 2 8 3 roots
1: -0.418861169916
0: -3.58113883008

 > clear 2 -4 2 roots
1: 1.0
0: 1.0

 >

Finally, it uses the platform's readline library if available.

					Andrew
					dalke-DxsMES/F/[email protected]

# Python solution to Perl Quiz of the Week #25 "RPN Calculator"
#  by Andrew Dalke < dalke @ dalke scientific . com >
# Contributed to the public domain Oct. 1, 2004
#
# The calculator is simple.  As words come in, either process
# them using entries in the dispatch table (if a string)
# or append to the stack.
#
# The handlers in the dispatch table are free to manipulate
# the calculator.  Usually they just modify the stack.  They
# may also change the function used to convert stack elements
# for the different display modes.
#
# Note: the oct, hex, and bin display modes only work on
# integers and not on floats.
#
# The 'SimpleFuncHandler' is an adapter to support existing
# Python functions.
#


import math, inspect, sys, operator

import readline  # enable readline support for raw_input

# Wrapper for calling a built-in function.
#   Get the args from the stack and call.
#   Append the result to the stack.
#   If there was a failure, revert the args to the stack
class SimpleFuncHandler:
     def __init__(self, func, nargs, name):
         self.func = func
         self.name = name
         self.nargs = nargs

     def __call__(self, rpncalc):
         # Only remove elements from the stack when the call succeeds
         if len(rpncalc.stack) < self.nargs:
             raise AssertionError(
                 "%s takes %d parameters, only %d available" % (
                 self.name, self.nargs, len(rpncalc.stack)))

         if self.nargs == 0:
             rpncalc.stack.append(self.func())
         else:
             terms = rpncalc.stack[-self.nargs:]
             result = self.func(*terms)
             rpncalc.stack[-self.nargs:] = [result]


class RPNCalc:
     def __init__(self):
         self.stack = []
         self.handlers = {}
         self.display_mode = display_dec
         self.do_quoting = 0
         self.stop_word = ""
         self.trace = 0

     def quote_until(self, stop_word):
         self.do_quoting = 1
         self.stop_word = stop_word

     def run(self, word = None):
         if word is None:
             word = self.stack.pop(-1)
         if word in self.handlers:
             if self.trace:
                 print "run", word, "with", self.stack[-5:], " --> ",
             self.handlers[word](self)
             if self.trace:
                 print self.stack[-5:]
         else:
             raise TypeError("unknown command %r" % (word,))

     def process(self, word):
         if word == "!'":  # stop quoting
             self.do_quoting = 0
             return

         if isinstance(word, basestring):
             if self.do_quoting:
                 if word == self.stop_word:
                     self.do_quoting = 0
                     self.run(word)
                 else:
                     self._append(word)
             else:
                 if word.startswith("'"):
                     self._append(word[1:])
                 else:
                     self.run(word)
         else:
             self._append(word)

     def _append(self, word):
         self.stack.append(word)
         if self.trace:
             print "Append", word, "to get", self.stack[-5:]

     def add_handler(self, handler, name = None):
         if name is None:
             name = handler.__name__
         self.handlers[name] = handler

     def display(self, outfile):
         for i, val in zip(range(len(self.stack)-1, -1, -1),
                           self.stack):
             print >>outfile, "%d: %s" % (i, self.display_mode(val))

# Some display functions (for 'dec', 'hex', 'bin' and 'oct' modes)

def display_dec(arg):
     return str(arg)

def display_hex(arg):
     if isinstance(arg, int):
         return hex(arg)
     elif isinstance(arg, long):
         return hex(arg)[:-1]    # remove the terminal 'L'
     return str(arg)

def display_oct(arg):
     if isinstance(arg, int):
         return oct(arg)
     elif isinstance(arg, long):
         return oct(arg)[:-1]    # remove the terminal 'L'
     return str(arg)

# Python doesn't have a 'bin' function.  Make one based on hex.
_hex2bin = {
     "0": "0000", "1": "0001", "2": "0010", "3": "0011", "4": "0100",
     "5": "0101", "6": "0110", "7": "0111", "8": "1000", "9": "1001",
     "a": "1010", "b": "1011", "c": "1100", "d": "1101", "e": "1110",
     "f": "1111"}

def bin(x):
     s = hex(x)
     if s[-1:] == "L":
         s = s[:-1]
     t = "".join(map(_hex2bin.__getitem__, s[2:]))
     # Remove any leading 0s.  If none, this returns -1
     # which is exactly what's needed to return "0b0"
     i = t.find("1")
     return "0b" + t[i:]

def display_bin(arg):
     if isinstance(arg, int) or isinstance(arg, long):
         return bin(arg)
     return str(arg)

class StackDefinedHandler:
     def __init__(self, commands):
         self.commands = commands
     def __call__(self, rpncalc):
         for command in self.commands:
             rpncalc.process(command)


def _rfind(stack, name, i = None):
     if i is None:
         i = len(stack)-1
     while i >= 0:
         if stack[i] == name:
             return i
         i = i - 1
     raise TypeError("Could not find %r in stack" % (name,))

# Kind of a hack.  Used to get a list of all the stack ops
_old_globals = globals().keys()

############# Start of the stack functions

# quit
def quit(rpncalc):
     sys.exit(0)

# drop    pops the top element and discards it.
def drop(rpncalc):
     del rpncalc.stack[-1]

# swap    swaps the 0th (top) and 1st elements in the stack.
def swap(rpncalc):
     stack = rpncalc.stack
     stack[-2], stack[-1] = stack[-1], stack[-2]

# clear   removes all values from the stack.
def clear(rpncalc):
     del rpncalc.stack[:]

# dup     duplicate the top element, push it onto the stack
def dup(rpncalc):
     rpncalc.stack.append(rpncalc.stack[-1])

# roll     An "N roll" command, which moves the Nth element in the
# stack to the 0th position, pushing the other elements up (where
# N is a value popped from the top of the stack).  It is an error
# if there are not N elements in the stack.
def roll(rpncalc):
     i = rpncalc.stack.pop(-1)
     if not isinstance(i, int):
         raise TypeError("can only roll with integers")
     if i < 0:
         raise TypeError("can not roll using negative numbers")
     if i == 0:
         return
     if i >= len(rpncalc.stack):
         raise TypeError("roll value larger than the stack")
     i = -i-1
     val = rpncalc.stack.pop(i)
     rpncalc.stack.append(val)

# rolld   "N rolld" would move the 0th element in the stack to the
# Nth position, dropping the other elements down.
def rolld(rpncalc):
     i = rpncalc.stack.pop(-1)
     if not isinstance(i, int):
         raise TypeError("can only rolld with integers")
     if i < 0:
         raise TypeError("can not rolld using negative numbers")
     if i == 0:
         return
     if i >= len(rpncalc.stack):
         raise TypeError("rolld value larger than the stack")
     i = -i-1
     rpncalc.stack.insert(i, rpncalc.stack[-1])
     del rpncalc.stack[-1]


def dir_(rpncalc):
     ops = rpncalc.handlers.keys()
     ops.sort()
     print " ".join(map(repr, ops))
dir_.__name__ = "?"

# suffix with "_" because hex and oct are builtins
def dec_(rpncalc):
     rpncalc.display_mode = display_dec
dec_.__name__ = "dec"

def bin_(rpncalc):
     rpncalc.display_mode = display_bin
bin_.__name__ = "bin"

def oct_(rpncalc):
     rpncalc.display_mode = display_oct
oct_.__name__ = "oct"

def hex_(rpncalc):
     rpncalc.display_mode = display_hex
hex_.__name__ = "hex"

# Use the name of the top item as the command to run
def exec_(rpncalc):
     word = rpncalc.stack.pop(-1)
     rpncalc.run(word)
exec_.__name__ = "exec"

def nop(rpncalc):
     pass

def if_(rpncalc):
     rpncalc.stack.append("if")
     rpncalc.quote_until("endif")
if_.__name__ = "if"

def endif(rpncalc):
     try:
         else_pos = _rfind(rpncalc.stack, "else")
     except TypeError:
         else_pos = None
     then_pos = _rfind(rpncalc.stack, "then", else_pos)
     if_pos = _rfind(rpncalc.stack, "if", then_pos)
     if_commands = rpncalc.stack[if_pos+1:then_pos]
     then_commands = rpncalc.stack[then_pos+1:else_pos]
     if else_pos is not None:
         else_commands = rpncalc.stack[else_pos+1:]
     else:
         else_commands = []

     del rpncalc.stack[if_pos:]
     for command in if_commands:
         rpncalc.process(command)
     val = rpncalc.stack.pop(-1)
     if val:
         for command in then_commands:
             rpncalc.process(command)
     else:
         for command in else_commands:
             rpncalc.process(command)

def def_(rpncalc):
     rpncalc.stack.append("def")
     rpncalc.quote_until("enddef")
def_.__name__ = "def"

def enddef(rpncalc):
     def_pos = _rfind(rpncalc.stack, "def")
     name_pos = def_pos + 1
     if name_pos >= len(rpncalc.stack):
         raise TypeError("No name given to new function")
     name = rpncalc.stack[name_pos]
     commands = rpncalc.stack[name_pos+1:]
     del rpncalc.stack[def_pos:]
     rpncalc.handlers[name] = StackDefinedHandler(commands)


def tron(rpncalc):
     rpncalc.trace = 1

def troff(rpncalc):
     rpncalc.trace = 0


############## end of stack function definitions

stack_funcs = dict([(k, v) for (k, v) in globals().items()
                              if (k not in _old_globals) and  
callable(v)])


del _old_globals, k, v

#### some extra functions

def factorial(n):
     if n < 1:
         raise TypeError("can only find factorials of positive numbers")
     if not (isinstance(n, int) or isinstance(n, long)):
         raise TypeError("can only find factorials of integers")
     fact = 1L
     while n > 1:
         fact *= n
         n -= 1
     return fact

def fib(n):
     if n < 1:
         raise TypeError("can only find fib(n) for n > 1")
     if not (isinstance(n, int) or isinstance(n, long)):
         raise TypeError("can only find fib(n) of integers")
     if n == 1:
         return 1
     if n == 2:
         return 1
     x = 1L
     y = 1L
     while n > 2:
         x, y = y, x+y
         n -= 1
     return y

####

# Let Python do the hard work of figuring out if the
# number is a number type.
def convert_type(s):
     # Is it a binary value?  Python doesn't understand
     # the "0b" notation so handle it ourselves.
     if s[:2] == "0b":
         return int(s[2:], 2)

     # Is it an integer?
     try:
         return int(s, 0)
     except ValueError:
         pass

     # A float?
     try:
         return float(s)
     except ValueError:
         pass

     # Unsupported data type.  Treat as a string
     return s


def main():
     rpncalc = RPNCalc()

     def addf(func, nargs = None, name = None):
         if nargs is None:
             nargs = len(inspect.getargspec(func)[0])
         if name is None:
             name = func.__name__
         rpncalc.add_handler(SimpleFuncHandler(func, nargs, name), name)

     addf(operator.add, 2, "+")
     addf(operator.sub, 2, "-")
     addf(operator.mul, 2, "*")
     addf(operator.truediv, 2, "/")
     addf(operator.floordiv, 2, "//")
     addf(pow, 2, "**")
     addf(pow, 3, "pow")           # x**y mod z
     addf(abs, 1, "abs")
     addf(operator.gt, 2, ">")
     addf(operator.ge, 2, ">=")
     addf(operator.eq, 2, "==")
     addf(operator.le, 2, "<=")
     addf(operator.lt, 2, "<")
     addf(operator.mod, 2, "%")
     addf(operator.lshift, 2, "<<")
     addf(operator.rshift, 2, ">>")
     addf(operator.and_, 2, "&")
     addf(operator.or_, 2, "|")
     addf(operator.xor, 2, "^")
     addf(operator.invert, 1, "~")

     addf(int, 1)
     addf(float, 1)
     addf(lambda :math.pi, 0, "pi")
     addf(lambda :math.e, 0, "e")
     addf(math.cos, 1)
     addf(math.sin, 1)
     addf(math.cosh, 1)
     addf(math.sinh, 1)
     addf(math.exp, 1)
     addf(math.log, 1)
     addf(math.log10, 1)
     addf(math.sqrt, 1)
     addf(math.atan2, 2)
     addf(math.asin, 1)
     addf(math.acos, 1)
     addf(math.radians, 1)
     addf(math.degrees, 1)
     addf(math.ceil, 1)
     addf(math.floor, 1)
     addf(math.hypot, 2)

     addf(factorial, name = "!")
     addf(fib)

     add = rpncalc.add_handler
     for k, v in stack_funcs.items():
         rpncalc.add_handler(v)

     print "Simple RPN calculator."
     print "Use '?' to get a list of commands, 'quit' to exit"
     while 1:
         try:
             inp = raw_input("> ")
         except EOFError:
             break
         i = inp.find("#")
         if i != -1:
             inp = inp[:i]
         words = inp.split()
         for word in words:
             word = convert_type(word)
             try:
                 rpncalc.process(word)
             except SystemExit:
                 raise
             except:
                 print "Processing stopped with %r because: %s" % (
                     word, sys.exc_info()[1])
                 break
         if not rpncalc.do_quoting:
             # we're defining a function or if statement
             rpncalc.display(sys.stdout)
             print

if __name__ == "__main__":
     main()