lending.amortisation-schedule@1.0.0
impl/typescript.ts
2,672 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 { type RoundingMode } from "./math_round_div.ts"; ← from math.round-div ^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 { loanPayment, paymentCount, roundWide } from "./lending_loan_payment.ts"; ← from lending.loan-payment ^1.0.0 · built alongside by fune
import { type AmortisationRow, type AmortisationSchedule } from "./lending_amortisation_schedule_types.ts";
/**
* Interest for one period on a positive balance: balance × b / D, rounded
* half-up to the minor unit, which is what gets posted to the account.
*/
export function periodInterest(balance: number, annualRateBasisPoints: number, paymentsPerYear: number): number {
if (balance <= 0 || annualRateBasisPoints === 0) return 0;
const product = BigInt(balance) * BigInt(annualRateBasisPoints);
// Rust holds the product in an i64; refuse it everywhere rather than in one language.
if (product > 9223372036854775807n) throw new RangeError("balance too large for exact interest");
return roundWide(product, 10000n * BigInt(paymentsPerYear), "half-up");
}
/**
* The schedule a lender's system produces: each period's interest is
* computed on the opening balance and rounded to a whole minor unit, the
* level payment repays interest first and principal with the rest, and the
* last payment is whatever clears the balance, so it ends at exactly zero.
*/
export function amortisationSchedule(
principal: Money,
annualRateBasisPoints: number,
termMonths: number,
paymentsPerYear: number,
mode: RoundingMode,
): AmortisationSchedule {
const payment = loanPayment(principal, annualRateBasisPoints, termMonths, paymentsPerYear, mode);
const n = paymentCount(annualRateBasisPoints, termMonths, paymentsPerYear);
const currency = principal.currency;
const rows: AmortisationRow[] = [];
let balance = principal.minor;
let totalInterest = 0;
let totalPaid = 0;
for (let period = 1; period <= n && balance > 0; period += 1) {
const interest = periodInterest(balance, annualRateBasisPoints, paymentsPerYear);
// The last period, or one where the level payment would overshoot (a
// rounded-up payment can clear the loan a period early), pays exactly
// what is owed.
const due = period === n || balance + interest <= payment.minor ? balance + interest : payment.minor;
const repaid = due - interest;
balance -= repaid;
totalInterest += interest;
totalPaid += due;
rows.push({
period,
payment: money(due, currency),
interest: money(interest, currency),
principal: money(repaid, currency),
balance: money(balance, currency),
});
}
return {
payment,
rows,
totalInterest: money(totalInterest, currency),
totalPaid: money(totalPaid, currency),
};
}