Skip to content

Compound Interest Calculator — Ruby 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 Ruby implementation — the same logic the interactive tool runs, in a shareable, citable form.

# compound-interest-calculator — compound growth projection.
#
# Language: Ruby (3.2, standard library only)
# Source:   CosmoDev polyglot showcase port of the Compound Interest
#           Calculator tool, ported from src/lib/interest.ts (the canonical
#           TypeScript implementation).
# Tool:     https://dev.cosmolabs.org/tools/compound-interest-calculator
# License:  display source — part of CosmoDev's polyglot tool pages.
#
# Display snippet: ports the core project() math from the TypeScript lib.
# The full projection (contributions, inflation, per-year rows) lives in
# the TypeScript/Go sources.
#
# Semantics: monthly simulation with the monthly rate set to 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.
# Returns the final balance rounded to 2 decimals, or nil on invalid input
# (negative principal, non-integer years, unsupported frequency,
# non-finite values).

module CompoundInterestCalculator
  VALID_COMPOUNDS = [1, 2, 4, 12, 365].freeze

  module_function

  # Round to 2 decimals, half-up (mirrors the TS Math.round).
  def round2(n)
    (n * 100 + 0.5).floor / 100.0
  end

  def project(principal, rate_pct, years, compounds_per_year)
    return nil unless [principal, rate_pct].all?(&:finite?)
    return nil if principal.negative? || years.negative? || !years.is_a?(Integer)
    return nil unless VALID_COMPOUNDS.include?(compounds_per_year)

    n = compounds_per_year.to_f
    monthly_rate = (1 + rate_pct / 100.0 / n)**(n / 12.0) - 1

    balance = principal.to_f
    (years * 12).times { balance *= 1 + monthly_rate }
    round2(balance)
  end
end

if $PROGRAM_NAME == __FILE__
  # Example:
  #   project(10000, 5, 10, 12) -> 16470.09
  result = CompoundInterestCalculator.project(10_000, 5, 10, 12)
  raise "expected 16470.09, got #{result}" unless result == 16_470.09

  # Invalid inputs are rejected, never clamped.
  raise 'negative principal' unless CompoundInterestCalculator.project(-1, 5, 10, 12).nil?
  raise 'negative years' unless CompoundInterestCalculator.project(10_000, 5, -1, 12).nil?
  raise 'unsupported frequency' unless CompoundInterestCalculator.project(10_000, 5, 10, 6).nil?
  raise 'non-finite value' unless CompoundInterestCalculator.project(Float::INFINITY, 5, 10, 12).nil?

  puts 'ok'
end

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 →