A colleague tossed over a forecast sheet: first month 2, then multiply by 1.7 every month, 18 months in total, what does it accumulate to. Her sheet took the previous period times 1.7 row after row and then summed the column; my script used the closed form a₁ × (1 - r^n) / (1 - r). Set the two results side by side and the tails are not the same: the closed form gives 40177.38414876491, her sheet gives 40177.384148764904, the gap sits at the 15th decimal place, and both of us swore we were right.
To judge who was wrong you need an answer that does not take part in the argument. Write 1.7 as the fraction 17/10, run the arithmetic in BigInt over rationals, and the exact sum of the 18 terms is 40,177.384148764929; both doubles sit 1.70e-11 away from it. Nobody miscalculated. Treating the fact that two double tails differ as proof that somebody miscalculated was the most expensive move in the whole argument.
The difference sits on three layers
- Formula layer: on this parameter set the closed form and the term-by-term accumulation differ by 0, and both routes hand back the same double 40177.38414876491.
- Algorithm layer: the last term is computed at a single point with
a × Math.pow(r, n - 1), list item i is computed on its own witha × Math.pow(r, i), and nowhere in the code is there a running product of thet = t * rkind. The sheet's convention is exactly that running product, so the two routes are not guaranteed to share a tail from the 6th term onward. - Display layer: the result area formats in four bands, decimals keep at most 10 places, and once the absolute value passes 1e15 only 7 significant digits survive, so a fork at ulp scale never reaches the eye.
They split at the 6th term
The first approximation happens before the digits even reach the input box. In double precision the value actually stored for 1.7 is 1.69999999999999995559, which is 4.44e-17 below the decimal 1.7. Math.pow is one call into the standard library, while the recurrence multiplies term after term and rounds once per multiplication. With r=1.7 and a₁=2 the first term where the two disagree is the 6th: pow returns 28.397139999999997, the running product returns 28.397139999999993, a gap of 3.55e-15, which is one ulp of roughly 4.44e-16.
Fig. 1: the index of the first term where pow and the running product disagree, for six pairs of (first term, common ratio).
How early the fork lands is not the same thing as how fast the sequence climbs: r=1.01 splits at term 4, r=1.1 at term 5, the integer ratio r=3 holds out until term 35, and a halving ratio such as r=0.5 sweeps across 100 terms without forking once.
When the gap starts to show
An ulp-scale fork cannot climb into a report, because the formatting step smooths it flat: 0.30000000000000004 displays as 0.3 and 9.825999999999999 displays as 9.826. The breakpoint also gets remembered one magnitude late: 1e21 really does output 1.000000e+21, yet the switch happens at 1e15, where 999999999999999 still prints with grouping commas while 1e15 already prints as 1.000000e+15. Past that line only 7 significant digits remain, 123456789012345678 turns into 1.234568e+17, and the printed figure is off from the true one by -1.099e+10.
| a₁ / r / n | Closed form double | Term by term double | Difference |
|---|---|---|---|
| 2 / 1.7 / 18 | 40177.38414876491 | 40177.38414876491 | 0 |
| 2 / 1.7 / 100 | 3.168283922230802e+23 | 3.1682839222308025e+23 | 6.71e+7 |
Once the term count grows this is the picture: the two routes differ by 6.71e+7 while both sides still display the same string 3.168284e+23. Arguing about who miscalculated has stopped being a question about mathematics and become a question about formatting.
A mistake more common than floating point: the term count
Three numbers in this forecast look very much alike and are not interchangeable: the 18th term is 16,544.8052377267, the sum of the first 18 terms is 40,177.3841487649, and 28,126.1689041354, which is what you get after multiplying 18 times, is the 19th term. Writing the general term with subscript n-1 is exactly where the off-by-one comes from. Recovering the ratio takes a root instead of a search: feed 40235.4285714286 in as the last term and the answer is 1.7912313617; for the same run from 2 up to 4,374, put 8 in the term box and you get r=3, put 9 and you get 2.6150566286.
A ratio hugging 1 is a different risk: the branch test is strict equality, r === 1, so an input of 0.9999999999999999 still falls back into the closed form and cancels on (1 - r). With a₁=1 and n=1000 it hands back a tidy 1,000 while the exact value is really 0.
Where these numbers came from
None of these figures were pushed by hand. I lifted the functions out of the geometric sequence page exactly as they stand (geometric sequence calculator), and the strings the page writes for itself are the evidence: on 2 / 1.7 / 18 it gives 40,177.3841487649, and its process line prints as "Sum: S18 = 2 × (1 - 1.718) / (1 - 1.7) = 40,177.3841487649", where the superscript has been flattened into two runs of digits sitting side by side, so reading it as 1.718 is no surprise. It takes three numbers at a time and hands back the last term, the sum and the term list; it solves no equation and never reconciles against a spreadsheet.
Two places will fool you. The list renders at most 500 terms, and with n=600 it appends a line "... 600 terms in total, only the first 500 are shown ...", yet S_n is the sum of all 600 terms, so adding up the list on screen never matches. Once the term count passes 1000 the page only pops "Too many terms, please keep it under 1000" and returns; the result area is not cleared, so the previous numbers stay on the screen. Overflow still happens inside the limit: with a₁=2 and r=3 the sum turns into +∞ at term 646 while the last term waits until term 647, and that screen pairs 1.107234e+308 with +∞. For a₁=1 and r=2 the last term needs term 1025, and the sum under the 1.7 convention needs term 1336.
If I had to carry this into a talk about a growth rate, I would run the same parameter set once more under the compound interest convention (compound interest calculator), because the final amount P × (1 + i)^n naturally carries one term more than a_n. Why compound and simple interest drift so far apart once the horizon lengthens cuts in from that same misalignment.
Three questions that came up on the spot
Q: The sheet and the script, which one do I trust?
A: Look at the magnitude first. Within a few dozen terms the gap stays in the ulp band, and at n=18 the two routes differ by 0. Once the term count reaches the hundreds the gap becomes a real absolute amount. To settle it, only 40,177.384148764929, the value you get by treating the ratio as a fraction, belongs in the footnote.
Q: Why does the pasted sequence carry so many decimals?
A: The copy path uses the raw doubles from the per-term pow and joins them with ", " without touching the formatter: 2, 3.4, 5.779999999999999, 9.825999999999999, 16.704199999999997. The 9.826 on screen is the formatted one; for any arithmetic afterwards use the former.
Q: What happens if I type 1.9 or 1e3 into the term box?
A: Both read as 1. The term count goes through parseInt, the first term and the ratio through parseFloat, and the same 1e3 is 1000 in the latter. One box, one parsing rule, and the term count is where you take the quiet hit.
The order I check things in
- Compare the doubles before comparing the screens: a magnitude like 1.70e-11 is inherent error, not somebody's typo.
- Then count the subscripts: a_n uses n-1, the final amount multiplies n times, and the two sit a whole term apart.
- Check whether the magnitude crossed 1e15: past that line only 7 significant digits survive, and a gap of 1.099e+10 will not be displayed.
Nobody convinced anybody that day. It ended only once the footnote spelled out the convention (18 terms, closed form), the magnitude (40,177.3841487649) and one sentence, "the tails differ at the 15th decimal place". Whatever belongs to floating point stays with floating point; whatever belongs to the convention stays with the convention.