[SPOILER] Solution for QotW #25 (RPN calculator)

[email protected] Fri, 01 Oct 2004 16:12:18 +0000
Newsgroups gmane.comp.lang.perl.qotw.discuss
Message-ID <[email protected]>
I liked this quiz for two reasons:
1) It looked simple and fun.
2) I've never really understood RPN notation, so it seemed like a good
   learning exercise. After this quiz, I just might be able to compute
   2 + 2 using an HP calculator.

I thought about using eval STRING, but decided against it. Instead, my 
solution is based around a dispatch table containing both functions and 
the number of stack arguments they expect. The functions' return value 
is added to the stack only when it's defined, so functions can return 
'undef' if they don't want that to happen.

I implemented all of the extras except the "N roll" commands, which 
seemed odd to me. In their place, I added a "rotate" command. I added a 
few things of my own, as well:

* ln     - base e logarithm (my 'log' is base 10)
* E      - base ten version of 'exp' (10 ** N).
* pi     - 3.14159...
* e      - 2.71828...
* fmt    - user defined formats for displaying the stack
* rotate - rotate the stack by N elements.
* ?      - provides a help listing of available commands
* q      - quit: exits the calculator.

#!/usr/bin/perl
BEGIN {require 5.006}
use strict;
use warnings;
use constant PI => 4 * atan2(1,1);
use constant E  => exp(1);

my @stack;
my $fmt = "%s";

my %op = (
	# arithmetic operators
	'_'    => {n => 1, f => sub {     - $_[0]    }},
	'+'    => {n => 2, f => sub { $_[0] +  $_[1] }},
	'-'    => {n => 2, f => sub { $_[0] -  $_[1] }},
	'*'    => {n => 2, f => sub { $_[0] *  $_[1] }},
	'/'    => {n => 2, f => sub { $_[0] /  $_[1] }},
	'%'    => {n => 2, f => sub { $_[0] %  $_[1] }},
	'**'   => {n => 2, f => sub { $_[0] ** $_[1] }},
	abs    => {n => 1, f => sub { abs($_[0])              }},
	int    => {n => 1, f => sub { int($_[0])              }},
	sqrt   => {n => 1, f => sub { sqrt($_[0])             }},
	sin    => {n => 1, f => sub { sin($_[0])              }},
	cos    => {n => 1, f => sub { cos($_[0])              }},
	tan    => {n => 1, f => sub { sin($_[0]) / cos($_[0]) }},
	atan2  => {n => 2, f => sub { atan2($_[0], $_[1])     }},
	ln     => {n => 1, f => sub { log($_[0])              }},
	log    => {n => 1, f => sub { log($_[0])/log(10)      }},
	exp    => {n => 1, f => sub { exp($_[0])              }},
	E      => {n => 1, f => sub { 10 ** $_[0]             }},
	rand   => {n => 1, f => sub { rand($_[0])             }},
	'&'    => {n => 2, f => sub { $_[0] & $_[1] }},
	'|'    => {n => 2, f => sub { $_[0] | $_[1] }},
	'^'    => {n => 2, f => sub { $_[0] ^ $_[1] }},
	'~'    => {n => 1, f => sub {    ~ $_[0]    }},

	# constants
	pi     => {n => 0, f => sub { PI }}, # 3.141592654...
	e      => {n => 0, f => sub { E  }}, # 2.718281828...

	# output formats
	dec    => {n => 0, f => sub { $fmt = "%s";   undef }},
	oct    => {n => 0, f => sub { $fmt = "0%o";  undef }},
	hex    => {n => 0, f => sub { $fmt = "0x%x"; undef }},
	bin    => {n => 0, f => sub { $fmt = "0b%b"; undef }},
	fmt    => {n => 1, f => sub { $fmt = $_[0];  undef }},

	# stack manipulation
	drop   => {n => 1, f => sub { undef }},
	swap   => {n => 2, f => sub { push @stack, @_;           undef }},
	clear  => {n => 0, f => sub { @stack = ();               undef }},
	dup    => {n => 1, f => sub { push @stack, $_[0], $_[0]; undef }},
	rotate => {n => 1, f => sub {
		unshift(@stack, splice(@stack, -$_[0]));
		undef;
	}},

	# program control
	q      => {n => 0, f => sub {exit}},
	'?'    => {n => 0, f => sub {help(); undef}},
);

$| = 1;
print "> ";
while (<>) {
	chomp;
	do_token($_) foreach (split);
	show_stack();
	print "> ";
}

sub do_token {
	my $t = shift;

	if (exists $op{$t}) {
		my $n = $op{$t}{n};

		unless (@stack >= $n) {
			print "ERROR: Not enough elements for operation.\n";
			return;
		}

		my @arg = $n ? splice(@stack, -$n) : ();
		my $rv  = $op{$t}{f}->(@arg);

		push @stack, $rv if defined $rv;
	}
	else {
		push @stack, ($t =~ /^0\w/) ? oct($t) : $t;
	}
}

sub show_stack {
	if (@stack) {
		my $i = $#stack;
		foreach (reverse @stack) {
			printf ("\r%2d: $fmt\n", $i--, $_);
		}
	}
	else {
		print "\r<empty>\n";
	}
}


sub help {
	print <<'EOT';
------------------------------------------------------------------------
Available Commands
------------------------------------------------------------------------

Arithmetic Operations:
    _         unary negation
    +         addition
    -         subtraction
    *         mulitplication
    /         division
    %         modulus
    **        exponentiation
    abs       absolute value
    int       integer truncation
    sqrt      square root
    sin       sine
    cos       cosine
    tan       tangent
    atan2     arctangent2
    ln        natural logarithm (base e)
    log       logarithm (base 10)
    exp       natural exponent (e ** N)
    E         base 10 exponent (10 ** N)
    rand      random number between 0 and N
    &         bitwise AND
    |         bitwise OR
    ^         bitwise XOR
    ~         bitwise NOT

Constants:
    pi        Pi (3.14159...)
    e         e  (natural logarithm base: 2.71828...)

Output Formatting:
    dec       Display stack in decimal     (base 10)
    oct       Display stack in octal       (base  8)
    hex       Display stack in hexadecimal (base 16)
    bin       Display stack in binary      (base  2)
    <X> fmt   Display stack formatted with <X> (sprintf() format).

Stack Manipulation:
    drop      Pops the top element off the stack.
    swap      Swaps the top two elements on the stack.
    clear     Clears the stack.
    dup       Duplicates the top element on the stack.
    rotate    Rotates the stack by N elements.

Program Control:
    q         Quit (Exit)
    ?         Prints this help message.
------------------------------------------------------------------------
EOT
}

__END__