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