Norway requires every consumer loan to advertise its effective annual rate, not the nominal one. The difference sounds like a rounding detail. It is not, and the reason is that there is no closed-form solution for it, so you end up writing a root finder to display a number on a landing page.
I built five small loan calculators recently, all vanilla JS, no dependencies, no build step. This is the part that took the longest to get right.
The easy half: the payment itself
The annuity payment for a fixed-rate loan is closed form and pleasant:
// pv = principal
// r = periodic rate (annual nominal / 12)
// n = number of periods
function payment(pv, r, n) {
if (r === 0) return pv / n;
return (r * pv) / (1 - Math.pow(1 + r, -n));
}
That covers the nominal rate. If you stop here, your calculator disagrees with every bank in the country.
The hard half: fees change the rate
The effective rate has to account for what the borrower actually receives and actually pays. An origination fee taken off the top means you borrow 300,000 but receive 297,500. A monthly administration fee means each payment is larger than the annuity formula says.
So the real cash flow is:
- at t=0 you receive
pv - originationFee - at t=1..n you pay
payment(pv, r, n) + monthlyFee
The effective periodic rate i is whatever makes the present value of those payments equal the amount received:
$$\sum_{t=1}^{n} \frac{P}{(1+i)^t} = PV_{net}$$
There is no algebraic solution for i. You have to search for it.
Bisection, not Newton
My first version used Newton-Raphson. It is faster, and it is also the one that will hand you a NaN at 2am when someone enters a 0% rate with a fee, because the derivative goes flat and you divide by roughly nothing.
Bisection is slower and cannot fail on a bracketed root. For something that runs once per keystroke on a form, slower is free:
function effectiveRate(pv, nominalAnnual, n, originationFee = 0, monthlyFee = 0) {
const net = pv - originationFee;
const pmt = payment(pv, nominalAnnual / 12, n) + monthlyFee;
const pvAt = (i) => {
if (i === 0) return pmt * n;
return pmt * (1 - Math.pow(1 + i, -n)) / i;
};
let lo = 0, hi = 1; // 0% to 100% per month, wide enough for anything legal
for (let k = 0; k < 200; k++) {
const mid = (lo + hi) / 2;
if (pvAt(mid) > net) lo = mid; else hi = mid;
}
const monthly = (lo + hi) / 2;
return Math.pow(1 + monthly, 12) - 1; // annualise by compounding, not by x12
}
Two things worth pointing out.
pvAt uses the annuity present value formula rather than looping the sum. Same result, and it stays accurate at long terms where summing 360 divisions accumulates noticeably.
The last line compounds instead of multiplying. A monthly rate of 1% is not 12% a year, it is 12.68%. Multiplying by twelve here is the single most common mistake I found when I checked what other calculators were outputting, and it under-reports the rate the borrower is quoted.
Money and floats
I kept everything in kroner as floats and rounded only at display. That is usually the wrong advice, but here the inputs are user estimates rather than ledger entries, and nothing is settled against a real balance. If you are reconciling actual payments, use integer minor units instead.
What did matter was rounding for display consistently:
const nok = new Intl.NumberFormat('nb-NO', {
style: 'currency', currency: 'NOK', maximumFractionDigits: 0
});
Intl.NumberFormat handles the Norwegian thousands separator, which is a non-breaking space, not a comma or a period. Doing that manually produces output that looks subtly foreign to every Norwegian reader.
No build step
All five are a single HTML file with inline CSS and JS, served from GitHub Pages. Total page weight under 6 KB. No framework, no bundler, no dependency to patch six months from now.
The tradeoff is real: no component reuse, and the fifth one shares no code with the first. For pages this small that was cheaper than the alternative.
The calculators are here if you want to look at the source:
- Debt consolidation calculator
- Effective interest rate calculator
- Rate increase calculator
- Debt free date calculator
- Repayment prioritisation
Disclosure so nobody has to guess: I work on samlegjeld.no, a Norwegian debt consolidation site, and these were built for it. The maths is jurisdiction-neutral though, and the effective rate problem is the same anywhere consumer credit disclosure is regulated.
If you have built one of these and solved the fee handling differently, I would genuinely like to hear it. The disagreement between calculators on the same inputs is larger than it should be.
Top comments (0)