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