Compound Interest Calculator — Go source
Project investment growth with any compounding frequency, scheduled contributions and inflation adjustment. Yearly breakdown, growth curve and CSV export — runs entirely in your browser.
This is the Go implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Package compoundinterest is the Go twin of CosmoDev's src/lib/interest.ts
// (dual source: the web lib is TypeScript, the CLI lib is Go — kept in
// lock-step). Pure + deterministic, never panics. Invalid input returns nil
// instead of throwing, mirroring the TS lib's null. The table-driven tests
// in compound-interest-calculator_test.go share vectors with
// src/lib/interest.test.ts so the two implementations are held to the same
// contract.
package compoundinterest
import "math"
// Contribution is a recurring deposit: Amount per period, made PerYear times
// a year. It mirrors the TS `contribution?: { amount; perYear }` — a nil
// pointer is the TS `undefined`.
type Contribution struct {
Amount float64
PerYear int
}
// Input mirrors the TS InterestInput. Years and CompoundsPerYear are ints in
// Go, which makes the TS "integer years" validation structural. InflationPct
// is a pointer so nil = TS `undefined`.
type Input struct {
Principal float64
AnnualRatePct float64
Years int
CompoundsPerYear int
Contribution *Contribution
InflationPct *float64
}
// Row is one calendar year of the projection (TS InterestRow).
type Row struct {
Year int
StartBalance float64
Contributed float64
Interest float64
EndBalance float64
}
// Summary holds the aggregate totals (TS InterestResult.summary).
// InflationAdjustedFinal is nil when no inflation was supplied (TS null).
type Summary struct {
FinalBalance float64
TotalContributed float64
TotalInterest float64
InflationAdjustedFinal *float64
}
// Result mirrors the TS InterestResult.
type Result struct {
Rows []Row
Summary Summary
}
// epsilon is 2^-52, identical to JavaScript's Number.EPSILON. Adding it
// before rounding absorbs binary floating-point noise, mirroring the TS
// lib's `n + Number.EPSILON` (the percentage-calculator twin's pattern).
var epsilon = math.Ldexp(1, -52)
// round2 rounds n to 2 decimals, round-half-away-from-zero after absorbing
// binary float noise via epsilon. It is the Go twin of round2() in
// src/lib/interest.ts.
func round2(n float64) float64 {
return math.Round((n+epsilon)*100) / 100
}
// validCompounds reports whether n is one of the supported compounding
// frequencies: 1 (annually), 2, 4 (quarterly), 12 (monthly), 365 (daily).
func validCompounds(n int) bool {
switch n {
case 1, 2, 4, 12, 365:
return true
}
return false
}
// finite reports whether every value is neither NaN nor an infinity — the
// Go equivalent of the TS Number.isFinite guards.
func finite(values ...float64) bool {
for _, v := range values {
if math.IsNaN(v) || math.IsInf(v, 0) {
return false
}
}
return true
}
// Project grows the principal (plus optional recurring contributions) with a
// monthly simulation, returning one row per calendar year plus a summary.
// It returns nil for invalid input — the twin of the TS `project()` null.
//
// The monthly rate is the exact equivalent of the stated compounding
// frequency — (1 + r/n)^(n/12) − 1 — so the effective annual rate is
// preserved for every supported frequency and contributions apply naturally
// per month (amount × perYear / 12).
func Project(in Input) *Result {
if !finite(in.Principal, in.AnnualRatePct) {
return nil
}
if in.Principal < 0 || in.Years < 0 {
return nil
}
if !validCompounds(in.CompoundsPerYear) {
return nil
}
if in.InflationPct != nil && !finite(*in.InflationPct) {
return nil
}
if in.Contribution != nil {
if !finite(in.Contribution.Amount) {
return nil
}
if in.Contribution.Amount < 0 || in.Contribution.PerYear <= 0 {
return nil
}
}
monthlyRate := math.Pow(1+in.AnnualRatePct/100/float64(in.CompoundsPerYear),
float64(in.CompoundsPerYear)/12) - 1
monthlyContribution := 0.0
if in.Contribution != nil {
monthlyContribution = in.Contribution.Amount * float64(in.Contribution.PerYear) / 12
}
months := in.Years * 12
balance := in.Principal
rows := make([]Row, 0, in.Years)
for year := 1; year <= in.Years; year++ {
start := balance
for m := 0; m < 12; m++ {
balance = balance*(1+monthlyRate) + monthlyContribution
}
contributed := monthlyContribution * 12
rows = append(rows, Row{
Year: year,
StartBalance: round2(start),
Contributed: round2(contributed),
Interest: round2(balance - start - contributed),
EndBalance: round2(balance),
})
}
totalContributed := monthlyContribution * float64(months)
finalBalance := balance
var inflationAdjusted *float64
if in.InflationPct != nil && finite(*in.InflationPct) {
adj := round2(finalBalance / math.Pow(1+*in.InflationPct/100, float64(in.Years)))
inflationAdjusted = &adj
}
if in.Years == 0 {
// A flat projection still gets its single row so tables never render empty.
rows = append(rows, Row{
Year: 0,
StartBalance: round2(in.Principal),
Contributed: 0,
Interest: 0,
EndBalance: round2(in.Principal),
})
}
return &Result{
Rows: rows,
Summary: Summary{
FinalBalance: round2(finalBalance),
TotalContributed: round2(totalContributed),
TotalInterest: round2(finalBalance - in.Principal - totalContributed),
InflationAdjustedFinal: inflationAdjusted,
},
}
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →