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 )