Skip to content

Mortgage Calculator — Ruby source

Compute monthly payments, the full amortization schedule and total interest, with extra-payment what-if analysis showing months and interest saved — runs entirely in your browser.

This is the Ruby implementation — the same logic the interactive tool runs, in a shareable, citable form.

# mortgage-calculator — amortized monthly payment.
#
# Language: Ruby (3.2, standard library only)
# Source:   CosmoDev polyglot showcase port of the Mortgage Calculator
#           tool, ported from src/lib/mortgage.ts (the canonical
#           TypeScript implementation).
# Tool:     https://dev.cosmolabs.org/tools/mortgage-calculator
# License:  display source — part of CosmoDev's polyglot tool pages.
#
# Display snippet: ports the core monthly_payment() formula from the
# TypeScript lib. The full amortization schedule (integer-cent tracking,
# extra payments, payoff dates) lives in the TypeScript/Go sources.
#
# Semantics: standard amortization formula P*r*(1+r)**n / ((1+r)**n - 1)
# with r = rate_pct/100/12 and n = years*12. A zero rate divides the
# principal evenly across n months. Returns the payment rounded to
# cents, or nil on invalid input (non-positive principal, non-positive
# or non-integer years, non-finite values).

def monthly_payment(principal, rate_pct, years)
  return nil unless [principal, rate_pct].all? { |v| v.is_a?(Numeric) && v.to_f.finite? }
  return nil unless principal.positive?
  return nil unless years.is_a?(Integer) && years.positive?

  r = rate_pct.to_f / 100 / 12
  n = years * 12

  payment = if r.zero?
              principal / n.to_f
            else
              growth = (1 + r)**n
              principal * r * growth / (growth - 1)
            end
  (payment * 100 + 0.5).floor / 100.0
end

# Example:
#   monthly_payment(300_000, 6, 30) -> 1798.65
result = monthly_payment(300_000, 6, 30)
raise "expected 1798.65, got #{result}" unless result == 1798.65

# A zero rate divides the principal evenly.
raise "expected 833.33 at a zero rate" unless monthly_payment(300_000, 0, 30) == 833.33

# Invalid inputs are rejected, never clamped.
raise "non-positive principal" unless monthly_payment(-1, 6, 30).nil?
raise "non-positive years" unless monthly_payment(300_000, 6, 0).nil?
raise "non-integer years" unless monthly_payment(300_000, 6, 30.5).nil?
raise "non-finite value" unless monthly_payment(Float::INFINITY, 6, 30).nil?

puts "ok"

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 →