Skip to content

feat: v1.5.0 — savings goal, early payoff, FIRE calculators#57

Open
JDIZM wants to merge 9 commits intomainfrom
feat/savings-fire-earlyPayoff
Open

feat: v1.5.0 — savings goal, early payoff, FIRE calculators#57
JDIZM wants to merge 9 commits intomainfrom
feat/savings-fire-earlyPayoff

Conversation

@JDIZM
Copy link
Copy Markdown
Owner

@JDIZM JDIZM commented Apr 21, 2026

Summary

Adds three new calculation families to the library and extracts a shared rate helper. All existing exports unchanged. Intended for release as v1.5.0.

New exports

  • solveContributionForGoal(options) / solveYearsToGoal(options) — inverse savings-goal solvers using the future-value-of-annuity formula.
  • earlyMortgagePayoff(options) — repayment schedule with extra monthly payments + optional lump sums; returns months and interest saved vs the baseline schedule.
  • fireNumber(options) / yearsToFire(options) — FIRE number from annual spend + safe withdrawal rate, and a years-to-FIRE solver.

Shared internal helper toDecimalRate extracted to calc/helpers.ts — the three new calcs and the two existing ones now normalise rate inputs the same way (percentage or decimal interchangeably).

Test coverage

  • 45 tests across 5 files (20 new tests, previous 25 unchanged).
  • Edge cases: zero-rate fallbacks, invalid inputs, unreachable goals, rounding residuals on amortisation schedules.
  • npm run tsc:check, npm run lint, npm test, npm run build all green.

Release

Tag this after merge as v1.5.0 and create a GitHub Release — release.yml will bump package.json, build, and npm publish automatically.

Test plan

  • CI node tests workflow passes on Node 18 + 20
  • Reviewer spot-checks the three new calc files + tests
  • After merge, create GitHub Release v1.5.0 from main — workflow publishes

JDIZM added 3 commits April 21, 2026 01:01
- solveContributionForGoal / solveYearsToGoal — FV annuity solver for
  reaching a target either by contribution or by time.
- earlyMortgagePayoff — repayment schedule with extra monthly payments
  and one-off lump sums; reports months and interest saved vs baseline.
- fireNumber / yearsToFire — savings target from annual spend and
  withdrawal rate, and years-to-FIRE solver.
- Shared toDecimalRate helper extracted to calc/helpers.ts.

45 tests, tsc clean, lint clean. Intended for v1.5.0.
New savings-goal, early-payoff, and FIRE calculators. See CHANGELOG.md.
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces version 1.5.0 of the finance library, adding new calculators for savings goals, early mortgage payoffs, and FIRE targets, along with a shared rate normalization helper. The review feedback highlights several mathematical and financial edge cases to address, including preventing potential NaN values in logarithmic formulas, minimizing cumulative rounding errors by using unrounded intermediate values, and refining input validation for withdrawal rates and interest rate heuristics.

Comment thread calc/fireNumber.ts Outdated
Comment thread calc/savingsGoal.ts Outdated
Comment thread calc/earlyPayoff.ts Outdated
Comment thread calc/earlyPayoff.ts
Comment thread calc/fireNumber.ts Outdated
Comment thread calc/helpers.ts
JDIZM added 2 commits April 21, 2026 11:39
- prettier --check was failing on calc/earlyPayoff.test.ts; ran the
  formatter.
- mortgageCalculator had two unreachable guards: 'Home value cannot
  be negative' and 'Deposit cannot be greater than home value' were
  both caught by the 'Principal cannot be negative' check before the
  specific messages could surface. Reordered the validation so
  user-facing guards fire before the derived 'principal < 0' check
  (which is now unreachable and removed).
- Added tests for every thrown error message across all five calc
  modules: mortgage (7 guards), earlyPayoff (8 guards including lump
  sum shape), fireNumber.yearsToFire (4 guards), savingsGoal
  (7 guards including the zero-denominator path), compoundInterest
  (the debtRepayment + accrualOfPaymentsPerAnnum combination guard).
