Functional Weave
Code in Rust

lending.loan-payment@1.0.1

impl/typescript.ts

3,440 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, roundDiv } 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

/** A hundred years of monthly payments, sixty of weekly ones. */
export const MAX_PAYMENTS = 3000;

/**
 * The number of payments a term holds, after checking the arguments every
 * lending capability shares.
 */
export function paymentCount(annualRateBasisPoints: number, termMonths: number, paymentsPerYear: number): number {
  if (!Number.isInteger(annualRateBasisPoints) || annualRateBasisPoints < 0 || annualRateBasisPoints > 100000) {
    throw new RangeError(`annualRateBasisPoints must be between 0 and 100000, received ${annualRateBasisPoints}`);
  }
  if (!Number.isInteger(paymentsPerYear) || paymentsPerYear < 1 || paymentsPerYear > 52) {
    throw new RangeError(`paymentsPerYear must be between 1 and 52, received ${paymentsPerYear}`);
  }
  if (!Number.isInteger(termMonths) || termMonths < 1) {
    throw new RangeError(`termMonths must be at least 1, received ${termMonths}`);
  }
  if ((termMonths * paymentsPerYear) % 12 !== 0) {
    throw new RangeError(`a term of ${termMonths} months is not a whole number of payments at ${paymentsPerYear} a year`);
  }
  const n = (termMonths * paymentsPerYear) / 12;
  if (n > MAX_PAYMENTS) {
    throw new RangeError(`${n} payments is more than the ${MAX_PAYMENTS} payment limit`);
  }
  return n;
}

/**
 * Round the exact quotient numerator / denominator (both positive) with a
 * math.round-div mode, however large they are. Only the integer part and
 * where the remainder sits against one half matter, so the decision is handed
 * to roundDiv as a small fraction with the same integer part and the same
 * side of the half: q, q + 1/4, q + 1/2 or q + 3/4.
 */
export function roundWide(numerator: bigint, denominator: bigint, mode: RoundingMode): number {
  const q = numerator / denominator;
  const twice = (numerator % denominator) * 2n;
  const small = Number(q);
  if (twice === 0n) return roundDiv(small, 1, mode);
  if (twice === denominator) return roundDiv(2 * small + 1, 2, mode);
  return roundDiv(4 * small + (twice < denominator ? 1 : 3), 4, mode);
}

/**
 * The level payment that repays `principal` over the term at a nominal
 * annual rate divided equally between the periods:
 *
 *   payment = P · r / (1 − (1 + r)^−n),   r = rate / paymentsPerYear
 *
 * With r = b / D (b basis points, D = 10000 × paymentsPerYear) this is the
 * exact fraction P · b · (D + b)^n / (D · ((D + b)^n − D^n)), evaluated in
 * integers of whatever size it takes and rounded once. A zero rate is P / n.
 */
export function loanPayment(
  principal: Money,
  annualRateBasisPoints: number,
  termMonths: number,
  paymentsPerYear: number,
  mode: RoundingMode,
): Money {
  if (!Number.isInteger(principal.minor) || principal.minor <= 0) {
    throw new RangeError(`principal must be greater than zero, received ${principal.minor}`);
  }
  const n = paymentCount(annualRateBasisPoints, termMonths, paymentsPerYear);
  if (annualRateBasisPoints === 0) {
    return money(roundDiv(principal.minor, n, mode), principal.currency);
  }
  const b = BigInt(annualRateBasisPoints);
  const d = 10000n * BigInt(paymentsPerYear);
  const grown = (d + b) ** BigInt(n);
  const numerator = BigInt(principal.minor) * b * grown;
  const denominator = d * (grown - d ** BigInt(n));
  return money(roundWide(numerator, denominator, mode), principal.currency);
}