[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__