File Coverage

blib/lib/Math/Financial.pm
Criterion Covered Total %
statement 39 225 17.3
branch 0 108 0.0
condition 0 6 0.0
subroutine 11 24 45.8
pod 9 10 90.0
total 59 373 15.8


line stmt bran cond sub pod time code
1             package Math::Financial;
2              
3             # Copyright 1999 Eric Fixler
4             # All rights reserved. This program is free software;
5             # you can redistribute it and/or modify it under the same terms as Perl itself.
6              
7             # $Id: Financial.pm,v 1.5 1999/09/15 19:08:41 fix Exp $
8             # $Source: /www/cgi/lib/Math/RCS/Financial.pm,v $
9              
10             =pod
11              
12             =head1 NAME
13              
14             Math::Financial - Calculates figures relating to loans and annuities.
15              
16             =head1 SYNOPSIS
17              
18             $calc = new Math::Financial(fv =E 100000, pv =E 1000);
19             $calc-Eset->(pmt => 500, ir => 8);
20              
21             $calc->compound_interest(find =E 'fv');
22              
23             =head1 DESCRIPTION
24              
25             This package contains solves mathematical problems relating to loans and annuities.
26              
27             The attributes that are used in the equations may be set on a per-object basis, allowing
28             you to run a set of different calculations using the same numbers, or they may be fed
29             directly to the methods.
30              
31             The attribute types, accessed through the C and C methods are
32              
33             =over4
34              
35             =item pv =E Present Value
36              
37             =item fv =E Future Value
38              
39             =item ir =E Yearly Interest Rate (in percent)
40              
41             =item pmt =E Payment Amount
42              
43             =item np =E Number of Payments/Loan Term
44              
45             =item tpy =E Terms Per Year (defaults to 12)
46              
47             =item pd =E Payments made so far (used only for loan/annuity balances)
48              
49             =back
50              
51             Attributes are case-insensitive. The documentation for the individual methods
52             indicates which attributes must be set for those methods.
53              
54             Calculations are based B on the attributes set with the C or C
55             methods, B with arguments fed directly to the methods. This seemed like the
56             least confusing way to make the interface flexible for people who are using the
57             module in different ways.
58              
59             Also, performing a calculation
60             does B update the attribute of the solution. In other words, if
61             you solve an equation that returns fv, the solution is returned but the
62             internal fv field is unaffected.
63              
64             Any attempted calculation which cannot be completed -- due to either missing or
65             invalid attributes -- will return C.
66              
67             I am interested to hear from people using this module -- let me know what
68             you think about the interface and how it can be improved.
69              
70             =head1 METHODS
71              
72             =cut
73              
74             sub BEGIN {
75 1     1   4 *{__PACKAGE__.'::loan_payment'} = \&monthly_payment;
  1         7  
76 1     1   765 use strict;
  1         2  
  1         43  
77 1     1   912 use POSIX qw(:ctype_h);
  1         12748  
  1         10  
78 1         175 use vars qw(@ISA $VERSION @EXPORT @EXPORT_OK %EXPORT_TAGS
79 1     1   2008 @ATTRIBUTES $DEFAULT_OBJECT $re_object);
  1         9  
80 1         9 $VERSION = 0.76;
81 1     1   8 use constant PV => 0;
  1         2  
  1         106  
82 1     1   6 use constant FV => 1;
  1         2  
  1         46  
83 1     1   6 use constant NP => 2;
  1         1  
  1         49  
84 1     1   6 use constant PMT => 3;
  1         15  
  1         48  
85 1     1   5 use constant IR => 4;
  1         2  
  1         57  
86 1     1   6 use constant TPY => 5; # TERMS PER YEAR
  1         2  
  1         11087  
87 1     1   19 use constant PD => 6;
  1         2  
  1         189  
88 1         5 @ATTRIBUTES = qw(PV FV NP PMT IR TPY PD);
89 1         2 $re_object = '(?i)[a-z][\w]*?::[\w]';
90 1         22 @ISA = qw(Exporter);
91 1         3 @EXPORT= ();
92 1         4 @EXPORT_OK = qw(loan_term loan_payment compound_interest funding_annuity
93             loan_balance loan_size simple_interest);
94 1         9628 %EXPORT_TAGS = ( procedural => \@EXPORT_OK,
95             standard => \@EXPORT_OK);
96             }
97              
98              
99             sub new {
100             =pod
101              
102             =head2 new
103              
104             C<$calc = new Math::Financial();
105              
106             C<$calc = new Math::Financial(pv =E 10000, ir =E 5, np => 12)>
107              
108             Object constructor. See above for a description of the available attributes.
109             You do not I to set attributes here, you can also do so using C,
110             or feed attributes directly to the methods.
111              
112             There are no default values for any of the attributes except C (Terms Per Year),
113             which is 12 by default, and C which defaults to zero.
114              
115             If you don't want to use the object-oriented interface, see the L section
116             below.
117              
118             =cut
119 0   0 0 1   my $class = ref($_[0]) || ($_[0] =~ /(.*?::.*)/)[0];
120 0 0         my $parent = ref($class) ? $_[0] : [undef,undef,undef,undef,undef,12,0] ;
121 0 0         if ($class) { shift(@_); } else { $class = __PACKAGE__ ; };
  0            
  0            
122 0           my $params = { pv => $parent->[PV],
123             fv => $parent->[FV],
124             ir => $parent->[IR],
125             np => $parent->[NP],
126             pmt => $parent->[PMT],
127             tpy => $parent->[TPY],
128             pd => $parent->[PD],
129             @_ };
130 0           my $self = [];
131 0           bless($self,$class);
132 0           $self->set(%$params);
133 0           return $self;
134             }
135              
136              
137             sub _get_attribute_key {
138             # if fed a list, will return a list
139 0     0     my ($self,@args) = _get_self(@_);
140 0 0         return undef unless scalar(@args);
141 0           my @keys = ();
142 0           foreach (@args) {
143 0 0         if (isdigit($_)) { push(@keys,$_); next; };
  0            
  0            
144 0           my $attrib = quotemeta($_);
145 0           for (my $j = 0; $j <= $#ATTRIBUTES; $j++) {
146 0 0         if ($ATTRIBUTES[$j] =~ /$attrib/i) { push(@keys,$j); next; };
  0            
  0            
147             };
148 0           push(@keys,undef); #unfound key
149             }
150 0 0         if (not($#args)) {
151 0           return $keys[0];
152             } else {
153 0 0         return wantarray ? @keys : \@keys;
154             };
155             };
156              
157             sub set {
158             =pod
159              
160             =head2 set
161              
162             C<$calc-Eset(fv =E 100000, pmt =E 500)>
163              
164             You can set any of the stored attributes using this method, which is is also
165             called by . Returns the number of attributes set.
166              
167             =cut
168 0     0 1   my ($self,@args) = _get_self(@_);
169 0           my $params = { @args };
170 0           my ($field,$val,$key); my $count = 0;
  0            
171 0           while (($field, $val) = each(%$params)) {
172 0           $key = $self->_get_attribute_key($field);
173 0 0         if (defined($key)) { $self->[$key] = $val; $count++; }
  0            
  0            
174             }
175 0           return $count;
176             }
177              
178             sub get {
179             =pod
180              
181             =head2 get
182              
183             C<$calc-Eget(field => 'ir')>
184              
185             C<$calc-Eget('ir','pmt','pv')>
186              
187             C<$calc-Eget([qw(ir pmt pv)])>
188              
189             You can get one or several attributes using this method. In the multiple
190             attribute formats, it accepts either a list or a list reference as input.
191              
192             In single attribute context, returns a scalar. In multiple attribute context,
193             it returns a list or a reference to a list, depending on the calling context.
194              
195             =cut
196 0     0 1   my ($self,@args) = _get_self(@_);
197 0 0         ($args[0] =~ /field/io) and shift(@args);
198 0           my @gets = ();
199 0           foreach my $field (@args) {
200 0 0         if (ref($field) eq 'ARRAY') { push(@gets,map({ $self->get($_) } @$field)) ; next; }
  0            
  0            
  0            
201 0           else { my $key = $self->_get_attribute_key($field);
202 0 0         push(@gets, defined($key) ? $self->[$key] : $key); next; }
  0            
203             }
204 0 0         if ($#gets) {
205 0 0         return wantarray ? @gets : \@gets;
206 0           } else { return $gets[0]; };
207             }
208              
209              
210             sub compound_interest {
211             =pod
212              
213             =head2 compound_interest
214              
215             C<$calc-Ecompound_interest>
216              
217             C<$calc-Ecompound_interest-E('fv')>
218              
219             C<$calc-Ecompound_interest-E(find =E 'fv')>
220              
221             Calculates compund interest for an annuity. With any 3 of pv, fv, np, and ir,
222             you can always solve the fourth.
223              
224             Without arguments, the method will attempt to figure out what you'd like to solve
225             based on what attributes of the object are defined. Usually, you'll probably want to
226             explicitly request what attribute you'd like returned, which you can do using
227             the second or third method.
228              
229             =cut
230 0     0 1   my ($self,@args) = _get_self(@_);
231 0 0         (scalar(@args) == 1) and unshift(@args,'find');
232 0 0         if (scalar(@args) > 2) {
233 0           my $temp = __PACKAGE__->new(@args[2..$#args]);
234 0           return $temp->compound_interest(@args[0..1]);
235             };
236 0           my $solve_for = $self->_get_attribute_key($args[1]);
237 0           my (@numbers,$result);
238 0 0         if (not(defined($solve_for))) {
239 0 0         if (@numbers = $self->_verify_fields(IR,PV,NP)) { $solve_for = FV; }
  0 0          
    0          
    0          
240 0           elsif (@numbers = $self->_verify_fields(IR,FV,NP)) { $solve_for = PV; }
241 0           elsif (@numbers = $self->_verify_fields(IR,PV,FV)) { $solve_for = NP; }
242 0           elsif (@numbers = $self->_verify_fields(PV,FV,NP)) { $solve_for = IR; }
243 0           else { return undef; };
244             } else {
245 0           my @combos = ();
246 0           $combos[FV] = [IR,PV,NP]; $combos[PV] = [IR,FV,NP]; $combos[NP] = [IR,PV,FV];
  0            
  0            
247 0           $combos[IR] = [PV,FV,NP];
248 0           $set = $combos[$solve_for];
249 0 0         @numbers = $self->_verify_fields(@$set) or return undef;
250             }
251 0 0         eval {if ($solve_for == FV) {
  0 0          
    0          
    0          
252 0           $ir = ($numbers[0]/100) / $self->[TPY];
253 0           ($pv,$np) = @numbers[1,2];
254 0           $result = abs($pv) * ( ($ir + 1) ** $np);
255             } elsif ($solve_for == PV) {
256 0           $ir = ($numbers[0]/100) / $self->[TPY];
257 0           ($fv,$np) = @numbers[1,2];
258 0           $result = abs($fv) * ( ($ir + 1) ** (0 - $np) );
259             } elsif ($solve_for == NP) {
260 0           $ir = $numbers[0]/100/$self->[TPY];
261 0           ($pv,$fv) = @numbers[1,2];
262 0           my $num = log(abs($fv)/$pv);
263 0           my $den = log( 1 + $ir);
264 0           $result = $num / $den;
265             } elsif ($solve_for == IR) {
266 0           ($pv,$fv,$np) = @numbers;
267 0           $ir = (( abs($fv)/abs($pv) ) ** (1 / $np) ) - 1;
268 0           $result = $ir * 100 * $self->[TPY];
269             };};
270            
271 0 0         return ($@) ? undef : $result;
272             }
273              
274             sub funding_annuity {
275             =pod
276              
277             =head2 funding_annuity
278              
279             C<$calc-Efunding_annuity>
280              
281             C<$calc-Efunding_annuity-E(pmt =E 2000, ir =E 6.50, np =E 40, tpy => 4)>
282              
283             C calculates how much money ( C ) you will have at the end of C periods
284             if you deposit C into the account each period and the account earns C interest per year.
285              
286             You may want to set the C attribute here to something other than 12, since, while loans
287             usually compound monthly, annuities rarely do.
288              
289             =cut
290              
291 0     0 1   my ($self,@args) = _get_self(@_);
292 0 0         if (scalar(@args)) {
293 0           my $temp = __PACKAGE__->new(@args);
294 0           return $temp->funding_annuity();
295             };
296 0           my @numbers = $self->_verify_fields(IR,PMT,NP);
297 0 0         return undef unless scalar(@numbers);
298 0           my ($result); #solving for fv here
299 0           my ($pmt,$np) = @numbers[1,2];
300 0           my $ir = $numbers[0]/100/$self->[TPY];
301 0           eval { $result = ($pmt * ( ((1 + $ir) ** $np) - 1))/$ir; };
  0            
302 0 0         return $@ ? undef : $result;
303             }
304              
305              
306             sub loan_balance {
307             =pod
308              
309             =head2 loan_balance
310              
311             C<$calc-Eloan_balance>
312              
313             C<$calc-Eloan_balance-E(pmt =E 2000, ir =E 6.50, np =E 360, pd =E 12)>
314              
315             C calculates the balance on a loan that is being made in C equal payments,
316             given that C payments have already been made. You can also use this method to determine
317             the amount of money left in an annuity that you are drawing down.
318              
319             =cut
320 0     0 1   my ($self,@args) = _get_self(@_);
321 0 0         if (scalar(@args)) {
322 0           my $temp = __PACKAGE__->new(@args);
323 0           return $temp->loan_balance();
324             };
325 0           my @numbers = $self->_verify_fields(IR,PMT,NP);
326 0 0         return undef unless scalar(@numbers);
327 0           my ($pmt,$np) = @numbers[1,2];
328 0           my $ir = $numbers[0]/100/$self->[TPY]; my ($result);
  0            
329 0           eval { my $a = (1 + $ir) ** ($self->[PD] - $np);
  0            
330 0           $result = $pmt/$ir * (1 - $a) ; };
331 0 0         return $@ ? undef : $result;
332             }
333              
334             sub monthly_payment {
335             =pod
336              
337             =head2 loan_payment
338              
339             C<$calc-Eloan_payment>
340              
341             Return the payment amount, per period, of a loan. This is also known as amortizing.
342             The ir, np, and pv fields must be set.
343              
344             =cut
345 0     0 0   my ($self,@args) = _get_self(@_);
346 0 0         if (scalar(@args)) {
347 0           my $temp = __PACKAGE__->new(@args);
348 0           return $temp->monthly_payment();
349             };
350 0           my @numbers = $self->_verify_fields(IR,PV,NP);
351 0 0         return undef unless scalar(@numbers);
352 0           my ($result,$ir);
353 0           my ($pv,$np) = @numbers[1,2];
354 0           $ir = ($numbers[0]/100) / $self->[TPY];
355 0           my $a = (1 + $ir) ** (0 - $np);
356 0           my $denominator = 1 - $a;
357 0           my $numerator = $pv * $ir;
358 0           $result = eval { $numerator / $denominator };
  0            
359 0 0         return $@ ? undef : $result;
360             }
361              
362              
363             sub loan_size {
364             =pod
365              
366             =head2 loan_size
367              
368             C<$calc-Eloan_term>
369              
370             C<$calc-Eloan_size-E(pmt =E 2000, ir =E 6.50, np =E 360)>
371              
372             C calculates the size of loan you can get based on the monthly payment
373             you can afford.
374              
375             =cut
376              
377 0     0 1   my ($self,@args) = _get_self(@_);
378 0 0         if (scalar(@args)) {
379 0           my $temp = __PACKAGE__->new(@args);
380 0           return $temp->loan_size();
381             };
382 0           my @numbers = $self->_verify_fields(IR,PMT,NP);
383 0 0         return undef unless scalar(@numbers);
384 0           my ($result);
385 0           my ($pmt,$np) = @numbers[1,2];
386 0           my $ir = $numbers[0]/100/$self->[TPY];
387 0           eval { $result = ($pmt * (1 - ((1 + $ir) ** (0 - $np))))/$ir; };
  0            
388 0 0         return $@ ? undef : $result;
389             };
390              
391             sub loan_term {
392             =pod
393              
394             =head2 loan_term
395              
396             C<$calc-Eloan_term>
397              
398             Return the number of payments (term) of a loan given the interest rate
399             C, payment amount C and loan amount C. The ir, pmt, and pv fields must be set.
400              
401             =cut
402 0     0 1   my ($self,@args) = _get_self(@_);
403 0 0         if (scalar(@args)) {
404 0           my $temp = __PACKAGE__->new(@args);
405 0           return $temp->loan_term();
406             };
407 0           my @numbers = $self->_verify_fields(IR,PMT,PV);
408 0 0         return undef unless scalar(@numbers);
409 0           my ($pmt, $pv) = @numbers[1,2];
410 0           $pv = abs($pv);
411 0           my $ir = $numbers[0]/100/$self->[TPY];
412 0           my ($result);
413 0           $result = eval {
414 0           my $numerator = log($pmt/($pmt - ($ir * $pv)));
415 0           my $denominator = log(1 + $ir);
416 0           return $numerator / $denominator;
417             };
418 0 0         return $@ ? undef : $result;
419             }
420              
421              
422             sub simple_interest {
423             =pod
424              
425             =head2 simple_interest
426              
427             C<$calc-Esimple_interest>
428              
429             C<$calc-Esimple_interest-E('ir')>
430              
431             C<$calc-Esimple_interest-E(find =E 'ir')>
432              
433             This works just like compound interest, but there is no consideration of C.
434             With any 2 of pv, fv, and ir, you can always solve for the third.
435              
436             Without arguments, the method will attempt to figure out what you'd like to solve
437             based on what attributes of the object have been defined. Usually, you'll probably want to
438             explicitly request what attribute you'd like returned, which you can do using
439             the second or third method.
440              
441             =cut
442 0     0 1   my ($self,@args) = _get_self(@_);
443 0 0         (scalar(@args) == 1) and unshift(@args,'find');
444 0 0         if (scalar(@args) > 2) {
445 0           my $temp = __PACKAGE__->new(@args[2..$#args]);
446 0           return $temp->simple_interest(@args[0..1]);
447             };
448 0           my $solve_for = $self->_get_attribute_key($args[1]);
449 0           my (@numbers,$ir,$pv,$pmt,$result);
450 0 0         if (not(defined($solve_for))) {
451 0 0         if (@numbers = $self->_verify_fields(IR,PV)) { $solve_for = PMT; }
  0 0          
    0          
452 0           elsif (@numbers = $self->_verify_fields(IR,PMT)) { $solve_for = PV; }
453 0           elsif (@numbers = $self->_verify_fields(PMT,PV)) { $solve_for = IR; }
454 0           else { return undef; };
455             } else {
456 0           my @combos = ();
457 0           $combos[PV] = [IR,PMT]; $combos[IR] = [PMT,PV]; $combos[PMT] = [IR,PV];
  0            
  0            
458 0           $set = $combos[$solve_for];
459 0 0         @numbers = $self->_verify_fields(@$set) or return undef;
460             }
461             # equations go here
462 0 0         if ($solve_for == PMT) {
    0          
    0          
463 0           $result = $numbers[1] * ($numbers[0]/100);
464             } elsif ($solve_for == PV) {
465 0           eval { $result = $numbers[1]/($numbers[0]/100); };
  0            
466             } elsif ($solve_for == IR) {
467 0           eval { $result = ($numbers[0]/$numbers[1]) * 100; };
  0            
468             }
469 0 0         return ($@) ? undef : $result;
470             }
471              
472             sub _get_self {
473 0 0 0 0     my $self = (ref($_[0]) !~ /$re_object/o) ? $DEFAULT_OBJECT ||= new __PACKAGE__ : shift(@_) ;
474 0           return($self,@_);
475             }
476              
477             sub _verify_fields {
478 0     0     my ($self,@args) = _get_self(@_);
479 0           my @defined = grep(/[0-9]/, @$self[@args]);
480 0 0         return (scalar(@defined) == scalar(@args)) ? @defined : ();
481             }
482              
483              
484             1;
485              
486             __END__