Contributor: J.W. RIDER               

unit ufinance;                                      { last modified 920520 }

{ Math Routines for Finance Calculations in Turbo Pascal }
{ Copyright 1992, J. W. Rider                            }
{ CIS mail: [70007,4652]                                 }

{  These are pascal implementations some of the finance functions
   available for ObjectVision and Quattro Pro. They are intended to
   work exactly as described in the Quattro Pro 3.0 @Functions manual.

   The following are the Lotus 1-2-3 compatibility functions.

           CTERM ( Rate, FV,      PV)
           DDB   ( cost, salvage, life, period)
           FV    ( Pmt,  Rate,    Nper)
           PMT   ( PV,   RATE,    Nper)
           PV    ( Pmt,  Rate,    Nper)
           RATE  ( FV,   PV,      Nper)
           SLN   ( cost, salvage, life)
           SYD   ( cost, salvage, life, period)
           TERM  ( pmt,  rate,    fv)

   Also implemented are the extended versions of the routines that
   balance the following "cash-flow" equation:

 pval*(1+rate)^nper + paymt*(1+rate*ptype)*((1+rate)^nper-1)/rate + fval = 0

           IRATE (            nper, pmt, pv, fv, ptype)
           NPER  ( rate,            pmt, pv, fv, ptype)
           PAYMT ( rate,      nper,      pv, fv, ptype)
           PPAYMT( rate, per, nper,      pv, fv, ptype)
           IPAYMT( rate, per, nper,      pv, fv, ptype)
           PVAL  ( rate,      nper, pmt,     fv, ptype)
           FVAL  ( rate,      nper, pmt, pv,     ptype)

   In QPro and OV, the ptype code is either 0 or 1 to indicate that the
   is made at the end or beginning of the month respectively.  My preferred
   explanation is that "ptype" is the fraction of the interest rate that is
   applied to a payment in the period that it is paid.  This has the same
   effect when ptype is 0 or 1, but complicates the explanation for what is
   right when ptype=1. THE EXAMPLES IN THE QPRO AND OV MANUALS DO NOT AGREE
   FOR THE "PPAYMT" FUNCTION.  Someone needs to explain these discrepancies.
   UFinance follows the QPro3 style, but the formula is different than what
   QPro3 function reference says is used for IPaymt.

   The "block" financial functions from QPro3 are also implemented:

                   IRR ( guess, block)
                   NPV ( rate, block, ptype)

   These make use of the "UBlock.BlockType" object designed especially
   for these functions.  The BlockType object provides access to a list
   of indexed floating point numbers. See the test program FINTEST.PAS
   for an example of BlockType usage.

   Caveats:  under no circumstances will I be held responsible if someone
   misuses this code.  The code is provided for the convenience of other
   programmers.  It is the someone else's responsibility to ensure that
   these functions satisfy financial needs.

   While this is a relatively complete set of functions, it is not possible
   to calculate all desirved components in the compound interest equation
   directly.  In particular, there is no way provided to compute directly
   the interest rate on an annuity or loan that goes from "pv" to "fv" in
   "nper" intervals, paying "pmt" each period.  The "RATE" function
   provided only determines the rate at which a compounded amount grows.
   The "IRATE" function computes a value by successive approximation and
   is inherently unstable. (The "IRR" function is subject to similar
   instability.)

   One way in which programmers go wrong is misunderstanding the
   distinction between binary floating point representations of numbers and
   decimal floating point representation.  Turbo Pascal, as well as most
   other high speed number processing systems, uses the binary form.  While
   such binary operations give results that are close to their decimal
   counterparts, some differences may arise.  Especially, when you expect
   results to round one way versus the other.
}

interface

uses ublock; { for "blocktype" of NPV and IRR functions }

{ "Extended" math is used if $N+ is set.  Otherwise, use "real" math.}

{$ifopt N-}
type extended = real;
{$endif}

function CTERM ( Rate, FV, PV: extended):extended;
  { number of compounding periods for initial amount "PV" to accumulate
    into amount "FV" at interest "Rate" }

function DDB   ( cost, salvage, life, period:extended):extended;
  { double declining balance depreciation for the "period" (should be a
    positive, whole number) interval on an item with initial "cost" and
    final "salvage" value at the end of "life" intervals }

function FV    ( Pmt, Rate, Nper:extended):extended;
  { accumulated amount from making "nper" payments of amount "pmt" with
    interest accruing on the accumulated amount at interest "rate"
    compounded per interval }

