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 →