« RPN Calculator in PowerShell | Main | Random Slashdot Stuff for Auction on eBay »

October 17, 2007

Hacking on the PowerShell RPN Calculator

I couldn't resist "improving" the calculator by making it more generic. I put quotes around "improving" because it is possible to overdo something like this. Right now the code is a bit harder to read than the earlier version but it also is much more extensible, so I think it's OK. It has four hashtables maping operators to .NET functions; there are four because it distinguishes between functions on the Math class and functions on the Decimal class, and between methods that take one parameter and ones that take two. I would make that data-driven also if I tried, but that might be overkill. In fact I could make it generically take any unknown token it finds and try to invoke a matching method on the Math or Decimal classes...

I also haven't quite figured out how to get the static properties programmatically, so I had to hardcode in e, pi, max and min. This may be due to bugs in PowerShell 1.0; I know there is a bug where you should be able to write:

$add = [Decimal]::Add
$add.Invoke(2,3)

but it doesn't work. That's not exactly the same as getting the value of a static property, but maybe there's a related issue, or maybe I just can't figure out how to do it right.

Anyway, this is the code right now:

$s = new-object System.Collections.Stack
$n = new-object System.Decimal
$df1 = @{
    "neg" = "Negate"
    "floor" = "Floor"
    "ceil" = "Ceiling"
    "round" = "Round"
}
$df2 = @{
    "+" = "Add" ;
    "-" = "Subtract" ;
    "*" = "Multiply" ;
    "/" = "Divide" ;
    "mod" = "Remainder"
}
$mf1 = @{
    "cos" = "Cos" ;
    "sqrt" = "Sqrt" ;
    "exp" = "Exp"
}
$mf2 = @{
    "^" = "Pow" ;
    "log" = "Log"
}
 
foreach ($a in $args) {
    switch ($a) {
        { $df1.$a } {
            $s.Push(
                [Decimal].GetMethod($df1.$a,@([Decimal])).Invoke(
                    $null,
                    @($s.Pop())))
        }
        { $df2.$a } {
            $temp = $s.Pop()
            $s.Push(
                [Decimal].GetMethod($df2.$a,@([Decimal],[Decimal])).Invoke(
                    $null,
                    @($s.Pop(),$temp)))
        }
 
        { $mf1.$a } {
            $s.Push(
                [Convert]::ToDecimal(
                    [Math].GetMethod($mf1.$a,@([Double])).Invoke(
                        $null,
                        @([Decimal]::ToDouble($s.Pop())))))
        }
        { $mf2.$a } {
            $temp = [Decimal]::ToDouble($s.Pop())
            $s.Push(
                [Convert]::ToDecimal(
                    [Math].GetMethod($mf2.$a,@([Double],[Double])).Invoke(
                        $null,
                        @([Decimal]::ToDouble($s.Pop()),$temp))))
        }
 
        "e" { $s.Push([Convert]::ToDecimal([Math]::E)) }
        "pi" { $s.Push([Convert]::ToDecimal([Math]::PI)) }
        "max" { $s.Push([Decimal]::MaxValue) }
        "min" { $s.Push([Decimal]::MinValue) }
        "dup" { $s.Push($s.Peek()) }
 
        { [Decimal]::TryParse($a,[ref]$n) } { $s.Push($n) }
    }
}
$s

(Weird random fact: when I removed the "e" from the variable name $temp, Movable Type returned an error trying to post this. In fact just including the t m p word anywhere seems to mess it up.) It gets a bit deep in the indenting; I decided that the calls to GetMethod() would not be worthy of their own indent because they were resolve before the end--that is, the closing parenthesis for those calls happens before the chain of ))) at the end of the multi-line statement.

Because it mixes Math and Decimal it doesn't always work up to the size of Decimal, which is 2^96-1. So something like:

./rpn2 2 95 ^ dup 1 - +

actually throws an exception because the number winds up being an overflow due to rounding; you can compare:

./rpn2 2 95 ^ which uses the Math and returns

39614081257132200000000000000

to

./rpn2 max 2 /

which returns the slightly smaller

39614081257132168796771975168

Actually that last number is 2 ^ 95 exactly, when it should really be 2 ^ 95 - 1, but I guess there is some rounding or something at the limit.

A tip of the hat to Bruce Payette for some help getting this working.

Posted by AdamBa at October 17, 2007 11:07 PM

Trackback Pings

TrackBack URL for this entry:
http://proudlyserving.com/cgi-bin/mt-tb.cgi/627

Comments

My mindful of the lure of premature generalization, my young padawan.

Posted by: Andrew at October 18, 2007 08:42 AM

I think a wise Jedi Master once said, "Premature generalization is the root of all evil, unless it leads to code with six right parentheses in a row, in which case it's kinda cool."

- adam

Posted by: Adam Barr at October 18, 2007 10:32 AM