Cat Toolbox
Help CatBlog About Partnership 中文

Mortgage, Interest and Percentage Calculators, and the Numbers They Do Not Show You

CalculatorUpdated 2026-09-29

Three calculator pages sit next to each other in the Calculator category, and each one is written differently enough from the others that the habits you build on one of them misfire on the next. This page is the reference for what each one computes, what it does when the input is silly, and which displayed numbers are not what they appear to be. Every figure below came from running the pages themselves, not from a manual.

Three pages, three different jobs

  • The mortgage calculator answers one question — what does a fixed-rate amortising loan cost — and it recomputes on every keystroke.
  • The interest calculator holds three separate models (simple, compound, and monthly contributions) behind three tabs, and it only computes when you press its button.
  • The percentage calculator answers four one-line questions with no time dimension at all, and prints the substitution next to each answer.

They also disagree about money. The mortgage page takes its amount in units of ten thousand and treats the result as CNY. The interest page's CSV header says USD. The percentage page is unitless, so any of its outputs can be quietly wrong if the two inputs were not in the same unit to begin with.

All three read numbers the same way: parseFloat(field.value) || 0. That single expression decides most of the edge-case behaviour, because parseFloat returns NaN for anything it cannot parse and || 0 converts that to zero. Empty box, abc, 12abc (which becomes 12) — all of them are accepted, and none of them produce an error.

Mortgage: what the headline number is

At the page defaults — 100 in the amount box (so 1,000,000), 30 years, 3.85% — the equal payment method gives:

Field Value
Monthly payment 4,688
Total repayment 1,687,710
Total interest 687,710
Interest as a share of everything paid 40.7%

The arithmetic is the standard annuity: periods are years × 12 and the monthly rate is the annual rate divided by 100 and by 12. That is the nominal convention, so a quoted rate is treated as if it compounds monthly at one twelfth of itself. A lender's APR that already rolls fees in will not match. The totals come from the unrounded payment, not from the figure printed in the headline box.

The same loan, repaid differently, costs 108,606 less

Switching to equal principal at the same defaults changes every headline number:

Method First / fixed payment Total repayment Total interest Interest share
Equal principal and interest 4,688 every month 1,687,710 687,710 40.7%
Equal principal 5,986 in month one 1,579,104 579,104 36.7%

The equal principal branch also prints a line under the headline: the first month is 5,986 and the payment falls by 8.91 each month after that. That decrement is principal ÷ periods × monthly rate, and it is exact for this model, which is why the total interest is lower — your balance is smaller earlier, and interest is charged on the balance.

The page keeps the two methods as two states of one variable, and nothing about your inputs carries between them except the raw field values.

Zero percent is rejected, even though the formula handles it

There is an explicit branch for a zero monthly rate: if r === 0 the payment is simply P / n. You cannot reach it. The guard that runs before any maths rejects the calculation whenever the amount is not above zero, the term is below 1, or the annual rate is not above zero — so a 0% loan prints dashes and the line Please enter complete parameters. An interest-free loan is the one case the code prepared for and the interface refuses.

The repayment table does not add up, by about three yuan

Every table cell goes through Math.round(value).toLocaleString(), so each row is rounded to a whole yuan independently. The totals are computed from exact values. That means the columns will not sum to the total row. Measured at the defaults:

  • Yearly view (31 rows: 30 years plus a Total row): the principal column adds up to 1,000,003, against a Total row of 1,000,000.
  • Monthly view (360 rows): the principal column adds up to 999,997. The monthly view has no Total row at all — 360 period rows and nothing after them — so there is no figure in the table to reconcile that column against, and the headline boxes above are the only totals it has.

The drift is a rounding artefact, not a wrong answer, and it grows with the number of rows you are summing. If you need columns that reconcile, take the exact monthly payment (4,688.0823…, i.e. 1,687,709.64 over 360 periods) rather than the displayed one.

The remainder column is also clamped — Math.max(0, remaining) — so the last row cannot show a negative balance, which hides any final-cent residue in the amortisation instead of displaying it.

The page source already contains an answer, and it is stale

Load the page with JavaScript disabled, or read its HTML, and you will find 4,686 for the monthly payment, 1,687,076 for the total, 687,076 for the interest, and 6,041 for the equal principal first month. Those are literal text in the markup. When the script runs, the same fields become 4,688 / 1,687,710 / 687,710 / 5,986. The pre-rendered numbers are 634 yuan off on the totals — the price of keeping a hand-written snapshot in the source. Trust the rendered page, and if you ever compare the two, remember which one the script overwrites.

The term is clamped; the rate is not

Typing 50 into the term box snaps it back to 30 — the script forces the value into Math.max(1, Math.min(30, v)) and syncs the slider. The rate box has a min="0.1" attribute, but attributes like that only affect the browser's own validity styling; nothing in the script checks the rate. So the defaults page will happily compute a 50% mortgage: 41,667 a month, 15,000,006 repaid, 93.3% of it interest. At 0.05% it gives 2,799 a month and 1,007,540 total (7,540 of interest, 0.7%), which is a more useful number — a thirty-year loan at that rate costs almost nothing beyond the principal.

