DEV3 Technologies home

EMI and amortisation schedule

Calculate a fixed monthly instalment and a full month-by-month principal and interest schedule that ends exactly at zero.

React / Next.jsUtilitiesloanemifinanceamortisation

Code

export type Row = { month: number; payment: number; interest: number; principal: number; balance: number };

const cents = (x: number) => Math.round(x * 100);

/** Monthly instalment in currency units (rounded to cents). */
export function emi(principal: number, annualRatePct: number, months: number): number {
  if (!(principal > 0) || !Number.isInteger(months) || months < 1) throw new Error('Invalid loan inputs');
  const r = annualRatePct / 12 / 100;
  if (r === 0) return cents(principal / months) / 100; // no interest: avoid dividing by zero
  const growth = (1 + r) ** months;
  return cents((principal * r * growth) / (growth - 1)) / 100;
}

/** Full schedule in integer cents; the last payment clears the balance exactly. */
export function schedule(principal: number, annualRatePct: number, months: number) {
  const r = annualRatePct / 12 / 100;
  const payment = cents(emi(principal, annualRatePct, months));
  let balance = cents(principal);
  const rows: Row[] = [];
  let totalInterest = 0;

  for (let month = 1; month <= months; month++) {
    const interest = Math.round(balance * r);
    let pay = payment;
    let principalPaid = pay - interest;
    if (month === months || principalPaid > balance) {
      principalPaid = balance; // final instalment absorbs rounding differences
      pay = principalPaid + interest;
    }
    balance -= principalPaid;
    totalInterest += interest;
    rows.push({ month, payment: pay, interest, principal: principalPaid, balance });
  }

  const totalPaid = rows.reduce((sum, row) => sum + row.payment, 0);
  return { rows, totalInterest, totalPaid }; // every amount is in cents
}

How it works

What it solves

Loan calculators are easy to get almost right. The standard formula returns a long decimal, rounding each month independently leaves a few cents over at the end, and a zero interest rate breaks the formula with a division by zero. This module handles all three and returns a schedule you can show in a table or export.

How it works

emi implements P * r * (1 + r)^n / ((1 + r)^n - 1) where r is the monthly rate (annual rate divided by twelve and by one hundred) and n is the number of months. If the rate is zero it simply returns principal divided by months. The result is rounded to cents once.

schedule works in integer cents to avoid floating point drift. Each month the interest is the balance times the monthly rate, rounded; principal paid is the instalment minus interest; and the balance falls by the principal. Because the instalment was rounded, the last row would not land on exactly zero, so the final payment is adjusted to equal the remaining balance plus its interest. This is how real lenders settle the last payment.

The function also returns totals for interest and amount paid, which are the numbers users usually look at first.

Adapting it

  • Add an optional extra monthly prepayment and stop when the balance reaches zero.
  • Use a day-count basis for exact interest between dates.
  • Group rows by year for a summary view.

Pitfalls

  • Banks may round differently or use daily interest; treat results as estimates.
  • Validate that months is a positive integer before calling.

How to use it

  1. Save as lib/emi.ts (pure TypeScript).
  2. const { rows, totalInterest } = schedule(250000, 7.5, 240) for a principal, annual percent and number of months.
  3. Format the cent values with Intl.NumberFormat after dividing by 100.
  4. emi(250000, 7.5, 240) returns just the monthly instalment in currency units.

Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.

Related snippets