PowerShell RPN Calculator

Mon, Oct 12, 2009 4-minute read

Adam Barr blogged bits and pieces of a PowerShell RPN calculator a few years ago: first the basics, and then some tweaks to clean it up. An RPN calculator, if you haven’t played with one before, flips the way you enter data. Rather than type “2 + 2”, you type “2 2 +”. RPN-style calculation supposedly has lots of great benefits. While I understand it and can do it, I wouldn’t say I “get it.”

Anyways.

One of the things he runs into with the last version is the PowerShell’s (version one) inability to dynamically invoke static methods:

… 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.

Well, in version two it does work, and that makes the RPN calculator a whole lot cleaner.

Another interesting aspect about the latest implementation is that it has a lot of very similar code segments. It has tables for operations on the Double class that take one argument, operations on the Double class that take two arguments, operations on the Math class that take one argument, and operations on the Math class that take two arguments. Afterward, there are four nearly identical blocks of code that perform the operation and store the result.

Note: This of course isn’t a slag on the implementation, this is just continuing the tinkering on an interesting concept.

We can simplify this in two ways:

  1. Don’t create hard-coded lists of operators. Instead, we’ll look at all methods in the Math and Double class to see if one matches. This approach gives us 30-something operators for free, and perhaps more in future versions of the .NET Framework.
  2. Don’t create hard-coded lists of arity: the number of arguments consumed by an operator. Instead, we’ll look at the method overloads that match the operator name, and see how many arguments they consume.

Now, there are some subtleties to both points:

  1. The method names in the Decimal and Math classes get kind of long. You don’t want to have to write “2 3 Multiply” in an RPN calculator. To work around that, we’ll define a hashtable of shortcuts that simply map operators to their names.
  2. Some operators have multiple overloads. For example, the one-argument Round method rounds a number to zero decimal points. The two-argument Round method rounds it to the specified number of decimal points.

By leveraging PowerShell’s built-in support for introspection and dynamic method invocation, we now have a script that is both much shorter, and much more powerful.

##############################################################################
##
## rpn.ps1
##
##############################################################################
<#

.SYNOPSIS
Evaluates a statement as an RPN calculator. Supports all operations from
System.Math and System.Decimal.

.EXAMPLE
rpn 2 2 +

.EXAMPLE
rpn 2 3 + 1.3 / 2 Round2 Negate

#>

$s = new-object System.Collections.Stack
$n = 0d

$shortcuts = @{
    "+" = "Add"; "-" = "Subtract"; "/" = "Divide"; "*" = "Multiply";
    "%" = "Remainder"; "^" = "Pow"; "||" = "Abs"
}

:ARGLOOP foreach ($a in $args) {
    if($shortcuts[$a]) { $a = $shortcuts[$a] }

    ## First, see if it's a number. If so, push it.
    try { $s.Push( [Decimal] $a ); continue } catch {}

    ## It's an operation. Extract the operation name
    ## (such as Floor, Round, etc.) It may also represent a
    ## specific operation (such as Round2 - Round to specified precision).
    $argCountList = $a -replace "(\D+)(\d*)",'$2'
    $op = $a.Substring(0, $a.Length - $argCountList.Length)

    ## We support any static operations from the Decimal or Math classes
    foreach($type in [Decimal],[Math])
    {
        if($definition = $type::$op)
        {
            ## If they haven't specifically given the number of arguments,
            ## see how many this method supports. We go through each overload
            ## definition, and see how many commas it has.
            if(-not $argCountList)
            {
                $argCountList = $definition.OverloadDefinitions |
                    Foreach-Object { ($_ -split ", ").Count } |
                    Sort-Object -Unique
            }

            ## Now, for each overload, see if we can call it.
            foreach($argCount in $argCountList)
            {
                try
                {
                    $methodArguments = $s.ToArray()[($argCount-1)..0]
                    $result = $type::$op.Invoke($methodArguments)

                    ## If we were able to call the method, pop all of its
                    ## arguments off of the stack.
                    $null = 1..$argCount | % { $s.Pop() }

                    ## Then push the result
                    $s.Push($result)
                    continue ARGLOOP

                }
                catch
                {
                    ## If we catch an error, try with the next number of
                    ## arguments
                }
            }
        }
    }
}

$s