The yearly and monthly tables are the same computation, just aggregated; nothing is recalculated differently when you toggle the checkbox, and the toggle is a live onchange. The years box is read with parseInt(...) || 1, so a 0 or junk in that field silently becomes a one-year loan rather than the "term below 1" rejection — only a negative number can reach that branch of the guard.

Interest: three tabs, three period conventions

The three tabs are not three variants of one formula, and the page's own copy says as much. What is worth pinning down is the period convention, because that is where the tabs silently differ.

Tab Period rule Rate per period Term input
Simple Continuous annual ÷ 100 years + months ÷ 12
Compound n per year, where n is 1 or 12 annual ÷ 100 ÷ n years + months ÷ 12
DCA Always monthly annual ÷ 100 ÷ 12 rounded to whole months

The compound tab has exactly two frequency buttons, yearly and monthly. At 10,000 principal, 5%, ten years they give 16,288.95 and 16,470.09 — the monthly option is 181.14 higher, which is the compounding gap and not a rounding issue. There is no quarterly or daily option to compare against.

The term fields are combined as years + months ÷ 12, and the DCA tab turns that back into whole periods with Math.round(term × 12). For ordinary whole-month entries the round-trip is exact: 1 year 7 months is 19 periods, 6 months is 6, and at the default 1,000 per period a 7-month plan gives 7 rows and 7,000 invested. The rounding only changes anything if you type a fraction into the months box, which the field accepts despite step="1" — 1 year 1.5 months is 13.5 periods on paper and 14 on screen (1 year 2.5 months likewise becomes 15), because JavaScript rounds a half upward. A fraction that rounds down is invisible in the same way: 1 year 0.4 months becomes a plain 12 periods. Either way you get a full contribution and a full month of interest for the extra period, so the headline total covers more time than you asked for.

Nothing computes until you press the button

This is the habit-breaking one. On the mortgage page every field has an oninput handler. On the interest page no field has one: the inputs are inert, and results only change when you press Calculate Simple Interest, Calculate Compound Interest or Calculate DCA Returns — or when you switch tabs, because tab switching recalculates the tab you land on. The contribution-timing dropdown (End of Period / Start of Period) has no change handler either, so choosing the other option leaves the old answer on screen until you press the button.

When the inputs fail, the numbers hide and the chart stays

Every tab guards on principal, rate and term all being above zero. Fail one and the result block loses its visibility class — measured as display: none — so the total, the interest and the yield disappear without any message. But the guard returns before the chart is redrawn, so the canvas keeps the curve from your previous, valid calculation. Blank the principal on the simple tab after a successful run and the figure area goes empty while the growth line above it stays exactly where it was, pixel for pixel.

The compound growth curve can end a year past your term

The simple tab clamps its chart points to the term you entered: at the defaults (10,000 principal, 5%) with a 6-month term, the second point is computed at min(1, 0.5) years and reads 10,250 — the correct six-month balance, labelled "1 Yr".

The compound tab does not clamp. Its chart points are computed at i × n periods for i up to ceil(term), so the same defaults with the term set to 0 year 6 months give a headline total of 10,246.95 and a curve whose last point is 10,500 — a full year of growth. (On the monthly frequency the same term reads 10,252.62 against a last point of 10,511.62.) The axis label of the last point says "1 Yr", which is honest: the curve really is plotting year one. It is the mismatch with the headline figure that will trip you up, and it appears whenever the term is not a whole number of years.

The English axis labels are off by ten times

Chart y-axis ticks are formatted by this line:

var labelText = val >= 10000 ? (val / 10000).toFixed(1) + 'K' : fmtInt(val);

It divides by ten thousand and appends K, so the gridline labelled 100.0K is actually 1,000,000. Verified on the compound tab at monthly frequency, 1,000,000 principal, 7%, twenty years (total 4,038,738.85), where the axis prints 0 / 100.0K / 200.0K / 300.0K / 400.0K / 500.0K for values up to five million. The Chinese version of this page uses the same divisor with the unit 万, which is correct — ten thousand is one 万 — so this is an artefact of translating the label and not the divisor. Read the English ticks as "ten-thousands", or ignore the tick text and use the figures.

The DCA schedule: one row per month, and a real export

The DCA tab is the only one that produces a table. It writes a row for every single period plus a Total row, so a ten-year plan is 121 rows and a thirty-year plan is 361. On the defaults (1,000 a month, 8%, ten years) the numbers are:

Timing Final value Total profit Cumulative invested
End of period 182,946.04 62,946.04 120,000
Start of period 184,165.68 64,165.68 120,000

Paying at the start is worth 1,219.64 over ten years, because every contribution earns for one extra month. The invested columns are rounded to whole units while the value and profit columns keep two decimals, so the Total row mixes precision.