function FVAL  ( rate, nper, pmt, pv, ptype:extended):extended;
  { extended version of the FV function }

function IPAYMT(rate, per, nper, pv, fv, ptype:extended):extended;
  { computes the portion of a loan payment that is interest on the
    principal }

function IRATE ( nper, pmt, pv, fv, ptype:extended):extended;
  { extended version of the RATE function }

function IRR   ( guess: extended; var block: blocktype): extended;
  { returns internal rate-of-return of sequence of cashflows }

function NPER  ( rate, pmt, pv, fv, ptype:extended):extended;
  { extended version of the CTERM and TERM functions }

function NPV   (
  rate: extended; var block: blocktype; ptype:extended): extended;
  { return net present value of sequence of cash flows }

function PAYMT ( rate, nper, pv, fv, ptype:extended):extended;
  { extended version of the PMT function }

function PMT   ( PV, RATE, Nper: extended): extended;
  { payment amount per interval on loan or annuity of initial value "PV"
    with payments spread out over "nper" intervals and with interest
    accruing at "rate" per interval }

function PPAYMT( rate, per, nper, pv, fv, ptype:extended):extended;
  { computes the portion of a loan payment that reduces the principal }

function PV    ( Pmt, Rate, Nper: extended): extended;
  { initial value of loan or annuity that can be paid off by making "nper"
    payments of "pmt" which interest on the unpaid amount accrues at
    "rate" per interval }

function PVAL  ( rate, nper, pmt, fv, ptype:extended):extended;
  { extended version of the PV function }

function RATE  ( FV, PV, Nper: extended): extended;
  { determines interest rate per interval when initial amount "pv"
    accumulates into amount "fv" by compounding over "nper" intervals }

function SLN   ( cost, salvage, life: extended): extended;
  { straight line depreciation per interval when item of initial value
    "cost" has a value of "salvage" after "life" intervals }

function SYD   ( cost, salvage, life, period: extended): extended;
  { sum-of-year-digits depreciation amount for the "period" (should be a
    positive, whole number) interval on a item with initial "cost" and
    final "salvage" value at the end of "life" intervals }

function TERM  ( pmt, rate, fv: extended): extended;
  { number of compounding periods required to accumulate "fv" by making
    periodic deposits of "pmt" with interest accumulating at "rate" per
    period }

implementation

function CTERM ( Rate, FV, PV: extended):extended;
begin cterm:=ln(fv/pv)/ln(1+rate) end;

function DDB   ( cost, salvage, life, period:extended):extended;
var x:extended; n:integer;
begin
  x:=0; n:=0;
  while period>n do begin
    x:=2*cost/life;
    if (cost-x)1e-6 then fv:=pmt*(exp(nper*ln(1+rate))-1)/rate
  else                   fv:=pmt*nper*(1+(nper-1)*rate/2); end;

function FVAL  ( rate, nper, pmt, pv, ptype:extended):extended;
var f: extended;
begin
  f:=exp(nper*ln(1+rate));
  if abs(rate)<1e-6 then
    fval :=-pmt*nper*(1+(nper-1)*rate/2)*(1+rate*ptype)-pv*f
  else
    fval := pmt*(1-f)*(1/rate+ptype)-pv*f;
end;

function IPAYMT(rate, per, nper, pv, fv, ptype:extended):extended;
begin
  ipaymt := rate
    * fval( rate, per-ptype-1, paymt( rate, nper, pv, fv, ptype), pv, ptype);
end;

function IRATE ( nper, pmt, pv, fv, ptype:extended):extended;
var rate,x0,x1,y0,y1:extended;

  function y:extended;
  var f:extended;
  begin
    if abs(rate)<1e-6 then y:=pv*(1+nper*rate)+pmt*(1+rate*ptype)*nper+fv
    else begin
      f:=exp(nper*ln(1+rate));
      y:=pv*f+pmt*(1/rate+ptype)*(f-1)+fv; end; end;

begin {irate}

  { JWR: There are two fundamental problems with solutions by successive
    approximation.  One is figuring out where you want to start; the
    other is figuring out where you want to stop.  If you don't set them
    right, then your solution will approximate successively forever.
    This is my guess, but there is no guarantee that the solution will
    even exist, much less converge. }

  rate:=0; y0:=pv+pmt*nper+fv; x0:=rate;
  rate:=exp(1/nper)-1; y1:=y; x1:=rate;
  while abs(y0-y1)>1e-6 do begin { find root by secant method }
    rate:=(y1*x0-y0*x1)/(y1-y0); x0:=x1; x1:=rate; y0:=y1; y1:=y; end;
  irate:=rate;
