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 <enter>
5 <enter>
+ <enter>

or

3 <space> 5 <space> + <enter>

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 <space> OPERATOR <space> PROC_CODE <enter>

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__

James Edward Gray II wrote:

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.

Hello,

it's really nice, however there is one problem: if a define contains a syntax error, the calculator quits instead of just showing the error message.

Andreas

···

--
http://www.mikrocontroller.net - Das Mikrocontroller-Forum

Thank you :slight_smile:

Also, if you are interested in RPN, I have ported a FORTH to Ruby (just
because ;-)). You can find it at

http://raa.ruby-lang.org/project/ratlast/

All the RPN features you could want, plus more (ie Full Ruby).

Trivial e.g.

  require 'Atlast'

  t = Atlast.new
  t.eval(": sqr ( n -- n*n ) dup * ;")
  n = t.run("4 sqr .")

  puts "4*4 = #{n}"

Regards,

···

James Edward Gray II <james@grayproductions.net> wrote:

  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.

--
-mark. (probertm @ acm dot org)

I noticed that about the same time you posted this. It's fixed in the version below.

Did I mention it has display modes? Type "bin", or "hex". :slight_smile:

I know, it's a toy. A fun one though...

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 # support four types of numerical display
        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 SyntaxError
        puts "Syntax Error: " + $!
      rescue
        puts "Error: " + $!
      end
      print "\n" + rpn.to_s + "\n"
      print "> " if STDIN.tty?
    end
  end
end

__END__

···

On Sep 30, 2004, at 5:55 PM, Andreas Schwarz wrote:

Hello,

it's really nice, however there is one problem: if a define contains a syntax error, the calculator quits instead of just showing the error message.

Andreas