#!/usr/bin/perl -w use strict; my $Cur= '$'; # Currency symbol to use. my $Fmt= '%.2f'; # Format to output currency with. my $Cents= 100; # Fractions of $Cur to round to. my $Comma= ','; # Thousands separator. Main( @ARGV ); exit( 0 ); sub Usage { die "Usage: $0 [-v] 10000 8% /12 5.5\n", "Computes payment amount for a ${Cur}10,000 loan at 8%\n", "with 12 payments per year over 5.5 years.\n"; } sub PmtRatio { my( $periodic, $count )= @_; my $mult= $periodic ** $count; my $ratio= ($mult-1)/($periodic-1); return wantarray ? ( $ratio, $mult ) # "large payment ratio", "principle multiplier" : $ratio/$mult; # "small payment ratio" } =head1 Derivation P= principle (starting amount of loan) i= annual interest rate (0.10 == 10%) p= amount of periodic payment (to be calculated) f= payment frequency (12 for monthly payments and monthly compounding) N= total number of payments (N/f equals number of years) If we owe R, then after one period, R*i/f in interest will accrue so we will owe R+R*i/f or R*(1+i/f) r= periodic rate of growth of principle = (1+i/f) Payments Owe 0 P | 1 P*r - p ||||||| 2 [ P*r - p ]*r - p 2 P*r^2 - p*r - p 2 P*r^2 - p*( r + 1 ) ||||||||||||||||||| 3 [ P*r^2 - p*( r + 1 ) ]*r - p 3 P*r^3 - p*[ r^2 + r + 1 ] N P*r^N - p*sum( r^j ) L= sum( r^j ) L= 1+r+..+r^(N-1) r*L - L = r*( 1+r+..+r^(N-2)+r^(N-1) ) - ( 1+r+..+r^(N-1) ) r*L - L = ( r+r^2+..+r^(N-1)+r^N ) - ( 1+r+..+r^(N-1) ) r*L - L = r+r^2+..+r^(N-1) + r^N - ( 1 + r+..+r^(N-1) ) r*L - L = r+r^2+..+r^(N-1) + r^N - 1 - ( r+..+r^(N-1) ) ^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^ r*L - L = r^N - 1 (r - 1)*L = r^N - 1 L = ( r^N - 1 ) / ( r - 1 ) After payment N, we owe nothing so: 0 = P*r^N - p*L 0 = P*r^N - p*(r^N-1)/(r-1) p*(r^N-1)/(r-1) = P*r^N p = P * r^N / [ (r^N-1) / (r-1) ] ^^^ ^^^^^^^^^^^^^^^ M L We'll call M the "multiplier". It is the ratio of ending principle over starting principle if you never make any payments. It is useful for computing how your savings will grow based on different interest compounding schemes. We'll call L the "large payment ratio". It is the ratio of P*M (ending priciple if no payments) over payment size. It will be somewhat larger than N (your payments will be somewhat smaller than P*M/N). We'll call S=L/M the "small payment ratio". It is the ratio of P (starting principle) over payment size. It will be somewhat smaller than N (your payments will be somewhat larger than P/N). =cut sub Round { my( $amt )= @_; return int( $amt*$Cents + 0.5 ) / $Cents; } sub readLine { my $line= ; die "Can't read STDIN: $!\n" unless defined $line; chomp $line; return $line; } sub getParams { my $params= @_ ? "@_" : ""; my $reAmt= '\d+[.]?\d*'; my( $P, $i, $f, $y ); if( $params =~ /\S/ ) { Usage() unless $params =~ s[ ^\s* ($reAmt) (\s|$) ][]x; $P= $1; } if( ! defined $P || "" eq $P ) { while( 1 ) { print "Enter current value [amount of loan]: "; $params= readLine(); if( $params =~ s[ ^\s* ($reAmt) (\s|$) ][]x ) { $P= $1; last; } warn "Invalid amount. Please try again.\n"; } } if( $params =~ /\S/ ) { warn "Ignoring garbage after principle ($params).\n" unless $params =~ s[ ^\s* ($reAmt)\s*% ([\s/]|$) ][$2]x; $i= $1; } if( ! defined $i || "" eq $i ) { while( 1 ) { print "Enter interest annual percentage rate: "; $params= readLine(); if( $params =~ s[ ^\s* ($reAmt)\s*%? ([\s/]|$) ][$2]x ) { $i= $1; last; } warn "Invalid amount. Please try again.\n"; } } if( $params =~ /\S/ ) { warn "Ignoring garbage after interest rate ($params).\n" unless $params =~ s[ ^\s* /\s*(\d+) (\s|$) ][]x; $f= $1; } if( ! defined $f || "" eq $f ) { while( 1 ) { print qq{Enter payment frequency [periods per "year"]: }; $params= readLine(); if( $params =~ s[ ^\s* (?:/\s*)?(\d+) (\s|$) ][]x ) { $f= $1; last; } warn "Invalid amount. Please try again.\n"; } } if( $params =~ /\S/ ) { warn "Ignoring garbage after frequency ($params).\n" unless $params =~ s[ ^\s* ($reAmt) (\s|$) ][]x; $y= $1; } if( ! defined $y || "" eq $y ) { while( 1 ) { print qq{Enter loan duration [years]: }; $params= readLine(); if( $params =~ s[ ^\s* ($reAmt) (\s|$) ][]x ) { $y= $1; last; } warn "Invalid amount. Please try again.\n"; } } my $N= int( $y * $f + 0.5); $y= $N/$f; $P= Round( $P ); return( $P, $i, $f, $y, $N ); } sub Main { my $verbose= 0; if( @_ && $_[0] eq "-v" ) { shift(@_); $verbose= 1; } my( $P, $i, $f, $y, $N )= getParams( @_ ); $i /= 100; my( $rat, $mult )= PmtRatio( 1+$i/$f, $N ); my $p= $P*$mult/$rat; my $amt= $Cur . $P; 0 while $amt =~ s/(\d)(\d\d\d(\D|$))/$1$Comma$2/; $p= Round( $p ); print "\n$amt at ", $i*100, qq{%/$f per period, requires $N payments of $Cur$p ($y "years")\n}; if( $verbose ) { print "\npayment_number +interest_accrued -payment_made =still_owe\n"; } my $pi; my $ti= 0; my $yi= 0; for( 1..$N ) { $pi= Round( $P * $i / $f ); $yi= Round( $yi + $pi ); $ti= Round( $ti + $pi ); $P += $pi - $p; if( $verbose and 1 == $_ % $f || 0 == $_ % $f ) { printf "%3d +$Fmt -$Fmt =$Fmt", $_, $pi, $p, $P; if( 0 == $_ % $f ) { printf " ($Cur$Fmt int)", $yi; $yi= 0; } print "\n"; } } print "\n" if $verbose; $ti= sprintf $Fmt, $ti; 0 while $ti =~ s/(\d)(\d\d\d(\D|$))/$1$Comma$2/; printf "Final payment=$Cur$Fmt, Total interest=$Cur%s, APR=%f%%\n", $p+$P, $ti, 100*( (1+$i/$f)**$f - 1 ); }