end; {irate}

function IRR( guess: extended; var block: blocktype): extended;
var orate, rate: extended;

  function drate(rate:extended):extended;
  var npv,npvprime,blockvaluei:extended; i:longint;
  begin
    npv:=0; npvprime:=0; rate:=1/(1+rate);
    for I:=block.count downto 1 do begin
      blockvaluei:=block.value(i);
      npv:=npv*rate+blockvaluei;
      npvprime:=(npvprime+blockvaluei*i)*rate; end;
    if abs(npvprime)<1e-6 then drate:=npv*1e-6 { a guess }
    else                       drate:=npv/npvprime; end;

begin {IRR}

  { JWR: same caveats as for IRate }

  orate:=guess; rate:=orate+drate(orate);
  while abs(rate-orate)>1e-6 do begin { find root by newton-raphson }
    orate:=rate; rate:=rate+drate(rate); end;
  irr:=rate;
end;

function NPER  ( rate, pmt, pv, fv, ptype:extended):extended;
var f:extended;
begin
  f:=pmt*(1+rate*ptype);
  if abs(rate)>1e-6 then
    nper:=ln((f-rate*fv)/(pv*rate+f))/ln(1+rate)
  else
    nper:=-(fv+pv)/(pv*rate+f); end;

function NPV   (
  rate: extended; var block: blocktype; ptype:extended): extended;
var x:extended; i:longint;
begin
  x:=0; rate:=1/(1+rate); {note: change in meaning of "rate"!}
  for I:=block.count downto 1 do x:=x*rate+block.value(i);
  npv:=x*exp((1-ptype)*ln(rate)); end;

function PAYMT ( rate, nper, pv, fv, ptype:extended):extended;
var f:extended;
begin
  f:=exp(nper*ln(1+rate));
  paymt:= (fv+pv*f)*rate/((1+rate*ptype)*(1-f)); end;

function PMT   ( PV, RATE, Nper: extended): extended;
begin pmt:=pv*rate/(1-exp(-nper*ln(1+rate))) end;

function PPAYMT( rate, per, nper, pv, fv, ptype:extended):extended;
var f:extended;
begin
  f:=paymt(rate,nper,pv,fv,ptype);
  ppaymt:=f-rate*fval(rate,per-ptype-1,f,pv,ptype);
end;

function PV    ( Pmt, Rate, Nper: extended): extended;
begin
  if abs(rate)>1e-6 then
    pv:=pmt*(1-exp(-nper*ln(1+rate)))/rate
  else
    pv:=pmt*nper*(1+(nper-1)*rate/2)/(1+nper*rate)
end;

function PVAL  ( rate, nper, pmt, fv, ptype:extended):extended;
var f:extended;
begin
  if abs(rate)>1e-6 then begin
    f:=exp(nper*ln(1+rate)); pval := (pmt*(1/rate+ptype)*(1-f)-fv)/f; end
  else
    pval:=-(pmt*(1+rate*ptype)*nper+fv)/(1+nper*rate)
end;

function RATE  ( FV, PV, Nper: extended): extended;
begin rate:=exp(ln(fv/pv)/nper)-1 end;

function SLN   ( cost, salvage, life: extended): extended;
begin sln:=(cost-salvage)/life end;

function SYD   ( cost, salvage, life, period: extended): extended;
begin syd:=2*(cost-salvage)*(life-period+1)/(life*(life+1)) end;

function TERM  ( pmt, rate, fv: extended): extended;
begin  term:=ln(1+(fv/pmt)*rate)/ln(1+rate) end;

end.

{ ----------------------    CUT HERE -------------------------- }

unit ublock;

{ defines the "BlockType" object used for the UFinance NPV and IRR functions }
{ Copyright 1992 by J. W. Rider }
{ CIS mail: [70007,4652] }

interface

{$ifopt N-}
type
  extended = real;
{$endif}

type

  { the abstract "block": this is the type that is used for the
    type of "var" parameters in procedures and functions }
  BlockTypePtr = ^BlockType;
  BlockType = object
    function count: longint; virtual;  { number of values in "block" }
    function value(n:longint):extended; virtual; { return nth value }
    destructor done; virtual;
    end;

type
  ExtendedArrayPtr = ^ExtendedArray;
  ExtendedArray = array [1..$fff8 div sizeof(extended)] of extended;

