Sunday, March 16, 2008

RPN Calculator

After seeing my dad's old HP calculator, I had the sudden interest to write an RPN calculator. Although completely operational (but not done), there may still be some debug stuff in the code.

stack.py

The almighty stack.

#!/usr/bin/python
class stack(object):
"""A simple stack class."""
class node(object):
def __init__(self, store, next = None):
self.nxt = next
self.dat = store
def getdata(self):
return self.dat
def getnext(self):
return self.nxt
stored = property(getdata)
next = property(getnext)
def __init__(self):
self.length = 0
self.top = None
def push(self, data):
self.top = stack.node(data, self.top)
self.length += 1
def pop(self):
if len(self):
ret = self.top.stored
todel = self.top
self.top = self.top.next
self.length -= 1
del todel
return ret
else:
return None
def peek(self, index = 0):
ret = self.top
for i in range(0, index):
ret = ret.next
return ret.stored
def list(self, sort = 'oldest'):
"""Returns a representation of the list as a stack.
sort = 'newest': Places the newest first
sort = 'oldest': Places the oldest first"""
ret = []
idx = self.top
if sort == 'oldest':
while(idx != None):
ret.insert(0, idx.stored)
idx = idx.next
elif sort == 'newest':
while(idx != None):
ret.append(idx.stored)
idx = idx.next
return ret
def __len__(self):
return self.length

rpn.py

The base RPN library.

#!/usr/bin/python
from stack import stack
import math

class StackLength(Exception):
def __init__(self, oper):
self.value = "Stack too small for operation:", oper
def __str__(self):
return repr(self.value)
class MathError(Exception):
def __init__(self, oper):
self.value = "Math exception:", oper
def __str__(self):
return repr(self.value)

class rpn(stack):
def push(self, data):
self.top = stack.node(float(data), self.top)
self.length += 1
def pop(self):
if len(self):
ret = self.top.stored
todel = self.top
self.top = self.top.next
self.length -= 1
del todel
return ret
else:
raise StackLength("X pop")
def add(self):
if len(self) < 2:
raise StackLength("X Y +")
return 0
Y = self.pop()
X = self.pop()
ret = X + Y
self.push(ret)
return ret
def root(self):
if len(self) < 2:
raise StackLength("X 1/Y ^")
return 0
Y = self.pop()
X = self.pop()
ret = math.pow(X, 1 / Y)
self.push(ret)
return ret
def sub(self):
if len(self) < 2:
raise StackLength("X Y -")
return 0
Y = self.pop()
X = self.pop()
ret = X - Y
self.push(ret)
return ret
def mul(self):
if len(self) < 2:
raise StackLength("X Y *")
return 0
Y = self.pop()
X = self.pop()
ret = X * Y
self.push(ret)
return ret
def div(self):
if len(self) < 2:
raise StackLength("X Y /")
return 0
Y = self.pop()
X = self.pop()
ret = X / Y
self.push(ret)
return ret
def swap(self):
if len(self) < 2:
raise StackLength("X Y <->")
return 0
Y = self.pop()
X = self.pop()
self.push(Y)
self.push(X)
return 0
def pow(self):
if len(self) < 2:
raise StackLength("X Y ^")
return 0
Y = self.pop()
X = self.pop()
ret = math.pow(X, Y)
self.push(ret)
return ret
def sqrt(self):
if len(self) < 1:
raise StackLength("Y sqrt")
return 0
X = self.pop()
ret = math.sqrt(X)
self.push(ret)
return ret
def sin(self):
if len(self) < 1:
raise StackLength("Y sin")
return 0
X = self.pop()
ret = math.sin(X)
self.push(ret)
return ret
def cos(self):
if len(self) < 1:
raise StackLength("Y cos")
return 0
X = self.pop()
ret = math.cos(X)
self.push(ret)
return ret
def d2r(self):
if len(self) < 1:
raise StackLength("Y d2r")
return 0
X = self.pop()
ret = math.pi * X / 180
self.push(ret)
return ret
def r2d(self):
if len(self) < 1:
raise StackLength("Y r2d")
return 0
X = self.pop()
ret = 180 * X / math.pi
self.push(ret)
return ret
def log(self, base = None):
if base == None:
if len(self) < 2:
raise StackLength("X Y logy")
return 0
Y = self.pop()
X = self.pop()
ret = math.log(X, Y)
self.push(ret)
return ret
else:
if len(self) < 1:
raise StackLength("Y log")
return 0
X = self.pop()
ret = math.log(X, base)
self.push(ret)
return ret

grpn.py

The GUI.

#!/usr/bin/python
# coding=utf8
import pygtk
pygtk.require('2.0')
import gtk
import rpn
import math

