finance.compound-interest@1.0.0
impl/typescript.ts
2,408 bytes · the TypeScript implementation · view raw
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { roundDiv } from "./math_round_div.ts"; ← from math.round-div ^1.0.0 · built alongside by fune
import { subtractMoney } from "./money_add.ts"; ← from money.add ^1.0.0 · built alongside by fune
import { type Money, money } from "./money_amount.ts"; ← from money.amount ^1.0.0 · built alongside by fune
import { type CompoundInterestResult } from "./finance_compound_interest_types.ts";
/**
* A thousand years of daily compounding is already past the point where this
* is modelling anything; beyond it a caller has passed the wrong unit and is
* asking for a loop that will not finish.
*/
const MAX_PERIODS = 120000;
/**
* Compound interest accrued period by period, in whole minor units.
*
* The loop is the point. A statement is built by posting a real, rounded entry
* each period and letting the next period earn on the posted balance, so that
* is what happens here. principal * (1 + r/n)^(n*y) evaluated once at the end
* gives a slightly different answer and cannot be reconciled against the
* statement it disagrees with.
*
* The division by periodsPerYear stays inside the same rounding step as the
* interest itself: a nominal 4.5% compounded monthly is 37.5 basis points per
* period, and rounding that rate to 37 or 38 first would distort the result far
* more than rounding the accrued pennies ever does.
*/
export function compoundInterest(
principal: Money,
annualRateBasisPoints: number,
periodsPerYear: number,
years: number,
): CompoundInterestResult {
if (!Number.isInteger(annualRateBasisPoints)) {
throw new TypeError(`annualRateBasisPoints must be an integer, received ${annualRateBasisPoints}`);
}
if (!Number.isInteger(periodsPerYear) || periodsPerYear < 1) {
throw new RangeError(`periodsPerYear must be at least 1, received ${periodsPerYear}`);
}
if (!Number.isInteger(years) || years < 0) {
throw new RangeError(`years must not be negative, received ${years}`);
}
const periods = periodsPerYear * years;
if (periods > MAX_PERIODS) {
throw new RangeError(`${periods} periods is beyond the ${MAX_PERIODS} period limit`);
}
let balance = money(principal.minor, principal.currency);
for (let period = 0; period < periods; period += 1) {
const accrued = roundDiv(balance.minor * annualRateBasisPoints, 10000 * periodsPerYear, "half-up");
balance = money(balance.minor + accrued, balance.currency);
}
return {
principal: money(principal.minor, principal.currency),
interest: subtractMoney(balance, principal),
total: balance,
periods,
};
}