type
  { a special-purpose block that extracts values from "extended" arrays.
    This is the type that would be declared as "const" or "var" or
    allocated on the heap in your program.  This one is very simple; you
    could easily extend the abstract block to other storage forms. }
  {  Note that "extended" means the same as "real" if $N-. }
  ExtendedArrayBlockTypePtr = ^ExtendedArrayBlockType;
  ExtendedArrayBlockType = object(BlockType)
    c: word;
    d: extendedarrayptr;
    function count:longint; virtual;
    function value(n:longint):extended; virtual;
    constructor init(dim:word; var firstvalue:extended);
    end;

implementation

function blocktype.count; begin count:=0 end;
function extendedarrayblocktype.count; begin count:=c; end;

destructor blocktype.done; begin end;

constructor extendedarrayblocktype.init; begin c:=dim; d:=@firstvalue; end;

function blocktype.value; begin value:=0; end;
function extendedarrayblocktype.value; begin value:=d^[n] end;

end.

{ ========================   DEMO ============================= }

{JWR: The output scrolls without stopping.  You might want to replace
 "writeln;" with "readln;" so that you can follow along in the QPRO
 manual while you run the example. What I usually do for testing is
 just to redirect everything to a file from the command line and then
 examine the file.}

program fintest;
uses ufinance,ublock;

{ these types and consts are used for the IRR and NPV functions }

type
  xray3 = array [1..3] of extended;
  xray5 = array [1..5] of extended;
  xray7 = array [1..7] of extended;
  bt = object(extendedarrayblocktype) end;

const
  x1: xray3 = (-10,150,-145);
  x2: xray3 = (-10,150.1,-145);
  a: xray7 = (-3000,700,600,750,900,1000,1400);
  b: xray7 = (-50000,-8000,2000,4000,6000,5000,4500);
  c: xray7 = (-10000,1000,1000,1200,2000,3000,4000);
  a2: xray5 = (-5000,2000,2000,2000,2000);
  b2: xray7 = (8000,9000,8500,9500,10000,11000,10000);
  c2: xray7 = (200,350,-300,600,700,1000,1200);
  d2: xray7 = (3500,4000,3000,5000,4000,6500,7000);

  block1:bt = (c:3; d:@x1);
  block2:bt = (c:3; d:@x2);
  block3:bt = (c:7; d:@a);
  block4:bt = (c:7; d:@b);
  block5:bt = (c:7; d:@c);
  block6:bt = (c:5; d:@a2);
  block7:bt = (c:4; d:@a2[2]);
  block8:bt = (c:7; d:@b2);
  block9:bt = (c:7; d:@c2);
  block10:bt = (c:7; d:@d2);

begin

  writeln('Test of UFinance unit.  Examples from');
  writeln('    Quattro Pro 3.0 @Functions and Macros manual');
  writeln;
  writeln('page 29 (CTERM):');
  writeln(cterm(0.07,5000,3000):10:2);
  writeln(nper(0.07,0,-3000,5000,0):10:2,'(nper)');
  writeln(cterm(0.1,5000,3000):10:6);
  writeln(cterm(0.12,5000,3000):10:6);
  writeln(cterm(0.12,10000,7000):10:6);
  writeln;
  writeln('pages 35-36 (DDB):');
  writeln(ddb(4000,350,8,2):10:0);
  writeln(ddb(15000,3000,10,1):10:0);
  writeln(ddb(15000,3000,10,2):10:0);
  writeln(ddb(15000,3000,10,3):10:0);
  writeln(ddb(15000,3000,10,4):10:0);
  writeln(ddb(15000,3000,10,5):10:0);
  writeln;
  writeln('page 48 (FV):');
  writeln(fv(500,0.15,6):10:2);
  writeln(fval(0.15,6,-500,0,0):10:2,'(fval)');
  writeln(fv(200,0.12,5):10:2);
  writeln(fv(500,0.9,4):10:2);
  writeln(fv(800,0.9,3):10:2);
  writeln(fv(800,0.9,6):10:2);
  writeln;
  writeln('page 49 (FVAL):');
  writeln(fval(0.15,6,-500,0,1):10:2);
  writeln(fval(0.15,6,-500,-340,1):10:2);
  writeln;
  writeln('page 57 (IPAYMT):');
  writeln(ipaymt(0.1/12,2*12,30*12,100000,0,0):10:2);
  writeln;
  writeln('pages 57-58 (IRATE):');
  writeln(irate(5*12,-500,15000,0,0):10:5);
  writeln(irate(5,-2000,-2.38,15000,0):10:4);
  writeln;
  writeln('pages 60-61 (IRR):');
  writeln(irr(0,block1)*100:10:2,'%');
  writeln(irr(10,block1)*100:10:0,'%');
  writeln(irr(0,block2)*100:10:2,'%');
  writeln(irr(10,block2)*100:10:0,'%');
  writeln(irr(0,block3)*100:10:2,'%');
  writeln(irr(0,block4)*100:10:2,'%');
  writeln(irr(0,block5)*100:10:2,'%');
  writeln;
  writeln('page 73 (NPER):');
  writeln(nper(0.115,-2000,-633,50000,0):10:2);
  writeln;
  writeln('page 75 (NPV):');
  writeln(npv(0.1,block6,1):10:0);
  writeln(a2[1]+npv(0.1,block7,0):10:0);
  writeln(npv(0.0125,block8,0):10:2);
  writeln(npv(0.15/12,block9,0):10:0);
  writeln(npv(0.15/12,block10,0):10:0);
  writeln;
  writeln('page 77 (PAYMT):');
  writeln(paymt(0.175/12,12*30,175000,0,0):10:2);
  writeln(paymt(0.175/12,12*30,175000,0,1):10:2);
  writeln(paymt(0.175/12,12*30,175000,-80000,0):10:2);
  writeln;
  writeln('pages 78-79 (PMT)');
  writeln(pmt(10000,0.15/12,3*12):10:2);
  writeln(paymt(0.15/12,3*12,10000,0,0):10:2,'(paymt)');
  writeln(pmt(1000,0.12,5):10:2);
  writeln(pmt(500,0.16,12):10:2);
  writeln(pmt(5000,0.16/12,12):10:2);
  writeln(pmt(12000,0.11,15):10:2);
  writeln;
  writeln('page 79 (PPAYMT):');
  writeln(ppaymt(0.1/12,2*12,30*12,100000,0,0):10:2);
  writeln(ppaymt(0.15/4,24,40,10000,0,1):10:2);
  writeln;
  writeln('page 81 (PV)');
  writeln(pv(350,0.07/12,5*12):10:2);
  writeln(pval(0.07/12,5*12,-350,0,0):10:2,'(pval)');
  writeln(pv(277,0.12,5):10:2);
  writeln(pv(600,0.17,10):10:2);
  writeln(pv(100,0.11,12):10:2);
  writeln;
  writeln('page 82 (PVAL)');
  writeln(pval(0.1,12,2000,0,0):10:2);
  writeln(pval(0.1,15,0,30000,0):10:2);
  writeln;
  writeln('page 84 (RATE)');
  writeln(rate(4000,2000,10)*100:6:2,'%');
  writeln(rate(10000,7000,6*12)*100:6:2,'%');
  writeln(rate(1200,1000,3)*100:6:2,'%');
  writeln(rate(500,100,25)*100:6:2,'%');
  writeln;
  writeln('page 89 (SLN)');
  writeln(sln(4000,350,8):10:2);
  writeln(sln(15000,3000,10):10:0);
  writeln(sln(5000,500,5):10:0);
  writeln(sln(1800,0,3):10:0);
  writeln;
  writeln('pages 94-95 (SYD)');
  writeln(syd(4000,350,8,2):10:2);
  writeln(syd(12000,1000,5,1):10:0);
  writeln(syd(12000,1000,5,2):10:0);
  writeln(syd(12000,1000,5,3):10:0);
  writeln(syd(12000,1000,5,4):10:0);
  writeln(syd(12000,1000,5,5):10:0);
  writeln;
  writeln(ddb(12000,1000,5,1):10:0,'(ddb)');
  writeln(ddb(12000,1000,5,2):10:0,'(ddb)');
  writeln(ddb(12000,1000,5,3):10:0,'(ddb)');
  writeln(ddb(12000,1000,5,4):10:0,'(ddb)');
  writeln(ddb(12000,1000,5,5):10:0,'(ddb)');
  writeln;
  writeln('page 96 (TERM)');
  writeln(term(2000,0.11,50000):10:2);
  writeln(nper(0.11,-2000,0,50000,0):10:2,'(nper)');
  writeln(term(300,0.06,5000):10:1);
  writeln(term(500,0.07,1000):10:2);
  writeln(term(500,0.07,1000):10:2);
  writeln(term(1000,0.10,50000):10:1);
  writeln(term(100,0.05,1000):10:1);
end.