class ErrorDialog(object):
def destroy(self, widget, data=None):
self.dialog.destroy()
def __init__(self, message, parent = None):
self.dialog = gtk.Dialog(title = 'Error', parent = parent, flags = gtk.DIALOG_DESTROY_WITH_PARENT | gtk.DIALOG_MODAL)
self.button = gtk.Button('Okay')
self.dialog.action_area.pack_start(self.button, True, True, 1)
self.button.connect('clicked', self.destroy, None)
self.button.show()
self.label = gtk.Label(message)
self.dialog.vbox.pack_start(self.label)
self.label.show()
def run(self):
self.dialog.run()

class ControlWindow(object):
numbers = range(48, 58) + [46]
controls = {
65293 : 'enter',
65307 : 'escape',
112 : 'pop',
43 : 'plus',
45 : 'minus',
42 : 'times',
47 : 'divide',
94 : 'pow',
114 : 'root',
116 : 'sqrt',
108 : 'ln',
115 : 'swap',
113 : 'quit'
}
editors = {
65288 : 'backspace',
65535 : 'delete',
65361 : 'left',
65363 : 'right'
}
cmds = 36
def push(self, widget, event, data=None):
try:
self.stack.push(float(self.input.get_text()))
except ValueError, e:
#dlg = ErrorDialog('Not a valid number: ' + self.input.get_text())
#dlg.run()
self.append_log('Not a valid number: ' + self.input.get_text())
self.refresh_display()
self.input.grab_focus()
def pop(self, widget, event, data = None):
try:
self.input.set_text(str(self.stack.pop()))
except rpn.StackLength, e:
#dlg = ErrorDialog(str(e))
#dlg.run()
self.append_log(text = str(e))
self.refresh_display()
def command(self, cmd):
try:
if cmd == 'enter':
self.push(None, None)
elif cmd == 'pop':
self.pop(None, None)
elif cmd == 'swap':
self.stack.swap()
self.refresh_display()
elif cmd == 'plus':
self.stack.add()
self.refresh_display()
elif cmd == 'minus':
self.stack.sub()
self.refresh_display()
elif cmd == 'times':
self.stack.mul()
self.refresh_display()
elif cmd == 'divide':
self.stack.div()
self.refresh_display()
elif cmd == 'pow':
self.stack.pow()
self.refresh_display()
elif cmd == 'sqrt':
self.stack.sqrt()
self.refresh_display()
elif cmd == 'root':
self.stack.root()
self.refresh_display()
elif cmd == 'sin':
self.stack.sin()
self.refresh_display()
elif cmd == 'log10':
self.stack.log(10)
self.refresh_display()
elif cmd == 'ln':
self.stack.log(math.e)
self.refresh_display()
elif cmd == 'log':
self.stack.log()
self.refresh_display()
elif cmd == 'escape':
self.window.grab_focus()
elif cmd == 'quit':
gtk.main_quit()
else:
return False
return True
except rpn.StackLength, e:
#dlg = ErrorDialog(str(e))
#dlg.run()
self.append_log(text = str(e))
finally:
return True
def altcom(self, widget, event, data = None):
if isinstance(data, str):
return self.command(data)
def enter(self, widget, event, data = None):
print str(event.keyval)
if event.keyval in self.controls.keys():
cmd = self.controls[event.keyval]
return self.command(cmd)
elif event.keyval in self.numbers:
if widget == self.input:
return False
else:
self.input.append_text(str(event.keyval - 48))
return True
elif event.keyval in self.editors.keys():
return False
elif event.keyval == self.cmds:
self.cmdinput.grab_focus()
return True
else:
return True
def cmdenter(self, widget, event, data = None):
print str(event.keyval)
if event.keyval in self.controls.keys():
if self.controls[event.keyval] == 'enter':
entry = self.cmdinput.get_text()
self.cmdinput.set_text('')
if entry in self.controls.values():
self.command(entry)
else:
self.append_log("No such command: " + str(entry) + "\n")
self.input.grab_focus()
return True
elif self.controls[event.keyval] == 'escape':
self.cmdinput.set_text('')
self.input.grab_focus()
return True
#elif self.controls[event.keyval] == 'quit':
# self.command('quit')
return False
def delete_event(self, widget, event, data=None):
return False
def destroy(self, widget, data=None):
gtk.main_quit()
def append_log(self, text = ''):
#print text
buffer = self.errlog.get_buffer()
buffer.insert(buffer.get_end_iter(), text + "\n")
def refresh_display(self):
slist = self.stack.list(sort = 'newest')
for i in range(0, len(self.sinfo)):
if i < len(slist):
self.sinfo[i][1].set_text(str(slist[i]))
else:
self.sinfo[i][1].set_text('0.0')
def __init__(self):
self.stack = rpn.rpn()
self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
self.window.connect("delete_event", self.delete_event)
self.window.connect("destroy", self.destroy)
self.window.set_border_width(4)
self.window.set_title("gRPN Calculator")

