PowerShell RPN Calculator

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.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
##############################################################################
##
## 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

One Response to “PowerShell RPN Calculator”

  1. Blue writes:

    An RPN stack can be useful, only if you have a running stack (and probably when you have stack manipulation functions like the most fine HP RPN calculators.) This script works very well, but instead of ignoring stack entry errors, it should simply choke and (simply) explain the error; as the HP calculators do.

Leave a Reply