Re: [SPOILER] Ruby RPNCalc (Solution to Quiz #25)
Mike Stok <[email protected]> Sun, 3 Oct 2004 10:13:24 -0400 (EDT)
| Newsgroups | gmane.comp.lang.perl.qotw.discuss |
|---|---|
| Message-ID | <[email protected]> |
On Sat, 2 Oct 2004, Mike Stok wrote:
> Hmmm, args = @stack.shift(n_args) worked for the 2004-09-17 snapsoot, but
>
> args = @stack.slice!(0, n_args)
>
> seems more likely to succeed!
Given a moment to work, the following code seems a reasonable Ruby
attempt.
Mike
#!/usr/bin/env ruby
require 'mathn'
require 'readline'
include Readline
$VERBOSE = true
class RpCalc
# class implementing a reverse polish calculator for perl
# quiz of the week 25
# quick & easy way to deal with the 2 / 3 = 1 problem :-)
require 'mathn'
# @@procs contains procs for anything whose effect on the
# stack is predictable in terms of number of arguments required
# and number of results returned.
#
# "Irregular" operations are handled in do()
@@procs = Hash.new
def RpCalc.add_proc(name, &p)
@@procs[name] = p
end
# binary ops which Ruby can handle for me
%w{ + - * / % ** }.each do |op|
s = op.to_sym
add_proc(op) { |a, b| b.send(s, a) }
end
# self multilation (Ruby uses -@ as the unary - message)
{ 'neg' => :-@, 'abs' => :abs, 'int' => :to_i }.each_pair do |op, sym|
add_proc(op) { |a| a.send(sym) }
end
# other operations
%w{ cos sin exp log sqrt atan2 }.each do |op|
add_proc(op, &Math.method(op.to_sym))
end
add_proc('swap') { |a, b| [b, a] }
add_proc('drop') { |a| [] }
# n.b. can't dup Fixnums, hence rescue...
add_proc('dup') { |a| [a, (a.dup rescue a)] }
def initialize
do_clear
do_dec
end
# Enter value(s) onto the stack
def enter(*values)
values.each do |value|
@stack.unshift(value)
end
self
end
# Perform an operation on data in the stack
#
# operations are either procs in the @@procs hash, or implemented
# as a do_<op>.
def do(*ops)
ops.each do |op|
if p = @@procs[op]
n_args = p.arity
@stack.size < n_args and
raise RuntimeError, "stack too empty (need #{n_args} items)"
args = @stack.slice!(0, n_args)
@stack.unshift(*p[*args])
else
begin
self.send("do_#{op}")
rescue NoMethodError
raise RuntimeError, "bad command '#{op}'"
end
end
end
self
end
# Return contents of the stack as "normal" ruby types
#
# This hides mathn Rational results from the outside world.
# Don't know if it's necessary or not.
def to_a
@stack.collect { |n| n.integer? ? n.to_i : n.to_f rescue n }
end
def to_s
rows = []
self.to_a.each_with_index do |val, i|
rows << "#{i}: #{format(val)}"
end
rows.reverse.join("\n")
end
protected
# clear the stack
def do_clear; @stack = []; end
def do_roll
n = check_roll_args('roll')
@stack.unshift(@stack.delete_at(n))
end
def do_rolld
n = check_roll_args('rolld')
@stack.insert(n, @stack.shift)
end
# check that the stack is set up appropriately for roll / rolld
#
# return the index of the "non-top" element or raise a RuntimeError
def check_roll_args(name)
@stack.size >= 1 or raise RuntimeError, "stack empty"
n = @stack.shift.to_i
n >= 0 or raise ArgumentError, "#{name} needs 0 or +ve arg"
@stack.size > n or
raise RuntimeError, "stack not full enough for #{n} #{name}"
n
end
# number base & prefix used for output formatting only
def do_bin; @base, @prefix = 2, '0b'; end
def do_oct; @base, @prefix = 8, '0'; end
def do_dec; @base, @prefix = 10, ''; end
def do_hex; @base, @prefix = 16, '0x'; end
# Format a value so that we can tell if an integer is being
# displayed in a "wierd" base.
def format(n)
n.integer? or return n.to_s
n.zero? and return '0'
sign = n < 0 ? '-' : ''
sign + @prefix + n.abs.to_s(@base)
end
end
calc = RpCalc.new
loop do
break unless line = readline('> ', true)
line.split(' ').each do |item|
if num = (Integer(item) rescue Float(item) rescue nil)
calc.enter(num)
else
begin
calc.do(item)
rescue
puts "oops: #{$!}"
break # ditch rest of line
end
end
end
puts calc
end
--
[email protected] | The "`Stok' disclaimers" apply.
http://www.stok.co.uk/~mike/ | GPG PGP Key 1024D/059913DA
| Fingerprint 0570 71CD 6790 7C28 3D60
| 75D2 9EC4 C1C0 0599 13DA