self.topbox = gtk.VBox(True, 1)
self.topbox.set_homogeneous(False)
self.subbox = gtk.HBox(True, 1)
self.table = gtk.Table(2, 4, True)

# Stack view with push
self.stackvals = gtk.Table(3, 7)
self.sinfo = []
stacklen = 5;
for i in range(0, stacklen):
tlabel = gtk.Label((i == 0 and '1 (Y):') or (i == 1 and '2 (X):') or (str(i + 1) + ':'))
tinput = gtk.Entry(max = 0)
tinput.set_text('0.0')
tinput.set_editable(False)
#tinput.set_cursor_visible(False)
tmp = (tlabel, tinput)
self.sinfo.append(tmp)
self.stackvals.attach(tlabel, 0, 1, stacklen - 1 - i, stacklen - i)
if not i:
self.stackvals.attach(tinput, 1, 2, stacklen - 1 - i, stacklen - i)
self.popbutton = gtk.Button('Pop')
self.popbutton.connect('clicked', self.pop, None)
self.stackvals.attach(self.popbutton, 2, 3, stacklen - 1 - i, stacklen - i)
self.popbutton.show()
else:
self.stackvals.attach(tinput, 1, 3, stacklen - 1 - i, stacklen - i)
tlabel.show()
tinput.show()
self.label = gtk.Label('0:')
self.stackvals.attach(self.label, 0, 1, 5, 6)
self.input = gtk.Entry(max = 12)
self.input.connect('key-press-event', self.enter, None)
self.stackvals.attach(self.input, 1, 2, 5, 6)
self.pushbutton = gtk.Button('Push')
self.pushbutton.connect('clicked', self.push, None)
self.stackvals.attach(self.pushbutton, 2, 3, 5, 6)
self.pushbutton.show()
self.label.show()
self.input.show()

# command input
self.label = gtk.Label('$')
self.stackvals.attach(self.label, 0, 1, 6, 7)
self.cmdinput = gtk.Entry(max = 12)
self.cmdinput.connect('key-press-event', self.cmdenter, None)
self.stackvals.attach(self.cmdinput, 1, 3, 6, 7)
self.label.show()
self.cmdinput.show()

self.topbox.pack_start(self.subbox, False, False, 1)
self.subbox.pack_start(self.stackvals, False, False, 1)
self.stackvals.show()

# Button input
dim = (4, 4)
self.buttons = gtk.Table(dim[0], dim[1])
opers = [
('swap', '<=>'),
('plus', '+'),
('minus', '-'),
('times', u"×"),
('divide', u"÷"),
('power', '^'),
('sqrt', '√'),
('root', 'n√'),
('sin', 'sin'),
('cos', 'cos'),
('tan', 'tan'),
('r2d', 'r2d'),
('d2r', 'd2r'),
('ln', 'ln'),
('log', 'log10'),
('log_y', 'log')
]
pos = [0, 0]
for (cmd, txt) in opers:
button = gtk.Button(txt)
button.connect('clicked', self.altcom, cmd, cmd)
self.buttons.attach(button, pos[0], pos[0] + 1, pos[1], pos[1] + 1)
button.show()
if pos[0] < dim[1] - 1:
pos[0] += 1
else:
pos[0] = 0
pos[1] += 1
self.subbox.pack_start(self.buttons, False, False, 1)
self.buttons.show()




self.subbox.show()

# Error log
self.errlog = gtk.TextView()
self.errlog.set_editable(False)
self.errlog.set_cursor_visible(False)
self.errwin = gtk.ScrolledWindow()
self.errwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS)
self.errwin.add_with_viewport(self.errlog)
self.errlog.show()
self.topbox.pack_start(self.errwin, False, False, 5)
self.errwin.show()

# Quit button
self.quit = gtk.Button('X')
self.quit.connect_object("clicked", gtk.Widget.destroy, self.window)
self.topbox.pack_start(self.quit, False, False, 10)
self.quit.show()

self.window.add(self.topbox)
self.topbox.show()

self.refresh_display()
self.window.show()
self.input.grab_focus()
def main(self):
gtk.main()


window = ControlWindow()
window.main()

I don't actually use this all that often, but it's fun to play around with. Someday I'll get around to adding more functions.

No comments: