From: James Edward Gray II Date: 2004-10-01T07:23:57+09:00 Subject: RPN Fun Perl's Quiz of the Week this time around was to make an RPN calculator. If you're one of those people who has fond memories of HP's RPN calculators, you might want to play with this code a little. Just thought I would share. It understand postfix notation. You can do: 3 5 + or 3 5 + It'll break it down term by term. It understands Ruby numbers like -3 and 0b101_010 and even allows commas in them. It supports most of Ruby's math routines and even better, lets you define your own with: def OPERATOR PROC_CODE Those must be on their own line. Example: def avg { |left, right| (left + right) / 2.0 } It'll know that's a binary operator because it takes two args. Anything else is considered unary. If anyone has any use for this, just tell me and I'll throw it on the RAA. I was just having a little flashback fun. Enjoy. James Edward Gray II #!/usr/bin/env ruby # class for generating RPN Calculator objects class RPNCalc attr_accessor :mode # defines methods mode() and mode = ... # handles setup after constructor def initialize( mode = "dec", stack = [ ] ) @mode = mode @stack = stack @ops = { } end ### Stack manipulation methods ### # primary input method, adds something to stack if it's numerical def push( number ) if number.kind_of? Numeric # don't touch non-numbers # the following if converts floats to ints, when it doesn't matter if number.kind_of?(Float) and number == number.to_i @stack.unshift( number.to_i ) else @stack.unshift( number ) end return top # return top value, what we just added else return nil end end # pop from stack def drop( ) return unary { nil } end # swap top two elements from stack def swap( ) return binary do |l, r| push( r ) l end end # empty stack def clear( ) return @stack = [ ] end def dup( ) return unary do |num| push( num ) num end end # arbitrary stack position swap (up) def roll( ) if @stack.size < top raise "Insufficient elements on stack for operation." end return unary { |num| @stack.delete_at(num) } end # arbitrary stack position swap (down) def rolld( ) if @stack.size < top raise "Insufficient elements on stack for operation." end return binary do |num, n| @stack.insert(n, num) nil end end ### Basis math methods ### def add( ) return binary { |l, r| l + r } end def sub( ) return binary { |l, r| l - r } end def mul( ) return binary { |l, r| l * r } end def mod( ) return binary { |l, r| l % r } end def pow( ) return binary { |l, r| l ** r } end def div( ) return binary do |l, r| # defeat Ruby's integer division if l.integer? and r.integer? and l % r != 0 l / r.to_f else l / r end end end ### To int methods ### def floor( ) return unary { |num| num.floor } end def ceil( ) return unary { |num| num.ceil } end def round( ) return unary { |num| num.round } end ### Higher math methods ### def abs( ) return unary { |num| num.abs } end def sqrt( ) return unary { |num| Math.sqrt( num ) } end def exp( ) return unary { |num| Math.exp( num ) } end def log( ) return unary { |num| Math.log( num ) } end def log10( ) return unary { |num| Math.log10( num ) } end def sin( ) return unary { |num| Math.sin( num ) } end def cos( ) return unary { |num| Math.cos( num ) } end def tan( ) return unary { |num| Math.tan( num ) } end def sinh( ) return unary { |num| Math.sinh( num ) } end def cosh( ) return unary { |num| Math.cosh( num ) } end def tanh( ) return unary { |num| Math.tanh( num ) } end def asin( ) return unary { |num| Math.asin( num ) } end def acos( ) return unary { |num| Math.acos( num ) } end def atan( ) return unary { |num| Math.atan( num ) } end def asinh( ) return unary { |num| Math.asinh( num ) } end def acosh( ) return unary { |num| Math.acosh( num ) } end def atanh( ) return unary { |num| Math.atanh( num ) } end def atan2( ) return binary { |l, r| Math.atan2( l, r ) } end ### Bitewise manipulation methods ### # Warning: These methods convert their operands to ints before operation def bit_and( ) return binary { |l, r| l.to_i & r.to_i } end def bit_or( ) return binary { |l, r| l.to_i | r.to_i } end def bit_xor( ) return binary { |l, r| l.to_i ^ r.to_i } end def bit_neg( ) return unary { |num| ~num.to_i } end ### Operator definition methods ### # adds Ruby code as new operator def define_op( op, &code ) @ops[op] = code end ### Input/Output methods ### # parses and executes expressions in postfix notation # also understands "def OP RUBY_PROC_CODE" on it's own line def calc( postfix_exp ) if postfix_exp =~ /^\s*def\s+(\S+)\s+(\{.+\})\s*$/ # define new op define_op( $1.downcase, &eval( "proc #{$2}" ) ) else # ... or process terms terms = postfix_exp.downcase.split(" ") terms.each do |t| if @ops.include? t # use custom op definition if @ops[t].arity == 2 # choose handler by proc args binary &@ops[t] else unary &@ops[t] end next end case t # ... or hardcoded definition when "+" then add when "-" then sub when "*" then mul when "%" then mod when "**" then pow when "/" then div when "&" then bit_and when "|" then bit_or when "^" then bit_xor when "~" then bit_neg when /^-?(?:\d[,_\d]*|0[,_0-7]+|0x[,_0-9a-f]+|0b[,_01]+)$/, /^-?\d[,_\d]*\.\d[,_\d]*(?:e-?[,_\d]+)?$/ push( eval( t.tr(",", "") ) ) when "drop", "swap", "clear", "dup", "roll", "rolld", "floor", "ceil", "round", "abs", "sqrt", "exp", "log", "log10", "sin", "cos", "tan", "sinh", "cosh", "tanh", "asin", "acos", "atan", "asinh", "acosh", "atanh", "atan2" send( t.to_sym ) when "bin", "dec", "hex", "oct" @mode = t else raise "Invalid term: #{t}." end end end return top end # getter for top of stack def top( ) return @stack[0] end # primary output method, display stack with optional limit def to_s( limit = @stack.size ) if @stack.size == 0 return "Empty Stack\n" else case @mode when "bin" return (0...limit).to_a.reverse.inject("") do |str, i| str + "#{i}: #{'%b' % @stack[i]}\n" end when "dec" return (0...limit).to_a.reverse.inject("") do |str, i| str + "#{i}: #{@stack[i]}\n" end when "hex" return (0...limit).to_a.reverse.inject("") do |str, i| str + "#{i}: #{'%x' % @stack[i]}\n" end when "oct" return (0...limit).to_a.reverse.inject("") do |str, i| str + "#{i}: #{'%o' % @stack[i]}\n" end end end end private ### Operator processing methods ### def unary( &op ) if @stack.size < 1 raise "Insufficient elements on stack for operation." end return push( op.call( @stack.shift ) ) end def binary( &op ) if @stack.size < 2 raise "Insufficient elements on stack for operation." end right, left = @stack.slice!(0, 2) return push( op.call( left, right ) ) end end # basic stand-alone interface, just process lines and show the stack... if __FILE__ == $0 rpn = RPNCalc.new print "\n" + rpn.to_s + "\n" print "> " if STDIN.tty? while line = ARGF.gets line.chomp! if STDIN.tty? and line =~ /^q(?:uit)?|exit$/i break else begin rpn.calc line rescue puts "Error: " + $! end print "\n" + rpn.to_s + "\n" print "> " if STDIN.tty? end end end __END__