[SPOILER] RPN Calculator (take 3)

Dan Boger <[email protected]> Sat, 2 Oct 2004 08:55:24 -0400
Newsgroups gmane.comp.lang.perl.qotw.discuss
Message-ID <[email protected]>
I still don't see a copy of my reply anywhere (in mail or in the
archives), so I'll try again.

On Tue, Sep 28, 2004 at 10:14:28PM -0400, Dan Sanderson wrote:
> For this quiz, we'll implement an interactive RPN calculator.

It's pretty messy, but it gets the job done, and I wanted to make it
easy to extend. I considered using some sort of Readline, but didn't
want to introduce any module dependancies. So without any further
ramblings:

#!/usr/bin/perl -w

# Solution for QOTW 25 (RPN calculator) - Dan Boger <[email protected]>

use strict;

my @stack;

# define what we know how to do
my %operators = ( 
                  '+'   => \&op_add,
                  '-'   => \&op_sub,
                  '*'   => \&op_mult,
                  '/'   => \&op_div,
                  '%'   => \&op_mod,
                  '**'  => \&op_pow,
                  drop  => \&op_drop,  # pops the top element and discards it.
                  dropn => \&op_dropn, # pops the top N elements and discards them.
                  swap  => \&op_swap,  # swaps the 0th (top) and 1st elements in the stack.
                  clear => \&op_clear, # removes all values from the stack.
                  dup   => \&op_dup,   # duplicate the top element, push it onto the stack
                  dupn  => \&op_dupn,  # duplicate the top element N times
                  roll  => \&op_roll,  # moves the Nth element in the stack
                  load  => \&op_load,  # load stack from rpn.txt
                  save  => \&op_save,  # save stack to rpn.txt
                  quit  => sub { exit },
                );

# and what we can learn to do
my %macro;

# main prompt loop
print "Type 'help' for help.";
print "\n> ";
while (<>) {
  &ParseCmd(\@stack, $_);
  &PrintStack(\@stack);
  print "\n> ";
}

sub ParseCmd {
  my ($stackref, $cmd) = @_;

  # cleanup command
  chomp;
  s/^\s+//;
  s/\s+$//;
  
  # allow custom macro definitions
  if (/^def: (\w+)\s+(.*)/i) { 
    $macro{$1} = $2;
    print "Macro $1 defined.\n";
    return;
  }

  my @commands = split ' ';

  # actually process each command
  while (my $cmd = shift @commands) {
    if ($cmd =~ /^help$/i) {
      &Usage;
      last;
    }

    if (exists $macro{$cmd}) { # expand macros
      # place the expanded command in the front of the queue
      unshift @commands, split ' ', $macro{$cmd};
      next;
    }

    if ($cmd =~ /^0[bx]?([0-9a-f]+)$/i) {    #### parse hex, bin and oct numberd
      push @stack, oct($cmd) + 0; 
    } elsif (                                #### parse regular numbers
             $cmd =~ /^ 
                       [+-]?  # allow negative (and positive for consistancy) numbers
                       (?:
                         [\d_]+(?:\.\d+)? # plain integers (5) or real number (5.2)
                           |
                         \.\d+            # allow also naked decimals (.5)
                       )
                       (?:                # possible exponent
                         [eE]
                         [+-]?            # positive or negative
                         \d+    
                       )?$/x) {
      push @stack, $cmd + 0;   # force perl to "numerify" it
    } elsif (exists $operators{$cmd}) {      #### see if we know this operator
      # we do, so just call the coderef
      unless (defined $operators{$cmd}->(\@stack)) {
        print "Failed in '$cmd'.\n";
        last;
      }
    } else {                                 #### fallback, complain
      print "Unknown command: '$cmd'.\n";
      &Usage;
      last;
    }
  }
}

sub PrintStack {
  my $stackref = shift;

  if (@$stackref) {
    for (reverse 0..$#$stackref) {
      print "$_: $stackref->[$#$stackref - $_]\n";
    }
  } else {
    print "Empty stack.\n";
  }
}

sub Usage {
  print "Known operators: ", join ", ", "def:", sort keys %operators;
  print "\n";
  if (keys %macro) {
    print "Defined macros:\n";
    foreach (keys %macro) {
      print "  $_: $macro{$_}\n";
    }
  }
}

