CompoundCalc
Grows a balance year by year with contributions and interest apart, after inflation, and finds the rate needed to reach a target.
Demo
Compound interest
- Balance
- $144,572.72
- Contributions
- $48,000.00
- Interest
- $86,572.72
- In today's money
- $144,572.72
| Year | Contributions | Interest | Balance | In today's money |
|---|---|---|---|---|
| 1 | $2,400.00 | $801.42 | $13,201.42 | $13,201.42 |
| 2 | $2,400.00 | $1,032.85 | $16,634.27 | $16,634.27 |
| 3 | $2,400.00 | $1,281.01 | $20,315.28 | $20,315.28 |
| 4 | $2,400.00 | $1,547.11 | $24,262.39 | $24,262.39 |
| 5 | $2,400.00 | $1,832.44 | $28,494.83 | $28,494.83 |
| 6 | $2,400.00 | $2,138.41 | $33,033.24 | $33,033.24 |
| 7 | $2,400.00 | $2,466.50 | $37,899.74 | $37,899.74 |
| 8 | $2,400.00 | $2,818.29 | $43,118.03 | $43,118.03 |
| 9 | $2,400.00 | $3,195.52 | $48,713.55 | $48,713.55 |
| 10 | $2,400.00 | $3,600.03 | $54,713.58 | $54,713.58 |
| 11 | $2,400.00 | $4,033.76 | $61,147.34 | $61,147.34 |
| 12 | $2,400.00 | $4,498.86 | $68,046.20 | $68,046.20 |
| 13 | $2,400.00 | $4,997.59 | $75,443.79 | $75,443.79 |
| 14 | $2,400.00 | $5,532.35 | $83,376.14 | $83,376.14 |
| 15 | $2,400.00 | $6,105.79 | $91,881.93 | $91,881.93 |
| 16 | $2,400.00 | $6,720.67 | $101,002.60 | $101,002.60 |
| 17 | $2,400.00 | $7,380.00 | $110,782.60 | $110,782.60 |
| 18 | $2,400.00 | $8,087.00 | $121,269.60 | $121,269.60 |
| 19 | $2,400.00 | $8,845.10 | $132,514.70 | $132,514.70 |
| 20 | $2,400.00 | $9,658.02 | $144,572.72 | $144,572.72 |
Yearly deposits, daily interest
- Balance
- €104,638.64
- Contributions
- €30,000.00
- Interest
- €69,638.64
- In today's money
- €49,885.71
Reaching €500,000.00 takes a rate of 12.02% a year.
| Year | Contributions | Interest | Balance | In today's money |
|---|---|---|---|---|
| 1 | €1,000.00 | €339.22 | €6,339.22 | €6,184.60 |
| 2 | €1,000.00 | €414.93 | €7,754.15 | €7,380.51 |
| 3 | €1,000.00 | €494.93 | €9,249.08 | €8,588.69 |
| 4 | €1,000.00 | €579.44 | €10,828.52 | €9,810.10 |
| 5 | €1,000.00 | €668.74 | €12,497.26 | €11,045.76 |
| 6 | €1,000.00 | €763.08 | €14,260.34 | €12,296.65 |
| 7 | €1,000.00 | €862.77 | €16,123.11 | €13,563.81 |
| 8 | €1,000.00 | €968.07 | €18,091.18 | €14,848.28 |
| 9 | €1,000.00 | €1,079.35 | €20,170.53 | €16,151.11 |
| 10 | €1,000.00 | €1,196.90 | €22,367.43 | €17,473.40 |
| 11 | €1,000.00 | €1,321.10 | €24,688.53 | €18,816.24 |
| 12 | €1,000.00 | €1,452.34 | €27,140.87 | €20,180.75 |
| 13 | €1,000.00 | €1,590.98 | €29,731.85 | €21,568.09 |
| 14 | €1,000.00 | €1,737.46 | €32,469.31 | €22,979.41 |
| 15 | €1,000.00 | €1,892.23 | €35,361.54 | €24,415.92 |
| 16 | €1,000.00 | €2,055.74 | €38,417.28 | €25,878.84 |
| 17 | €1,000.00 | €2,228.51 | €41,645.79 | €27,369.41 |
| 18 | €1,000.00 | €2,411.03 | €45,056.82 | €28,888.90 |
| 19 | €1,000.00 | €2,603.88 | €48,660.70 | €30,438.62 |
| 20 | €1,000.00 | €2,807.63 | €52,468.33 | €32,019.90 |
| 21 | €1,000.00 | €3,022.90 | €56,491.23 | €33,634.10 |
| 22 | €1,000.00 | €3,250.33 | €60,741.56 | €35,282.63 |
| 23 | €1,000.00 | €3,490.64 | €65,232.20 | €36,966.91 |
| 24 | €1,000.00 | €3,744.52 | €69,976.72 | €38,688.40 |
| 25 | €1,000.00 | €4,012.76 | €74,989.48 | €40,448.62 |
| 26 | €1,000.00 | €4,296.15 | €80,285.63 | €42,249.09 |
| 27 | €1,000.00 | €4,595.59 | €85,881.22 | €44,091.39 |
| 28 | €1,000.00 | €4,911.94 | €91,793.16 | €45,977.15 |
| 29 | €1,000.00 | €5,246.17 | €98,039.33 | €47,908.02 |
| 30 | €1,000.00 | €5,599.31 | €104,638.64 | €49,885.71 |
Change something to see on_change.
Get it
It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/compound_calc.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/compound_calc.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/compound_calc.qwe
component CompoundCalcDemoLive() { state note = "Change something to see on_change." <div.demo-stack> <CompoundCalc id="compound-calc-demo-live" name="pension" title="Yearly deposits, daily interest" start="5000" contribution="1000" contribute_per_year={1} timing="start" rate={5.5} compound_per_year={365} years={30} inflation={2.5} target="500000" currency="EUR" on_change={(r) => { note = `on_change: balance ${r.balance}, interest ${r.interest}, needed ${r.needed_rate}.` }}/> <p.demo-note>{note}</p> </div>}view CompoundCalcDemo() = <div.demo-stack> <CompoundCalc id="compound-calc-demo" start="10000" contribution="200" rate={7} years={20}/> <CompoundCalcDemoLive/> </div>Source
ui/compound_calc.qwe
// CompoundCalc: what savings grow to, year by year. A starting amount and regular// contributions, made at the start or the end of each period, at a yearly rate compounded// as often as the account says, with contributions and interest kept apart so that the// rows always add up to the balance to the cent, and each balance in today's money after// inflation. How often contributions are made and how often interest compounds can// differ: each contribution period grows by the equivalent rate, (1 + r/m)^(m/p), an// exact power and roots by Newton's method, all in decimals. With a target it says// the lowest yearly rate, to 0.01%, that reaches it, or that no rate up to 1000% does.//// <CompoundCalc start="10000" contribution="200" rate={7} years={20}/>// <CompoundCalc start="5000" contribution="1000" contribute_per_year={1} timing="start" rate={5.5} compound_per_year={365}// years={30} inflation={2.5} target="500000" currency="EUR" on_change={(r) => { total = r.balance }}/>//// start, contribution, rate, years, inflation, target: where the fields start (numbers,// decimals or numeric text; empty counts as 0, target empty as none). rate and inflation are// yearly percents, up to 1000 and 100; years is a whole number from 1 to 100 (10).// contribute_per_year: 1, 2, 4 or 12 (12). compound_per_year: 1, 2, 4, 12 or 365 (12). timing:// "end" of the period (the default) or "start". currency: ISO code ("USD"; one Quillo does not// know falls back to it). locale: how to write amounts (the program's). title: the heading// ("Compound interest"). id: a prefix for its ids ("compound-calc"), unique on the page. name: a// prefix for the names of its fields ("compound": compound_start, compound_contribution,// compound_contribute_per_year, compound_timing, compound_rate, compound_compound_per_year,// compound_years, compound_inflation, compound_target). on_change(r): inside one of your// components, after each change, given { balance, contributed, interest, real, needed_rate }// as decimals when the figures are good (needed_rate null without an answer).// class: more classes for its outer element. A balance past 10 trillion is too large// to keep exact, so the table stops there and says so.//// compound_calc_schedule(start, contribution, per_year, at_start, rate, compound_per_year,// years, inflation) and compound_calc_needed(...) are the logic, for a program's own use.import ui/theme.qwe use ui_clslet compound_calc_css = css`.ui-compound-calc { container-type: inline-size; display: grid; gap: 16px; min-width: 0; max-width: 720px; padding: 20px 22px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); color: var(--ui-fg); font: 14px/1.45 var(--ui-font); }.ui-compound-calc .ui-compound-calc-title { margin: 0; font-size: 16px; font-weight: 650; }.ui-compound-calc-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px 14px; }.ui-compound-calc-field { display: grid; gap: 5px; min-width: 0; align-content: start; }.ui-compound-calc-field label { font-size: 13px; font-weight: 500; }.ui-compound-calc-field input, .ui-compound-calc-field select { box-sizing: border-box; width: 100%; min-width: 0; height: 36px; padding: 0 10px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: 14px var(--ui-font); font-variant-numeric: tabular-nums; }.ui-compound-calc-field input:hover, .ui-compound-calc-field select:hover { border-color: var(--ui-strong); }.ui-compound-calc-field input:focus, .ui-compound-calc-field select:focus { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-compound-calc-field input[aria-invalid] { border-color: var(--ui-danger); }.ui-compound-calc-timing { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 16px; margin: 0; padding: 0; border: 0; }.ui-compound-calc-timing legend { float: left; margin-right: 8px; padding: 0; font-size: 13px; font-weight: 500; }.ui-compound-calc-timing label { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; }.ui-compound-calc-timing input { margin: 0; accent-color: var(--ui-accent); }.ui-compound-calc-errors { margin: 0; padding: 0; list-style: none; color: var(--ui-danger); font-size: 13px; }.ui-compound-calc-out { display: grid; gap: 12px; padding: 12px 16px; border-radius: var(--ui-radius-sm); background: var(--ui-subtle); }.ui-compound-calc-sums { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 8px 16px; margin: 0; }.ui-compound-calc-sums dt { color: var(--ui-muted); font-size: 12px; }.ui-compound-calc-sums dd { margin: 0; font-size: 16px; font-weight: 650; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }.ui-compound-calc-needed, .ui-compound-calc-note { margin: 0; font-size: 13px; }.ui-compound-calc-note { color: var(--ui-warning); }.ui-compound-calc-scroll { max-height: 320px; overflow: auto; }.ui-compound-calc-scroll:focus-visible { outline: 2px solid var(--ui-accent); outline-offset: 2px; }.ui-compound-calc-table { width: 100%; border-collapse: collapse; font-size: 13px; font-variant-numeric: tabular-nums; }.ui-compound-calc-table th { position: sticky; top: 0; padding: 4px 0; border-bottom: 1px solid var(--ui-border); background: var(--ui-subtle); color: var(--ui-muted); font-size: 12px; font-weight: 600; text-align: right; }.ui-compound-calc-table td { padding: 4px 0; border-bottom: 1px solid var(--ui-border); text-align: right; }.ui-compound-calc-table th + th, .ui-compound-calc-table td + td { padding-left: 10px; }.ui-compound-calc-table th:first-child, .ui-compound-calc-table td:first-child { text-align: left; }.ui-compound-calc-table td:nth-child(4) { font-weight: 600; }`// The currency's code, USD when Quillo does not know it.fn compound_calc_code(currency) -> str { let c = upper(trim(str(currency ?? ""))) type_of(money.minor_units(c)) == "error" ? "USD" : c}// What was typed as a decimal: `blank` when empty, null when it is not a plain number from// 0 to most. A comma counts only as a thousands comma, so "12,50" is refused, not 1250.fn compound_calc_read(text, most, blank) { let t = trim(str(text ?? "")) if t == "" { return blank } if len(t) > 40 { return null } let plain = re.test(t, "^[0-9]{1,3}(,[0-9]{3})+(\\.[0-9]*)?$") ? replace(t, ",", "") : t if !re.test(plain, "^([0-9]+\\.?[0-9]*|\\.[0-9]+)$") { return null } let d = dec(plain) is_err(d) || d > most ? null : d}// x to the power n, a whole n, by squaring. The rates allowed keep this well inside a decimal.fn compound_calc_pow(x: dec, n: int) -> dec { let out = 1m let base = x let left = n while left > 0 { if left % 2 == 1 { out = out * base } left = int(floor(left / 2)) if left > 0 { base = base * base } } out}fn compound_calc_gcd(a: int, b: int) -> int = b == 0 ? a : compound_calc_gcd(b, a % b)// The k-th root of a (a >= 1), for a k made of twos and threes, as periods a year are:// decimals have no fractional power, and Newton's method on a high power from a large// start runs out of digits, so a square or cube root at a time.fn compound_calc_root(a: dec, k: int) -> dec { if k <= 1 { return a } let n = k % 2 == 0 ? 2 : 3 let x = 1m + (a - 1m) / n let i = 0 let moving = true while moving && i < 200 { let below = n == 2 ? x : x * x let step = (below * x - a) / (below * n) x = x - step moving = abs(step) > 0.00000000000000000000000001m i += 1 } compound_calc_root(x, int(k / n))}// What one contribution period multiplies a balance by: (1 + r/m)^(m/p) for a yearly// percent r compounded m times a year, with p contribution periods a year.fn compound_calc_growth(rate: dec, compound_per_year: int, per_year: int) -> dec { let a = 1m + rate / 100 / compound_per_year let g = compound_calc_gcd(compound_per_year, per_year) compound_calc_root(compound_calc_pow(a, int(compound_per_year / g)), int(per_year / g))}// The year-by-year schedule; amounts are decimals in, whole minor units of `code` out.// Gives { rows: [{ year, contributed, interest, balance, real }], stopped, balance, real,// contributed, interest, opening }; rows always add up: opening + contributed + interest = balance.fn compound_calc_schedule(start: dec, contribution: dec, per_year: int, at_start: bool, rate: dec, compound_per_year: int, years: int, inflation: dec, code: str) -> map { let minor = (x: dec) => money.to_minor(money.of(x, code, "half_up")) let g = compound_calc_growth(rate, compound_per_year, per_year) let opening = minor(start) let paid = minor(contribution) * per_year let balance = start let shown = opening let deflator = 1m let rows = [] let stopped = null let y = 1 while y <= years && stopped == null { let k = 0 while k < per_year { balance = at_start ? (balance + contribution) * g : balance * g + contribution k += 1 } if balance > 10000000000000m { stopped = y } else { deflator = deflator > 1000000000000000000000000m ? deflator : deflator * (1m + inflation / 100) let now_shown = minor(balance) rows = rows | push({ year: y, contributed: paid, interest: now_shown - shown - paid, balance: now_shown, real: minor(balance / deflator) }) shown = now_shown } y += 1 } let end = last(rows) { rows, stopped, opening, balance: shown, real: end == null ? opening : end.real, contributed: sum(rows | map((r) => r.contributed)), interest: sum(rows | map((r) => r.interest)) }}// True when a rate reaches the target by the end, stopping as soon as it does.fn compound_calc_reaches(rate: dec, start: dec, contribution: dec, per_year: int, at_start: bool, compound_per_year: int, years: int, target: dec) -> bool { let g = compound_calc_growth(rate, compound_per_year, per_year) let balance = start let k = 0 let periods = per_year * years let cap = target * 2 + 1m while k < periods && balance < cap { balance = at_start ? (balance + contribution) * g : balance * g + contribution k += 1 } balance >= target}// The lowest yearly percent, in steps of 0.01, that reaches target, as { rate } ; { rate: 0 }// when saving alone does; { rate: null } when not even 1000% does. Halves the range, about 17 runs.fn compound_calc_needed(start: dec, contribution: dec, per_year: int, at_start: bool, compound_per_year: int, years: int, target: dec) -> map { let hits = (h: int) => compound_calc_reaches(dec(h) / 100, start, contribution, per_year, at_start, compound_per_year, years, target) if hits(0) { return { rate: 0m } } if !hits(100000) { return { rate: null } } let low = 0 let high = 100000 while high - low > 1 { let mid = int(floor((low + high) / 2)) if hits(mid) { high = mid } else { low = mid } } { rate: dec(high) / 100 }}// All the figures from the fields as typed: { errors, schedule, needed, target }; schedule is// null with errors.fn compound_calc_work(start_text, contribution_text, per_year: int, at_start: bool, rate_text, compound_per_year: int, years_text, inflation_text, target_text, code: str) -> map { let errors = [] let most = 1000000000000m let start = compound_calc_read(start_text, most, 0m) let contribution = compound_calc_read(contribution_text, 1000000000m, 0m) let rate = compound_calc_read(rate_text, 1000m, 0m) let years = compound_calc_read(years_text, 100m, null) let inflation = compound_calc_read(inflation_text, 100m, 0m) let target = compound_calc_read(target_text, 10000000000000m, null) let whole_years = years != null && years == floor(years) && years >= 1 if start == null { errors = errors | push({ field: "start", text: "The starting amount must be a number of 0 or more, up to 1,000,000,000,000." }) } if contribution == null { errors = errors | push({ field: "contribution", text: "The contribution must be a number of 0 or more, up to 1,000,000,000." }) } if rate == null { errors = errors | push({ field: "rate", text: "The rate must be a percent from 0 to 1000." }) } if !whole_years { errors = errors | push({ field: "years", text: "The years must be a whole number from 1 to 100." }) } if inflation == null { errors = errors | push({ field: "inflation", text: "The inflation must be a percent from 0 to 100." }) } if target == null && trim(str(target_text ?? "")) != "" { errors = errors | push({ field: "target", text: "The target must be a number of 0 or more, up to 10,000,000,000,000." }) } if len(errors) > 0 { return { errors, schedule: null, needed: null, target: null } } let schedule = compound_calc_schedule(start, contribution, per_year, at_start, rate, compound_per_year, int(years), inflation, code) let needed = target != null && target > 0m ? compound_calc_needed(start, contribution, per_year, at_start, compound_per_year, int(years), target) : null { errors, schedule, needed, target: target != null && target > 0m ? money.to_minor(money.of(target, code, "half_up")) : null }}component CompoundCalc(start = null, contribution = null, contribute_per_year: int = 12, timing: str = "end", rate = null, compound_per_year: int = 12, years = 10, inflation = null, target = null, currency: str = "USD", locale: str? = null, title: str = "Compound interest", id: str = "compound-calc", name: str = "compound", on_change = null, class: str? = null) { style global compound_calc_css state start_text = start == null ? "" : str(start) state contribution_text = contribution == null ? "" : str(contribution) state rate_text = rate == null ? "" : str(rate) state years_text = years == null ? "" : str(years) state inflation_text = inflation == null ? "" : str(inflation) state target_text = target == null ? "" : str(target) state per_year = contains([1, 2, 4, 12], contribute_per_year) ? contribute_per_year : 12 state compounding = contains([1, 2, 4, 12, 365], compound_per_year) ? compound_per_year : 12 state at_start = timing == "start" let code = compound_calc_code(currency) let work = compound_calc_work(start_text, contribution_text, per_year, at_start, rate_text, compounding, years_text, inflation_text, target_text, code) let ok = work.schedule != null let shown = (minor: int) => locale != null ? money.format(money.from_minor(minor, code), locale) : money.format(money.from_minor(minor, code)) let bad = (field: str) => work.errors | any((e) => e.field == field) let field_id = (part: str) => `${id}-${part}` let named = (part: str) => `${name}_${part}` let tell = () => { if on_change != null && ok { let s = work.schedule let amount = (minor: int) => money.from_minor(minor, code).amount on_change({ balance: amount(s.balance), contributed: amount(s.contributed), interest: amount(s.interest), real: amount(s.real), needed_rate: work.needed == null ? null : work.needed.rate }) } } <section class={ui_cls(["ui-compound-calc", class])} aria-labelledby={field_id("title")}> <h3.ui-compound-calc-title id={field_id("title")}>{title}</h3> <div.ui-compound-calc-grid> <div.ui-compound-calc-field> <label for={field_id("start")}>Starting amount ({code})</label> <input type="text" inputmode="decimal" autocomplete="off" id={field_id("start")} name={named("start")} value={start_text} placeholder="0.00" aria-invalid={bad("start") ? "true" : null} @input={(e) => { start_text = e.value; tell() }}/> </div> <div.ui-compound-calc-field> <label for={field_id("contribution")}>Each contribution ({code})</label> <input type="text" inputmode="decimal" autocomplete="off" id={field_id("contribution")} name={named("contribution")} value={contribution_text} placeholder="0.00" aria-invalid={bad("contribution") ? "true" : null} @input={(e) => { contribution_text = e.value; tell() }}/> </div> <div.ui-compound-calc-field> <label for={field_id("per-year")}>Contributions</label> <select id={field_id("per-year")} name={named("contribute_per_year")} @change={(e) => { per_year = int(e.value); tell() }}> <option value="12" selected={per_year == 12}>Monthly</option> <option value="4" selected={per_year == 4}>Quarterly</option> <option value="2" selected={per_year == 2}>Twice a year</option> <option value="1" selected={per_year == 1}>Yearly</option> </select> </div> <div.ui-compound-calc-field> <label for={field_id("rate")}>Rate a year (%)</label> <input type="text" inputmode="decimal" autocomplete="off" id={field_id("rate")} name={named("rate")} value={rate_text} placeholder="0" aria-invalid={bad("rate") ? "true" : null} @input={(e) => { rate_text = e.value; tell() }}/> </div> <div.ui-compound-calc-field> <label for={field_id("compounding")}>Compounding</label> <select id={field_id("compounding")} name={named("compound_per_year")} @change={(e) => { compounding = int(e.value); tell() }}> <option value="365" selected={compounding == 365}>Daily</option> <option value="12" selected={compounding == 12}>Monthly</option> <option value="4" selected={compounding == 4}>Quarterly</option> <option value="2" selected={compounding == 2}>Twice a year</option> <option value="1" selected={compounding == 1}>Yearly</option> </select> </div> <div.ui-compound-calc-field> <label for={field_id("years")}>Years</label> <input type="text" inputmode="numeric" autocomplete="off" id={field_id("years")} name={named("years")} value={years_text} aria-invalid={bad("years") ? "true" : null} @input={(e) => { years_text = e.value; tell() }}/> </div> <div.ui-compound-calc-field> <label for={field_id("inflation")}>Inflation a year (%)</label> <input type="text" inputmode="decimal" autocomplete="off" id={field_id("inflation")} name={named("inflation")} value={inflation_text} placeholder="0" aria-invalid={bad("inflation") ? "true" : null} @input={(e) => { inflation_text = e.value; tell() }}/> </div> <div.ui-compound-calc-field> <label for={field_id("target")}>Target ({code}, optional)</label> <input type="text" inputmode="decimal" autocomplete="off" id={field_id("target")} name={named("target")} value={target_text} aria-invalid={bad("target") ? "true" : null} @input={(e) => { target_text = e.value; tell() }}/> </div> </div> <fieldset.ui-compound-calc-timing> <legend>Contribute at the</legend> <label><input type="radio" name={named("timing")} value="start" checked={at_start} @change={() => { at_start = true; tell() }}/> Start of each period</label> <label><input type="radio" name={named("timing")} value="end" checked={!at_start} @change={() => { at_start = false; tell() }}/> End of each period</label> </fieldset> {len(work.errors) > 0 && <ul.ui-compound-calc-errors role="alert">{for e in work.errors => <li key={e.field}>{e.text}</li>}</ul>} {ok && <div.ui-compound-calc-out aria-live="polite"> <dl.ui-compound-calc-sums> <div><dt>Balance</dt><dd data-key="balance">{shown(work.schedule.balance)}</dd></div> <div><dt>Contributions</dt><dd data-key="contributed">{shown(work.schedule.contributed)}</dd></div> <div><dt>Interest</dt><dd data-key="interest">{shown(work.schedule.interest)}</dd></div> <div><dt>In today's money</dt><dd data-key="real">{shown(work.schedule.real)}</dd></div> </dl> {work.schedule.stopped != null && <p.ui-compound-calc-note>The balance passes 10 trillion in year {work.schedule.stopped}, too large to keep exact, so the table stops before it.</p>} {work.needed != null && <p.ui-compound-calc-needed data-key="needed"> {work.needed.rate == null ? `No rate up to 1000% reaches ${shown(work.target)}.` : (work.needed.rate == 0m ? `The contributions alone reach ${shown(work.target)}, with no interest.` : `Reaching ${shown(work.target)} takes a rate of ${work.needed.rate}% a year.`)} </p>} <div.ui-compound-calc-scroll tabindex="0" role="region" aria-label="Balance by year"> <table.ui-compound-calc-table> <thead><tr><th scope="col">Year</th><th scope="col">Contributions</th><th scope="col">Interest</th><th scope="col">Balance</th><th scope="col">In today's money</th></tr></thead> <tbody> {for r in work.schedule.rows => <tr key={r.year}><td>{r.year}</td><td>{shown(r.contributed)}</td><td>{shown(r.interest)}</td><td>{shown(r.balance)}</td><td>{shown(r.real)}</td></tr>} </tbody> </table> </div> </div>} </section>}Related
BreakEvenCalcFinds break-even units and revenue from costs and price, the margin of safety at a volume, a chart, and the price a target profit needs.4 views · 6 downloadsBillSplitterSplits a bill with tax and tip equally, by shares or by items, cents shared out by largest remainder so the parts add up exactly.3 views · 6 downloadsBudgetPlannerPlans a month in envelopes with the 50/30/20 check, what is left to assign, overspending warnings and exact moves between envelopes.3 views · 5 downloadsCashCountCounts a drawer by notes and coins to a total and an over or short variance, and makes change in the fewest pieces from what is in it.2 views · 8 downloadsCurrencyConverterAn amount and two currencies with a swap button, the rate both ways and the converted amount, crossed through the base from the rates given.8 views · 11 downloadsDebtPayoffCompares avalanche and snowball on several debts with an extra payment: months, interest, payoff order, each month, and warns of a debt that never clears.3 views · 6 downloads
Thread
Thread 0 posts
On CompoundCalc: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.