File "equations.gpl.js"
Full Path: /home/cnerdvmd/public_html/wp-content/plugins/fc-loan-calculator/src/equations.gpl.js
File size: 17.88 KB
MIME-type: text/plain
Charset: utf-8
/* required for JSLint processing */
/*global alert: false, LIB$: false*/
/*jslint sub: true, vars: true, nomen: true*/
/**
* @preserve Copyright 2016 Pine Grove Software, LLC
* financial-calculators.com
* pine-grove.com
* equations.js
*/
/** @const */
var EQ$ = (function (LIB) {
'use strict';
var EQ = LIB || {},
periodType = LIB.INITIAL_PERIOD_TYPE.REGULAR,
// if a regular length 1st period or true if cash flow at beginning of period
isRegular,
// interest rate cannot be changed, thus rate vars are here.
periodicRate,
periodLen,
schedule1 = [],
DECEM = 12, // december, NOT 0 based. 1..12
// index (columns) values for 2 dimensional schedule array
EVENT_DATE = 0, // YYYYMMDD, used for sorting
LOAN_NO = 1,
ROW_TYPE = 2,
PER_STR = 3,
DATE_STR = 4,
CF = 5, // cash flow
CREDIT = 6,
DEBIT = 7,
INT = 8,
PRIN = 9,
NET = 10, // net change
BAL = 11,
MONTH = 12,
YEAR = 13;
/** @nocollapse */
EQ.roundingMethod = 0; // payment
// local amortization methods
EQ.AMORT_MTHD_EXT = {NORMAL: 0};
EQ.AMORT_MTHD_STRS_EXT = ['Normal'];
EQ.schedule = {
schedule1: 1
};
/**
* details about current cash flow
* exported values for schedule & charts
*/
EQ.summary = {
cf: [], // cash flow amount, payment, deposit etc
firstDebitDateStr: "",
firstCreditDateStr: "",
lastDebitDateStr: [],
lastCreditDateStr: [],
totalNDebits: [],
totalNCredits: [],
totalInterest: [],
totalPmts: [],
nominalRate: [],
NYears: 0,
pointsPct: 0,
pointsMoney: 0,
amortMthd: 0,
DIY: 0,
unadjustedBalance: 0,
roundingMethod: EQ.roundingMethod,
cashFlowType: 0, // loan
xPmtTotal: 0
};
/**
* Parameter object to initialize all financial equations
* @constructor
* @param {Object=} obj (optional)
*/
EQ.fin_params = function (obj) {
var o = obj || {};
this.nominalRate = (o.nominalRate !== undefined) ? o.nominalRate : null;
this.n = o.n || null; // number of cash flows
this.cf = o.cf || 0.0; // cash flow / pmt amount
this.pv = o.pv || 0.0;
this.fv = o.fv || 0.0;
this.pmtMthd = (o.pmtMthd !== undefined) ? o.pmtMthd : null; // Excel's "type" advance = 0 / arrears = 1
this.amortMthd = (o.amortMthd !== undefined) ? o.amortMthd : EQ.AMORT_MTHD_EXT.NORMAL;
this.oDate = o.oDate || new Date(0); // uninitialized origination date obj
this.fDate = o.fDate || new Date(0); // first cash flow date
this.lDate = o.lDate || new Date(0); // last cash flow date
};
// clone the initialization object
EQ.fin_params.prototype.clone = function () {
var o = {
nominalRate: this.nominalRate,
n: this.n,
cf: this.cf,
pv: this.pv,
fv: this.fv,
pmtFreq: this.pmtFreq,
cmpFreq: this.cmpFreq,
daysPerYear: this.daysPerYear,
pmtMthd: this.pmtMthd,
amortMthd: this.amortMthd,
oDate: new Date(this.oDate.valueOf()),
fDate: new Date(this.fDate.valueOf()),
lDate: new Date(this.lDate.valueOf())
};
return new EQ.fin_params(o);
};
/**
* initFirstPeriod()
* @param {Object} obj (required)
*/
function initFirstPeriod(obj) {
var periods = 0;
periodType = LIB.INITIAL_PERIOD_TYPE.REGULAR;
if (obj.oDate.valueOf() !== obj.fDate.valueOf()) {
periods = 1;
}
return periods;
} // initFirstPeriod(obj)
/**
* calcRates(obj)
* periodic rate is nominalRate / periods per year
* @param {Object} obj (required)
*/
function calcRates(obj) {
periodicRate = obj.nominalRate / LIB.PPY[obj.cmpFreq];
return periodicRate;
}
/**
* calcInt_periodicRate
* calculate interest on an amount (pv) for X periods
*/
function calcInt_periodicRate(pv, periods) {
var s, fv;
fv = pv;
if (periodicRate !== 0) {
// regular periods of the cash flow from fDate
s = Math.pow(1 + periodicRate, periods);
fv = fv * s; // * (s - 1.0) * (1.0 + periodicRate) / periodicRate; // note negative result
}
return fv - pv; // interest
} // calcInt_periodicRate
/**
* isValid(obj)
* Validate the inputs common to all equations
* @param {Object} obj (required)
*/
function isValid(obj) {
// set default dates
// start date default to 1st of next month
if (obj.oDate.valueOf() === 0) {
obj.oDate.setTime(LIB.dateMath.getFirstNextMonth(new Date()));
}
// if first cash flow date not initialized then set 1 pmtFreq unit after start date
if (obj.pmtMthd === LIB.pmt_method) {
obj.fDate.setTime(LIB.dateMath.addPeriods(obj.oDate, 1));
} else {
obj.fDate.setTime(obj.oDate.getTime());
}
return true;
}
/**
* Calc cash flow amount - loan payment
* Cash flow amounts will not change
* Interest rate will not change
*/
EQ.CF = {
/**
* EQ.CF.calc()
* Validates the user inputs
* Calculates an optimized / level cash flow amount
* @param {Object} obj (required)
*/
calc: function (obj) {
var r, s, cf;
// classic loan payment formula : http://www.financeformulas.net/Loan_Payment_Formula.html
r = calcRates(obj);
if (obj.pmtMthd === EQ.pmt_method) {
cf = r * obj.pv;
cf = -(cf / (1 - Math.pow(1 + r, -obj.n)));
} else {
s = 1 + r;
cf = (1 - Math.pow(s, -obj.n + 1)) / r;
cf = -(obj.pv / (cf + 1));
}
return cf;
} // calc: function (obj)
}; // EQ.CF
////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////// END EQUATIONS - BUILD SCHEDULE ARRAY
////////////////////////////////////////////////////////////////////////////////////////
/**
* insertSubTotals()
* Insert subtotals into schedule based on declared fiscal year end
* Last month of year, i.e. before total rows 1..12
*/
function insertSubTotals(schedule, whichSchedule) {
var i, ytdCF = 0.0, ytdCredit = 0.0, ytdDebit = 0.0, ytdInterest = 0.0, ytdPrincipal = 0.0, ytdNetChange = 0.0, ytdXPmt = 0.0, runningCF = 0.0, runningCredit = 0.0, runningDebit = 0.0, runningInterest = 0.0, runningPrincipal = 0.0, runningNetChange = 0.0, runningXPmt = 0.0, totals = [];
if (whichSchedule === undefined || whichSchedule === null) {
whichSchedule = LIB.schedule.schedule1;
}
i = 0;
do {
i += 1; // totals can only be inserted after array row 1
// this test may be needed
// if (schedule[i - 1][ROW_TYPE] === 1) {
// note, pick up values from last row after loop exit
ytdCF = LIB.roundMoney(schedule[i - 1][CF] + ytdCF);
ytdCredit = LIB.roundMoney(schedule[i - 1][CREDIT] + ytdCredit);
ytdDebit = LIB.roundMoney(schedule[i - 1][DEBIT] + ytdDebit);
ytdInterest = LIB.roundMoney(schedule[i - 1][INT] + ytdInterest);
ytdPrincipal = LIB.roundMoney(schedule[i - 1][PRIN] + ytdPrincipal);
//ytdXPmt = LIB.roundMoney(schedule[i - 1][PRIN] + ytdXPmt);
ytdNetChange = LIB.roundMoney(schedule[i - 1][NET] + ytdNetChange);
runningCF = LIB.roundMoney(schedule[i - 1][CF] + runningCF);
runningCredit = LIB.roundMoney(schedule[i - 1][CREDIT] + runningCredit);
runningDebit = LIB.roundMoney(schedule[i - 1][DEBIT] + runningDebit);
runningInterest = LIB.roundMoney(schedule[i - 1][INT] + runningInterest);
runningPrincipal = LIB.roundMoney(schedule[i - 1][PRIN] + runningPrincipal);
//runningXPmt = LIB.roundMoney(schedule[i - 1][PRIN] + runningXPmt);
runningNetChange = LIB.roundMoney(schedule[i - 1][NET] + runningNetChange);
if (schedule[i - 1][PER_STR].toUpperCase() === 'XPMT') {
ytdXPmt = LIB.roundMoney(schedule[i - 1][PRIN] + ytdXPmt);
runningXPmt = LIB.roundMoney(schedule[i - 1][PRIN] + runningXPmt);
}
// one test for when consecutive cash flows are in different calendar year and another test when cash flows are in same calendar year
if (((schedule[i - 1][YEAR] !== schedule[i][YEAR] && (schedule[i - 1][MONTH] <= DECEM || schedule[i][MONTH] > DECEM))) || ((schedule[i - 1][YEAR] === schedule[i][YEAR] && (schedule[i - 1][MONTH] <= DECEM && DECEM < schedule[i][MONTH])))) {
// insert the 2 total rows into schedule at fiscal year end
totals = [];
totals[EVENT_DATE] = schedule[i - 1][EVENT_DATE];
totals[EVENT_DATE] = totals[EVENT_DATE].substr(0, totals[EVENT_DATE].length - 3) + '-99';
totals[LOAN_NO] = whichSchedule;
totals[ROW_TYPE] = LIB.ROW_TYPES.ANNUAL_TOTALS; // YTD total row marker
totals[PER_STR] = schedule[i - 1][YEAR] + ' YTD:';
totals[DATE_STR] = null;
totals[CF] = ytdCF;
totals[CREDIT] = ytdCredit;
totals[DEBIT] = ytdDebit;
totals[INT] = ytdInterest;
totals[NET] = ytdNetChange;
totals[PRIN] = ytdPrincipal;
totals[BAL] = null;
totals[MONTH] = DECEM; // schedule[i - 1][MONTH];
totals[YEAR] = schedule[i - 1][YEAR];
// reset year-to-date
ytdCF = 0;
ytdCredit = 0.0;
ytdDebit = 0.0;
ytdInterest = 0.0; // schedule[0][INT];
ytdPrincipal = 0.0;
ytdNetChange = 0.0;
ytdXPmt = 0.0;
schedule.splice(i, 0, totals);
i += 1; // increment for row just insert
totals = [];
totals[EVENT_DATE] = schedule[i - 1][EVENT_DATE];
totals[EVENT_DATE] = totals[EVENT_DATE].substr(0, totals[EVENT_DATE].length - 3) + '-99';
totals[LOAN_NO] = whichSchedule;
totals[ROW_TYPE] = LIB.ROW_TYPES.RUNNING_TOTALS; // running total row marker
totals[PER_STR] = 'Running Totals:';
totals[DATE_STR] = null;
totals[CF] = runningCF;
totals[CREDIT] = runningCredit;
totals[DEBIT] = runningDebit;
totals[INT] = runningInterest;
totals[NET] = runningNetChange;
totals[PRIN] = runningPrincipal;
totals[BAL] = null;
totals[MONTH] = DECEM; // schedule[i - 1][MONTH];
totals[YEAR] = schedule[i - 1][YEAR];
schedule.splice(i, 0, totals);
i += 1; // increment for row just insert
}
} while (i < schedule.length - 1);
if (schedule[schedule.length - 1][ROW_TYPE] !== LIB.ROW_TYPES.RUNNING_TOTALS) {
// pick up the values from the last row
ytdCF = LIB.roundMoney(schedule[schedule.length - 1][CF] + ytdCF);
ytdCredit = LIB.roundMoney(schedule[schedule.length - 1][CREDIT] + ytdCredit);
ytdDebit = LIB.roundMoney(schedule[schedule.length - 1][DEBIT] + ytdDebit);
ytdInterest = LIB.roundMoney(schedule[schedule.length - 1][INT] + ytdInterest);
ytdPrincipal = LIB.roundMoney(schedule[schedule.length - 1][PRIN] + ytdPrincipal);
ytdNetChange = LIB.roundMoney(schedule[schedule.length - 1][NET] + ytdNetChange);
runningCF = LIB.roundMoney(schedule[schedule.length - 1][CF] + runningCF);
runningCredit = LIB.roundMoney(schedule[schedule.length - 1][CREDIT] + runningCredit);
runningDebit = LIB.roundMoney(schedule[schedule.length - 1][DEBIT] + runningDebit);
runningInterest = LIB.roundMoney(schedule[schedule.length - 1][INT] + runningInterest);
runningPrincipal = LIB.roundMoney(schedule[schedule.length - 1][PRIN] + runningPrincipal);
runningNetChange = LIB.roundMoney(schedule[schedule.length - 1][NET] + runningNetChange);
if (schedule[schedule.length - 1][PER_STR].toUpperCase() === 'XPMT') {
ytdXPmt = LIB.roundMoney(schedule[schedule.length - 1][PRIN] + ytdXPmt);
runningXPmt = LIB.roundMoney(schedule[schedule.length - 1][PRIN] + runningXPmt);
}
// add final set of total rows
totals = [];
totals[EVENT_DATE] = schedule[i][EVENT_DATE];
totals[EVENT_DATE] = totals[EVENT_DATE].substr(0, totals[EVENT_DATE].length - 3) + '-99';
totals[LOAN_NO] = whichSchedule;
totals[ROW_TYPE] = LIB.ROW_TYPES.ANNUAL_TOTALS; // YTD total row marker
totals[PER_STR] = schedule[schedule.length - 1][YEAR] + ' YTD:';
totals[DATE_STR] = null;
totals[CF] = ytdCF;
totals[CREDIT] = ytdCredit;
totals[DEBIT] = ytdDebit;
totals[INT] = ytdInterest;
totals[PRIN] = ytdPrincipal;
totals[NET] = ytdNetChange;
totals[BAL] = null;
totals[MONTH] = DECEM; // schedule[schedule.length - 1][MONTH];
totals[YEAR] = schedule[schedule.length - 1][YEAR];
schedule.push(totals);
totals = [];
totals[EVENT_DATE] = schedule[i][EVENT_DATE];
totals[EVENT_DATE] = totals[EVENT_DATE].substr(0, totals[EVENT_DATE].length - 3) + '-99';
totals[LOAN_NO] = whichSchedule;
totals[ROW_TYPE] = LIB.ROW_TYPES.RUNNING_TOTALS; // running total row marker
totals[PER_STR] = 'Running Totals:';
totals[DATE_STR] = null;
totals[CF] = runningCF;
totals[CREDIT] = runningCredit;
totals[DEBIT] = runningDebit;
totals[INT] = runningInterest;
totals[PRIN] = runningPrincipal;
totals[NET] = runningNetChange;
totals[BAL] = null;
totals[MONTH] = DECEM; // schedule[schedule.length - 1][MONTH];
totals[YEAR] = schedule[schedule.length - 1][YEAR];
schedule.push(totals);
}
EQ.summary.totalInterest[whichSchedule] = runningInterest;
EQ.summary.totalPmts[whichSchedule] = runningCF;
return null;
} // insertSubTotals()
/**
* initLoanScheduleArray()
* @param {Object} obj (required)
*/
function initLoanScheduleArray(obj) {
var L, balance, pmt, deposit, withdrawal, scheduledDateStr, sortDateStr, periodYearString, periods, nDays, nYears = 1, interestAccrued = 0, principalPaid = 0, fixedPrincipal = 0.0, trans = [], priorDate = new Date(0), transDate = new Date(0), intDenom = 0.0, totalInterestDue = 0.0, runningInterest = 0.0, schedule = [];
var whichSchedule = LIB.schedule.schedule1;
if (isValid(obj)) {
EQ.summary.nominalRate[whichSchedule] = obj.nominalRate;
EQ.summary.pmtFreq = obj.pmtFreq;
EQ.summary.cmpFreq = obj.cmpFreq;
EQ.summary.amortMthd = obj.amortMthd;
calcRates(obj);
// Assume regular length.
periods = initFirstPeriod(obj);
pmt = 0; // no payment with origination row, but first payment may be same date, however, in next schedule row
balance = obj.pv;
deposit = balance;
withdrawal = null;
// process origination
priorDate.setTime(obj.oDate.getTime());
scheduledDateStr = LIB.dateMath.dateToDateStr(priorDate, LIB.dateConventions);
EQ.summary.firstDebitDateStr = scheduledDateStr;
sortDateStr = LIB.dateMath.dateToDateStr(priorDate, LIB.sortConventions);
L = 0;
periodYearString = L + ':' + nYears;
schedule.push([sortDateStr, whichSchedule, LIB.ROW_TYPES.DETAIL, periodYearString, scheduledDateStr, pmt, deposit, withdrawal, interestAccrued, principalPaid, null, balance, priorDate.getMonth() + 1, priorDate.getFullYear(), priorDate, priorDate.valueOf()]);
// process 1st cash flow as special case...may not be regular length period
L = 1;
periodYearString = L + ':' + nYears;
pmt = -obj.cf; // payments are passed as debits, i.e. negative
transDate.setTime(obj.fDate.getTime());
scheduledDateStr = LIB.dateMath.dateToDateStr(transDate, LIB.dateConventions);
EQ.summary.firstCreditDateStr = scheduledDateStr;
sortDateStr = LIB.dateMath.dateToDateStr(transDate, LIB.sortConventions);
withdrawal = obj.cf;
deposit = null;
// 1 period of interest
interestAccrued = LIB.roundMoney(calcInt_periodicRate(balance, periods));
interestAccrued = LIB.roundMoney(interestAccrued);
principalPaid = LIB.roundMoney(pmt - interestAccrued);
balance = LIB.roundMoney(balance + interestAccrued + obj.cf);
schedule.push([sortDateStr, whichSchedule, LIB.ROW_TYPES.DETAIL, periodYearString, scheduledDateStr, pmt, deposit, withdrawal, interestAccrued, principalPaid, null, balance, transDate.getMonth() + 1, transDate.getFullYear(), transDate, transDate.valueOf()]);
// process from the first regular cash flow until balance is 0 or less or for number of user assigned periods (balloon)
L = 2;
// all remaining periods have to be pmtFreq length
periods = 1;
do {
priorDate.setTime(transDate.getTime());
transDate.setTime(LIB.dateMath.addPeriods(transDate, 1));
scheduledDateStr = LIB.dateMath.dateToDateStr(transDate, LIB.dateConventions);
sortDateStr = LIB.dateMath.dateToDateStr(transDate, LIB.sortConventions);
if (L % LIB.PPY[obj.pmtFreq] === 1) {
nYears += 1;
}
periodYearString = L + ':' + nYears;
// 1 period of interest
interestAccrued = LIB.roundMoney(calcInt_periodicRate(balance, periods));
interestAccrued = LIB.roundMoney(interestAccrued);
principalPaid = LIB.roundMoney(pmt - interestAccrued);
balance = LIB.roundMoney(balance + interestAccrued + obj.cf);
// record type 1, detail schedule row
schedule.push([sortDateStr, whichSchedule, LIB.ROW_TYPES.DETAIL, periodYearString, scheduledDateStr, pmt, deposit, withdrawal, interestAccrued, principalPaid, null, balance, transDate.getMonth() + 1, transDate.getFullYear(), transDate, transDate.valueOf()]);
// [KT] 11/13/2016 - add arbitrary overflow check, 99 trillion
if (Math.abs(balance) > 99999999999995.50) {
alert('Internal limit reached. Balance exceeds +/- 99 trillion.');
schedule = [];
schedule1 = schedule;
return schedule1;
}
L += 1;
} while (L <= obj.n && balance > 0);
trans = schedule[schedule.length - 1];
EQ.summary.unadjustedBalance = trans[BAL];
EQ.summary.cf[whichSchedule] = trans[CF]; // prior to any rounding
trans[CF] = trans[CF] + EQ.summary.unadjustedBalance;
trans[BAL] = trans[BAL] - EQ.summary.unadjustedBalance;
trans[PRIN] = trans[PRIN] + EQ.summary.unadjustedBalance;
EQ.summary.totalNDebits[whichSchedule] = L - 1;
EQ.summary.totalNCredits[whichSchedule] = 1;
EQ.summary.lastDebitDateStr[whichSchedule] = schedule[0][DATE_STR]; // loan date
EQ.summary.lastCreditDateStr[whichSchedule] = trans[DATE_STR];
insertSubTotals(schedule, whichSchedule);
schedule1 = schedule;
return schedule1;
} // if (isValid(obj))
return null; // error
} // initLoanScheduleArray(obj)
/**
* Calc loan schedule
*/
EQ.LOAN_SCHEDULE = {
/**
* EQ.LOAN_SCHEDULE.calc()
* Validates the user inputs
* @param {Object} obj (required)
*/
calc: function (obj) {
initLoanScheduleArray(obj);
return schedule1;
}
}; // EQ.LOAN_SCHEDULE
return EQ;
}(LIB$));