sub op {
  # main workhorse of the code.  Take the code in our special syntax, load, and replace
  # the vars we need, then 'eval' it.
  my ($stackref, $code) = @_;

  my $has_res = 0; # flag to signify if we're returning and pushing into the stack the result
                   # or if we just return the vars pulled
  my @res;
  my %vars;

  # get the current value of the vars from the stack
  while ($code =~ /:(\d+)/) {
    my $pos = $1;
    my $index = $#$stackref - $pos;
    if ($index < 0) {  # make sure the stack has enough data - otherwise, complain
      print "ERR: Stack not deep enough.  Need at least ", $pos + 1, " elements.\n";
      return undef;
    }

    $code =~ s/:$pos(k)?\b/\$vars{$index}->[0]/g;
    $vars{$index} = [$stackref->[$index], $1 || ""]; # store the value, and any flags
  }

  if ($code =~ s/:r/\@res/g) {  # we're required to return (and push) the result
    $has_res = 1;
  }

  # run the submitted code
  eval $code;

  if ($@) {
    # something bad happend
    print "ERR: $@\n";
    return undef;
  }

  # the eval succeeded, let's remove the vars
  foreach (sort {$b <=> $a} keys %vars) {
    next if $vars{$_}->[1] eq 'k';  # the 'k' flag means keep it - don't remove it from 
                                    # the stack
    #print "Removing $_\n";
    splice (@$stackref, $_, 1, ());
  }

  if ($has_res) {  # push and return the results
    @res = reverse @res;
    push @$stackref, @res;
    return @res;
  } else {         # just return the vars we pulled
    return map {$_->[0]} @vars{sort {$a <=> $b} keys %vars};
  }
}

# operation definitions.  most cases can be dealt with with a simple equasion
sub op_add {
  my $stackref = shift;

  return &op($stackref, ':r = :0 + :1');
}

sub op_sub {
  my $stackref = shift;

  return &op($stackref, ':r = :1 - :0');
}

sub op_mult {
  my $stackref = shift;

  return &op($stackref, ':r = :0 * :1');
}


sub op_div {
  my $stackref = shift;

  return &op($stackref, ':r = :1 / :0');
}

sub op_mod {
  my $stackref = shift;
  
  return &op($stackref, ':r = :1 % :0');
}

sub op_pow {
  my $stackref = shift;
  
  return &op($stackref, ':r = :1 ** :0');
}

sub op_drop {
  my $stackref = shift;

  return &op($stackref, ':0');
}

sub op_dropn {
  my $stackref = shift;

  # get the argument for dropn
  my ($n) = &op($stackref, ":0");

  if ($n >= 0) {
    &op($stackref, ':0') for 1..$n;
  } else {
    print "ERR: $n is not a positive number.\n";
  }
}

sub op_swap {
  my $stackref = shift;

  return &op($stackref, ':r = (:1, :0)');
}

sub op_clear {
  my $stackref = shift;

  @$stackref = ();
  return 1;
}

sub op_dup {
  my $stackref = shift;

  return &op($stackref, ':r = :0k');
}

sub op_dupn {
  my $stackref = shift;

  my $n;
  ($n) = &op($stackref, ":0");
  if ($n >= 0) {
    &op($stackref, ':r = :0k') for 1..$n;
  } else {
    print "ERR: $n is not a positive number.\n";
  }
}

sub op_roll {
  my $stackref = shift;

  my $n;
  ($n) = &op($stackref, ":0");
  if (defined $n) {
    return &op($stackref, ":r = :$n");
  } else {
    return undef;
  }
}

sub op_load {
  my $stackref = shift;

  unless (open(STACK, "<rpn.txt")) {
    print "Failed to read: $!";
    return undef;
  }

  &op_clear($stackref);
  while (<STACK>) {
    &ParseCmd($stackref, $_);
  }

  print "Loaded stack from rpn.txt.\n";
}

sub op_save {
  my $stackref = shift;

  unless (open(STACK, ">rpn.txt")) {
    print "Failed to write: $!";
    return undef;
  }

  print STACK join "\n", @$stackref;
  print STACK "\n";
  foreach (keys %macro) {
    print STACK "def: $_ $macro{$_}\n";
  }
  close STACK;

  print "Saved to rpn.txt.\n";
}


__END__

-- 
Dan Boger
[email protected]