Export CSV writes every period, not the sampled points the chart uses (the chart thins to roughly 120 samples). At the other extreme, a plan of 0 years 1 month produces its headline figures and a two-row table but no curve at all: the chart needs at least two points, so the canvas shows nothing but Insufficient data to draw chart. The header carries a byte-order mark so Excel opens it as UTF-8, names the columns in USD, and the file is DCA_Details_ plus the current ISO date. If no DCA calculation has run yet, the button raises No data to export. Please calculate DCA returns first. — and because the schedule is cached in a variable rather than read from the DOM, the export still works after you switch to another tab, as long as you computed DCA once this page load.

Resizing the window recalculates whichever tab is visible after a 300 ms pause, which is how the canvas gets redrawn at the new width.

Percentage: four questions, each printed with its own arithmetic

Each of the four modes shows the answer and the substitution line that produced it, which is the most useful feature on the page: 15% of 200 = 30, 30 ÷ 200 × 100% = 15%, (120 - 100) ÷ 100 × 100% = +20%, 200 × 30% = 60 discount, pay 200 - 60 = 140. Every field recalculates live, and the four example chips (15% tip, 20% off, 10% increase, 60% pass rate) switch the mode, fill both fields and compute.

Only the first mode is computed when the page loads. The other three panels show values that are written into the HTML — and those happen to agree with the default inputs, so nothing looks wrong until you change a default on a hidden panel and switch to it.

Division by zero answers 0 percent

Both modes with a denominator guard against dividing by zero, and the guard returns 0. So:

  • 30 ÷ 0 prints the answer 0% and the formula line 30 ÷ 0 × 100% = 0%.
  • Going from 0 to 120 prints +0% with the substitution (120 - 0) ÷ 0 × 100% = +0%.

There is no message, no "undefined", no disabled state. The formula line even shows you the zero denominator, right next to an answer that claims it worked out to nothing. Growth from a zero base is not zero percent, and 30 is not 0% of nothing. Read the denominator before you trust either answer.

Two decimals, then the zeros are stripped, so small answers collapse

Results are formatted with toFixed(2) and then a trailing-zero strip, which is why 15.00 shows as 15 and 25.50 shows as 25.5. The side effect is that anything below half a hundredth rounds to zero and then loses its decimal point entirely:

Inputs True value Displayed
12.5% of 200 25 25
33.333% of 1 0.33333 0.33
0.2% of 0.1 0.0002 0

That last row is the one to watch on money questions — a 0.2% fee on a small balance reads as exactly zero.

Nothing is clamped, including discounts over 100%

The discount mode is price × percent ÷ 100 and price − discount, with no bounds. Feed it 150% and you get Save 300, Pay -100. Feed it −20% and you get Save -40, Pay 240, with the substitution line printing the double negative verbatim as pay 200 - -40 = 240. Negative inputs are equally welcome: a % of b with a negative percentage returns a negative result and the formula line echoes the negative back.

The change mode signs its own output with r >= 0 ? '+' : '', so zero is displayed as +0% and 120 → 100 as -16.67%. Note the asymmetry that this exposes: the same 20-unit swing is +20% going up and −16.67% coming down, because the denominators differ — the page will happily show you both and explain neither.

Very large inputs are not formatted for humans. 1e21 at 50% prints 499999999999999934464 — the exponential form is expanded into a 21-digit string, and the digits beyond about the seventeenth are floating-point residue rather than arithmetic.

Using the three pages together

A sensible order for a real loan or investment decision:

  1. Use the mortgage page for anything with fixed payments against a shrinking balance, and read the interest share rather than the monthly payment. Compare the two methods before you compare lenders.
  2. Use the interest page for anything where money grows without a fixed repayment schedule: Compound for a lump sum, DCA for contributions. If your term has months in it, check the headline figure and not the end of the curve.
  3. Use the percentage page for the arithmetic checks around it — what a fee is in cash, what a discount actually saves, whether a quoted change is up or down from your own base. Keep the denominator in mind on every one of those.
  4. Reconcile by hand where they meet: a mortgage's total interest is not the same kind of number as a compound balance, and neither is a percentage change. If you get the same figure from two pages, that is worth double-checking rather than celebrating.

Where these pages stop

  • No page here reads a rate from anywhere; every one models a rate that never changes for the whole term. No prepayment, no rate reset, no annual step-up, no capitalisation of unpaid interest.
  • Nothing models volatility or default. The DCA schedule and the compound curve are both deterministic paths, not expected outcomes with a distribution around them.
  • The mortgage page has no fees, taxes, insurance or arrangement costs, so its total is the loan's, not the transaction's.
  • No input is persisted. Reload any of these pages and the defaults come back; nothing is saved to storage and nothing is sent anywhere — all three compute in the page's own script, and the only file any of them produces is the DCA CSV you explicitly ask for.
  • Rounding is display-only in every case above, so if you are reconciling to the cent, take the formulas printed on the pages and run them in a spreadsheet instead of adding up the table rows.

Tools used in this guide

Advertisement