Adding A New Vector Computation To AFC
A vector computation is a (typically financial) function that computes a single value from a vector (array) of values. Here, we shall look at how I implemented NPV()
. So let’s assume NPV()
has not been implemented yet.
Note: You must have read the topics on adding primitive functions and high-level functions to understand this section.
Docs and Tests
As always, we start with the announcement in the release notes, and the tests:
A | B | C | D | E | F | G | H | I | J | K | L | M | N | |
1 | Expected =IF(Q1,"Expected","FAILED!") |
Actual | Inputs | # of Inputs | Name | Highlight | Excel says | Skip for | ||||||
2 | 1.188 | 1.188 =NPV(C2,D2:G2) |
10% | -10 | 3 | 4.2 | 6.8 | 5 | NPV | NPV | ||||
3 | 1.922 | 1.922 =NPV(C3,E3:I3)+D3 |
8% | -40 | 8 | 9.2 | 10 | 12 | 14.5 | 7 | long, bigdecimal |
Parsing
Making NPV()
known to the parser is much like what we did for ABS()
. Array functions, however, need to accept vector arguments, not just simple values. These are denoted by shapedRangeExpr()
:
| "NPV" open() expr() <SEP> shapedRangeExpr() closeFun( Function.NPV )
This means:
open()
– open parenthesis, start collecting function arguments.expr()
– single-valued expression.shapedRangeExpr()
– vector range expression with shape information (width, height) available to compiler.closeFun( Function.NPV )
– packs collected function arguments into a function expression node.
Mathematical Definition
The function NPV()
is defined in the Excel help file as follows:
What we need here is a folding function that gives us access to the current index i
. The fold
we already encountered does not support this, but the very similar iterate
does.
Rewriting It
Since NPV
takes a vector as an argument, we need to apply the fold using apply... to vectors {v1, ..., vn}
instead of apply... to list xs
. And we tell the rewrite parser about this by suffixing the vector argument with #
:
rewrite npv( rate, vs# ) =
let rate1 = rate + 1
in
apply
iterate with
r = 0
index i
each vi as
r = r + vi / rate1 ^ i
end
to vectors {vs}
Note how I used let
here to move the common subexpression out of the folding loop.
Multiple Vectors
As the syntax above suggests, apply... to vectors {v1, ..., vn}
can be used to fold multiple vectors in parallel. This demonstrated by COVAR
:
rewrite covar( xs#, ys# ) =
if COUNT( xs ) <> COUNT( ys ) then NA() else
apply
fold with
sx = 0,
sy = 0,
sxy = 0
each xi, yi as
sx = sx + xi,
sy = sy + yi,
sxy = sxy + xi * yi
with count n into
(sxy - sx*sy/n) / n
when empty err( "#DIV/0! because list doesn't contain numbers in COVAR" ) ::NUMERIC
end
to vectors {xs, ys}