Skip to content

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 →