- Widened vitest coverage excludes to actually skip index.ts,
  .eslintrc.*, *.config.* with proper glob patterns.

Coverage: 90.47% -> 97.16% stmts, 75.54% -> 86.08% branches,
95.39% -> 99.58% lines. 45 -> 57 tests.
Review NIT: index.ts (re-export barrel) still showed in the coverage
per-file table. Added /* istanbul ignore file */ and widened the
config globs to types/** and **/index.ts. Aggregate coverage numbers
unaffected.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds new financial calculators (savings-goal solvers, early mortgage payoff simulation, and FIRE calculators) and updates public exports/docs/tests for the v1.5.0 release.

Changes:

  • Introduces new calculation modules: savingsGoal, earlyPayoff, fireNumber, plus new exported APIs via index.ts.
  • Adds shared toDecimalRate helper and new types for the added calculators.
  • Expands tests, adjusts coverage excludes, and updates README/CHANGELOG/version for the v1.5.0 release.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
vitest.config.ts Updates coverage exclude patterns.
types/calculator.ts Adds option/result types for savings goal, early payoff, and FIRE calculators.
package.json Bumps package version to 1.5.0.
index.ts Exports the new calculator modules.
calc/savingsGoal.ts Adds savings-goal inverse solvers (contribution and years).
calc/savingsGoal.test.ts Adds unit tests for the savings-goal solvers.
calc/mortgageCalculator.ts Refines input validation ordering/messages.
calc/mortgageCalculator.test.ts Adds validation-focused tests.
calc/helpers.ts Adds toDecimalRate helper for rate normalization.
calc/fireNumber.ts Adds FIRE number + years-to-FIRE calculators.
calc/fireNumber.test.ts Adds tests for FIRE calculators.
calc/earlyPayoff.ts Adds early mortgage payoff simulation with extras/lump sums.
calc/earlyPayoff.test.ts Adds tests for early payoff simulation.
calc/compoundInterest.test.ts Adds coverage for an invalid option-combination guard.
README.md Updates package overview and documents new APIs.
CHANGELOG.md Adds v1.5.0 changelog entry.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread calc/savingsGoal.ts Outdated
Comment thread calc/earlyPayoff.ts
Comment thread calc/earlyPayoff.ts
Comment thread calc/earlyPayoff.ts
Comment thread README.md Outdated
Comment thread CHANGELOG.md Outdated
Comment thread calc/earlyPayoff.ts Outdated
Comment thread calc/savingsGoal.ts Outdated
JDIZM added 4 commits April 21, 2026 16:51
Closes or supersedes Renovate PRs:
- #56 typescript v6 (5.5 -> 6.0.3)
- #55 vite-node v6 (3 -> 6)
- #50 vitest v4 (3 -> 4.1.5) + @vitest/coverage-istanbul v4
- #46 / #51 node (20.19 -> 24) + CI matrix now [20, 22, 24]
- #49 / #53 actions/setup-node + actions/checkout v4 -> v5
- #39 eslint-config-prettier v9 -> v10
- @types/node 22 -> 24
- esbuild added as explicit devDep (was transitive, removed by vitest 4)

Surface fixes:
- tsconfig.json: drop deprecated baseUrl + paths (no code used the
  @/* alias; TS 7 will drop the option entirely).
- vitest.config.ts: switch from vite's defineConfig to vitest/config
  so the `test` key type-checks under TS 6.
- calc/compoundInterest.test.ts: the interestOnly debtRepayment
  test was silently under-asserting the year-2 interestMatrix array
  (missing the last two months). vitest 4's toMatchObject is
  stricter on nested Maps and caught it. Added the missing
  288769.25 + 290521 entries.

Left alone: eslint v10 and typescript-eslint v8 require flat-config
migration. Low reward for this small library.

57/57 tests pass, 97.44% stmts, 92.09% branches, 100% lines.
Addresses Gemini Code Assist + reviewer comments on PR #57.

Calc bugs:
- fireNumber yearsToFire: also check numerator <= 0 (was only
  checking denominator). Negative returns that outpace contributions
  would return NaN instead of throwing.
- savingsGoal solveYearsToGoal: same numerator <= 0 check added.
- savingsGoal solveYearsToGoal: the zero-rate branch returned an
  unrounded float; the non-zero branch rounded to 2dp. Rounded both
  for consistency.
- savingsGoal solveContributionForGoal: interestEarned could go
  negative when the starting balance already outgrew the target.
  Clamp to 0 and compute from growthOfStartingBalance in that case.
- fireNumber: withdrawalRate == 1.0 is now accepted (one year of
  expenses saved). Previous `>= 1` guard rejected a legitimate edge
  case.
- earlyPayoff baselineTotalInterest: use the exact PMT value for
  the internal calculation instead of the 2dp-rounded display value.
  Cumulative rounding was distorting the baseline total over a
  300-month term.
- earlyPayoff: reject non-integer lump sum months (they'd never
  match the whole-month schedule), and reject terms longer than the
  100-year simulation cap.
- earlyPayoff: guard against negative amortization (scheduled payment
  <= monthly interest) so the loan can't silently fail to pay off.
- earlyPayoff: throw if the simulation hits MAX_MONTHS with a
  non-zero balance, instead of returning a misleading "paid off"
  result. Defensive — should be unreachable given the years guard
  + PMT properties — but protects future edits.

Docs:
- README: scope the "rates accept percentage or decimal" claim to
  the three new calculators. mortgageCalculator + compoundInterest-
  PerPeriod still take percentages only.
- CHANGELOG: same scoping clarification on the toDecimalRate note.

Left unchanged:
- helpers.ts toDecimalRate 0/1 boundary heuristic (reviewer flagged
  as ambiguous). Fixing properly means a breaking API shape change;
  queued as a minor-bump follow-up.

Test deltas: +4 (total 61), coverage 96.45% → 96.8% stmts,
91.19% → 91.7% branches.
pnpm-lock.yaml was created by the sibling finance-calculator repo's
pnpm link ../compound-interest during local iteration. This library
uses npm; add the lockfile to .gitignore and untrack it.
@JDIZM
Copy link
Copy Markdown
Owner Author

JDIZM commented Apr 22, 2026

Addressed all review feedback in 6fcc18a (plus d567b31 for a stray pnpm-lock.yaml).

Calc fixes

  • fireNumber.yearsToFire + savingsGoal.solveYearsToGoal: added numerator <= 0 check so unreachable-goal inputs throw instead of returning NaN.
  • savingsGoal.solveContributionForGoal: interestEarned clamped to 0 when the starting balance already outgrew the target (previously went negative).
  • savingsGoal.solveYearsToGoal: zero-rate branch now rounds to 2dp, matching the non-zero branch.
  • fireNumber: withdrawalRate == 1.0 (100%) is now accepted (previously rejected by >= 1).
  • earlyPayoff: baselineTotalInterest uses the exact unrounded PMT (avoiding cumulative 2dp drift); years * 12 > MAX_MONTHS guard; Number.isInteger(month) validation on lump sums; up-front negative-amortization guard; post-loop balance > 0 guard.

Docs

  • README.md + CHANGELOG.md: scoped the "rates accept percentage or decimal" claim to the three new calculators. The older compoundInterestPerPeriod + mortgageCalculator still take percentages only.

Not addressed (explained inline)

  • helpers.ts toDecimalRate 0/1 boundary heuristic — queued as a minor-bump API followup since the proper fix changes the calling shape across all new calculators.

Tests: 57 → 61 passing. Coverage 96.45% → 96.8% stmts, 91.19% → 91.7% branches. All quality gates green. Ready for re-review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants