Business-US_Amort
view release on metacpan or search on metacpan
lib/Business/US_Amort.pm view on Meta::CPAN
=back
=head2 ITERATION ATTRIBUTES
These are attributes of little or no interest once C<run> is done, but
may be of interest to callbacks while C<run> is running, or may
be of interest in examining snapshots in C<table>.
=over
=item _month_count
This is how many months we are into the loan. The first month is 1.
=item _abort
If you want callbacks to be able to halt the iteration for some
reason, you can have them set C<_abort> to true. You may also choose
to set C<error> to something helpful.
=item _monthly_payment
The amount to be paid to toward the principal each month. At the start
of the loan, this is set to whatever C<initial_monthly_payment> is
figured to be, but you can manipulate C<_monthly_payment> with
callbacks to change how much actually gets paid when.
=item _remainder
The balance on the loan.
=item _date
The given month's date, if known, in the format "YYYY-MM". Unless you'd
set the C<start_date> to something, this will be undef.
=item _h
The interest to be paid this month.
=item _old_amount
What the remainder was before we made this month's payment.
=item _c
The current monthly payment, minus the monthly interest, possibly
tweaked in the last month to avoid paying off more than is actually left
on the loan.
=back
=cut
###########################################################################
%Proto = # public attributes and their values
(
principal => 0,
interest_rate => 8, # annual, percent
term => 30, # years (target term)
error => '',
cent_rounding => 1,
start_date => undef,
initial_monthly_payment => undef,
total_paid_interest => undef,
total_month_count => undef,
am_snapshot => 0, # flag for objects that are snapshots
block_table => 0, # set to 1 to block table generation
table => undef,
callback_before_monthly_calc => undef,
callback_after_monthly_calc => undef,
_month_count_limit => undef,
_abort => undef,
_remainder => undef,
_date => undef,
_h => undef,
_old_amount => undef,
_monthly_payment => undef,
);
#===========================================================================
# make accessors -- just simple scalar accessors
foreach my $k (keys %Proto) { # attribute method maker
no strict 'refs';
*{$k} = sub {
my $it = shift @_;
return ($it->{$k} = $_[0]) if @_;
return $it->{$k};
}
unless defined &{$k}
}
#--------------------------------------------------------------------------
# the usual doofy service methods
=head1 METHODS
=over
=item $loan = Business::US_Amort->new
Creates a new loan object.
=cut
sub new { # constructor
my $class = shift @_;
$class = ref($class) || $class;
return bless { %Proto, @_ }, $class;
}
=item $loan->copy
Copies a loan object or snapshot object. Also performs a somewhat
deep copy of its table, if applicable.
lib/Business/US_Amort.pm view on Meta::CPAN
}
sub DEAD::destroy { return }
#===========================================================================
=item $loan->start_date_be_now
This sets C<start_date> to the current date, based on C<$^T>.
=cut
sub start_date_be_now {
my $this = $_[0];
$this->{'start_date'} = &__date_now;
}
#===========================================================================
sub maybe_round {
my $this = $_[0];
return $this->{'cent_rounding'} ? (0 + sprintf("%.02f", $_[1])) : $_[1];
}
#===========================================================================
=item $loan->run
This performs the actual amortization calculations.
Returns 1 on success; otherwise returns 0, in which case you should
check the C<error> attribute.
=cut
sub run {
my $this = $_[0];
croak "Can't call loan->run() on a snapshot" if $this->{'am_snapshot'};
$this->{'error'} = '';
# not a whole lot of sanity checking here
unless($this->{'principal'} > 0) {
$this->{'error'} = 'principal must be positive and nonzero';
return 0;
}
$this->{'_remainder'} = $this->maybe_round( $this->{'principal'} ); # AKA "p"
unless($this->{'interest_rate'} >= 0) {
$this->{'error'} = 'interest rate must be nonnegative';
return 0;
}
$this->{'term'} = abs($this->{'term'} + 0);
unless($this->{'term'}) {
$this->{'error'} = 'term must be positive and nonzero';
return 0;
}
# The only real voodoo is here:
my $j = # monthly interest rate in decimal -- in percent, not like .0875
$this->{'interest_rate'} / 1200;
my $n = # number of months the loan is amortized over
int($this->{'term'} * 12);
#print "j: $j\n";
if($j) {
#print "Nonzero interest\n";
$this->{'initial_monthly_payment'} =
$this->maybe_round(
$this->{'_remainder'} * $j / ( 1 - (1 + $j) ** (-$n) )
);
} else {
# interest-free loan -- much simpler calculation
$this->{'initial_monthly_payment'} =
$this->maybe_round(
$this->{'_remainder'} / $n
);
}
# ...the rest is just iteration
# init...
$this->{'table'} = []; # clear
$this->{'total_paid_interest'} = 0;
$this->{'_monthly_payment'} = $this->{'initial_monthly_payment'};
# this can vary if the user starts tweaking it
$this->{'_month_count'} = 0;
$this->{'_date'} = $this->{'start_date'} || undef;
$this->{'_month_count_limit'} = $n * 2 + 12
unless defined $this->{'_month_count_limit'};
# throw an error if our _month_count ever hits this
my $last_month_date;
while($this->{'_remainder'} >= 0.01) { # while there's more than a cent left
++$this->{'_month_count'};
$this->{'_old_amount'} = $this->{'_remainder'};
# maybe call the 'before' callback
if($this->{'callback_before_monthly_calc'}) {
my @list = ($this);
&{$this->{'callback_before_monthly_calc'}}(@list);
}
if($this->{'_abort'}) { $this->{'error'} ||= "Abort flag set."; return 0 }
# and now all the calcs for this month
$this->{'_h'} = $this->maybe_round( $this->{'_remainder'}
* $this->{'interest_rate'} / 1200
);
$this->{'total_paid_interest'} += $this->{'_h'};
$this->{'_c'} = $this->{'_monthly_payment'} - $this->{'_h'};
if($this->{'_remainder'} > $this->{'_c'}) { # normal case
$this->{'_remainder'} = $this->maybe_round($this->{'_remainder'}
- $this->{'_c'});
} else { # exceptional end case
$this->{'_c'} = $this->{'_remainder'};
$this->{'_remainder'} = 0;
}
# maybe take a snapshot
( run in 0.997 second using v1.01-cache-2.11-cpan-364913b4093 )