[SPOILER] Ruby RPNCalc (Solution to Quiz #25)

James Edward Gray II <james-AUi9nNu29NfWNcQ1/[email protected]> Fri, 1 Oct 2004 09:51:19 -0500
Newsgroups gmane.comp.lang.perl.qotw.discuss
Message-ID <[email protected]>
I better start with a big thanks to the quiz writer, because I thought 
this one was a blast.  Especially for those of us that have found 
memories of our HP calculators.

Below is my solution in Ruby.  I tried to be pretty liberal with my 
comments, to make it easier to follow, even if you aren't a Ruby 
person.  Unfortunately, I think it requires Ruby 1.8.x, which many 
people would probably need to install.  Sorry about that.

I believe my solution covers everything in the quiz and then some, but 
I better qualify a few points...

On Sep 28, 2004, at 9:14 PM, Dan Sanderson wrote:

> The calculator should support at least the following operators, whose
> functions are similar to those in Perl: + - * / % **
>
> The calculator should support the following stack manipulation 
> commands:
>
>   drop    pops the top element and discards it.
>   swap    swaps the 0th (top) and 1st elements in the stack.
>   clear   removes all values from the stack.
>   dup     duplicate the top element, push it onto the stack

These are all in there as expected.

Learned an important lesson in here too.  In Ruby, 16 / 5 = 3!  
<laughs>  Yes, Ruby uses integer division.  At first I considered this 
a feature, and almost left it alone.  You could of course get a 
floating point answer with 16 / 5.0 = 3.2.  After much though, I 
decided this wasn't proper behavior for a calculator though, and worked 
around Ruby's math.  16 / 5 == 3.2 in RPNCalc.

> This specification lends itself easily to extension.  Feel free to add
> features you think might make the calculator more useful.  A few
> ideas:
>
> * Support for additional Perl builtin operations:
>         & | ~ abs int cos sin exp log sqrt atan2

I didn't do int, in favor of floor.  Here's what I did along these 
lines:

floor, ceil, round
abs, sqrt,
exp, log, log10,
sin, cos, tan,
asin, acos, atan,
sinh, cosh, tanh,
asinh, acosh, atanh,
atan2

I also added the bitwise operators &, |, ^, and ~, but there's a gotcha 
here.  Ruby's version of these operators work on integers only and I 
didn't work around this.  So, if you use these ops on floats, they are 
converted to ints before the operation.

> * Accept numbers entered in hexadecimal beginning in "0x", binary
>   beginning in "0b", or octal beginning in "0".

I let Ruby read entered numbers for me, gaining all this for free.  
Ruby already ignores _ in numbers, but I added a little preprocessing 
to strip ,s as well, so you can use those in your numbers too.  Unary - 
works as expected, as long as you don't put a space between it and the 
number.

A possible gotcha:  Ruby requires a digit on both sides of the . in a 
float.  .5 must be entered as 0.5.

> * Support different display modes for how the stack is printed, with
>   commands to switch modes.  "dec", "bin", "oct" and "hex" would
>   switch the display to decimal, binary, octal or hexadecimal,
>   respectively.  (These commands do not affect the contents of the
>   stack.)

Check.  It's in there.

> * 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).

and

>   Similarly, "N rolld" would move the 0th element in the stack to the
>   Nth position, dropping the other elements down.

These are in there, but I wasn't 100% clear on how they were to be 
read.  Here's how I did it.  "2 roll" would push 2 onto the stack 
first.  "roll" would then pop it off and use it to perform it's 
operation on the stack (after 2 is gone) as described in the quiz.  The 
effect is identical, but it's processed as two separate terms.  I 
wasn't clear if this was intended, but its how I handled it.

Interesting Additional Features of RPNCalc

1.  RPNCalc is designed as a module, so it's easy to use in any Ruby 
program.  You can just "require" the module and use RPNCalc object's 
normally.  If executed directly, it just loops over STDIN, handling 
file reads or interactive calculation.

2.  Ruby's math handles arbitrary integers for free, so RPNCalc does 
too.  RPNCalc has no trouble with something like:

12,000,000 2 ** 2 ** 2 ** 2 ** ...

3.  RPNCalc allows you to define your own operators or override 
built-ins.  The syntax is:

def <space> OPERATOR <space> { RUBY_PROC_CODE } <newline>

Those must be on their own line.

If the Proc object generated by the code takes two arguments, it will 
be handled as a binary operator.  Otherwise, it's considered unary.

Here's a sample "avg" operator:

def avg { |left, right| (left + right) / 2.0 }

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
			format = '%s'	# support four types of numerical display
			case @mode
				when "bin" then format = '%b'
				when "hex" then format = '%x'
				when "oct" then format = '%o'
			end
			return (0...limit).to_a.reverse.inject("") do |str, i|
				str + "#{i}: #{format % @stack[i]}\n"
			end
		end
	end
	
	private
	
	### Operator processing methods ###
	
	# the following methods control most of RPNCalc's functionality
	# they handle bounds checking as well as ensuring the proper
	# pushing and popping for operators
	# the specific operations performed are left to passed block
	# of Ruby code
	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	# create object defined above, to drive application

	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
				if line =~ /^\s*def\s+(\S+)\s+\{.+\}\s*$/
					puts "New operator #{$1.downcase} defined."
				end
			rescue SyntaxError
				puts "Syntax Error:  " + $!
			rescue
				puts "Error: " + $!
			end
			print "\n" + rpn.to_s + "\n"
			print ">  " if STDIN.tty?
		end
	end
end

__END__