Amortizy

Gem Version License: MIT

A Ruby gem for generating loan amortization schedules. Supports monthly, bi-weekly, weekly, and daily payment frequencies with grace periods, interest-only payments, federal bank holidays, and multiple interest calculation methods.

Perfect for financial applications, lending platforms, and loan calculators.

Features

  • Multiple payment frequencies: Monthly, bi-weekly, weekly, or daily payments
  • Flexible loan terms: Any term length in months, or specify an exact payment count
  • Dual input model: Provide term_months (gem calculates payment count) or num_payments (you specify exactly how many)
  • Interest calculation methods: Simple (accrued daily) or precomputed (fixed per payment)
  • Grace periods: Grace periods before first payment (interest accrues and capitalizes)
  • Interest-only periods: Configure initial interest-only payment phases
  • Fee handling: Origination fees and additional fees with three treatment options
  • Bank day calculations: Automatically skip weekends and US Federal Reserve holidays
  • Multiple output formats: Console display or CSV export
  • Public API: Programmatic access to schedule data, summaries, and totals
  • Testing: 70 RSpec tests

Installation

Add this line to your application's Gemfile:

gem 'amortizy'

And then execute:

bundle install

Or install it yourself as:

gem install amortizy

Quick Start

Monthly Loan (most common)

require 'amortizy'

schedule = Amortizy::AmortizationSchedule.new(
  start_date: "2026-01-15",
  principal: 100_000.00,
  term_months: 12,
  annual_rate: 10.0,
  frequency: :monthly
)

# Display in console
schedule.generate

# Access data programmatically
schedule.summary          # => { start_date:, end_date:, principal:, total_payments:, ... }
schedule.payment_amount   # => 8791.59
schedule.total_interest   # => 5499.06
schedule.total_paid       # => 105499.06
schedule.end_date         # => #<Date: 2027-01-15>

# Get the full schedule as an array of hashes
schedule.schedule.each do |payment|
  puts "#{payment[:date]} - $#{'%.2f' % payment[:total_payment]}"
end

# Generate CSV file
schedule.generate(output: :csv, csv_path: "schedule.csv")

Specify Exact Payment Count

Instead of a term length, tell the gem exactly how many payments you want:

schedule = Amortizy::AmortizationSchedule.new(
  start_date: "2026-01-15",
  principal: 50_000.00,
  num_payments: 26,
  annual_rate: 12.0,
  frequency: :biweekly
)

schedule.schedule.length    # => 26
schedule.end_date           # => the date of the 26th payment
schedule.term_months        # => nil (not applicable when using num_payments)

Advanced Example with All Features

schedule = Amortizy::AmortizationSchedule.new(
  start_date: "2026-01-15",
  principal: 100_000.00,
  term_months: 12,
  annual_rate: 17.75,
  frequency: :monthly,
  origination_fee: 10_000.00,
  additional_fee: 2_500.00,
  additional_fee_label: "Processing Fee",
  additional_fee_treatment: :distributed,
  bank_days_only: true,
  interest_only_periods: 3,
  grace_period_days: 3,
  interest_method: :simple
)

schedule.generate

API Reference

Initialization Parameters

Parameter Type Required Default Options Description
start_date String/Date Yes - YYYY-MM-DD Loan start date
principal Float Yes - > 0 Loan principal amount
term_months Integer One of* - Any positive integer Loan term in months
num_payments Integer One of* - Any positive integer Exact number of payments
annual_rate Float Yes - - Annual interest rate (%)
frequency Symbol Yes - :monthly, :biweekly, :weekly, :daily Payment frequency
origination_fee Float No 0 - Fee added to principal
additional_fee Float No 0 - Additional processing fee
additional_fee_label String No "Additional Fee" - Label for additional fee
additional_fee_treatment Symbol No :distributed :distributed, :add_to_principal, :separate_payment How to handle additional fee
bank_days_only Boolean No false - Skip weekends and holidays
interest_only_periods Integer No 0 - Number of interest-only payments
grace_period_days Integer No 0 - Days before first payment
interest_method Symbol No :simple :simple, :precomputed Interest calculation method

*Provide either term_months or num_payments, not both.

Public Methods

#generate(output: :console, csv_path: nil)

Render the schedule to console or CSV file.

schedule.generate                                          # console output
schedule.generate(output: :csv, csv_path: "schedule.csv")  # CSV file

#schedule

Returns a frozen array of payment hashes. Each hash contains:

{
  payment_number: 1,
  date: #<Date>,
  principal_payment: 8302.48,
  interest_payment: 489.11,
  additional_fee_payment: 0.0,
  total_payment: 8791.59,
  principal_balance: 91697.52,
  accrued_interest: 489.11,
  total_balance: 92186.63,
  payment_type: "Regular Payment",
  days_in_period: 29
}

#summary

Returns a hash with loan totals:

{
  start_date: #<Date>,
  end_date: #<Date>,
  principal: 100000.0,
  total_payments: 12,
  frequency: :monthly,
  annual_rate: 10.0,
  payment_amount: 8791.59,
  total_interest: 5499.06,
  total_paid: 105499.06
}

Convenience Methods

Method Returns
#end_date Date of the last payment
#total_interest Sum of all interest payments
#total_paid Sum of all payments
#payment_amount Regular payment amount

Payment Frequencies

Monthly (:monthly)

Payments on the same day each month. If the start date is the 31st, payments on shorter months use the last day of the month (e.g., Jan 31 -> Feb 28 -> Mar 28).

Bi-weekly (:biweekly)

Payments every 14 days. Common for payroll-aligned loan repayment.

Weekly (:weekly)

Payments every 7 days.

Daily (:daily)

Payments every day. When combined with bank_days_only: true, payments skip weekends and federal holidays.

Fee Treatment Options

1. Distributed (:distributed)

The fee is spread evenly across all payments.

Best for: Keeping individual payments manageable while recovering fees over time

2. Add to Principal (:add_to_principal)

The fee is added to the loan principal upfront, increasing the base amount financed.

Best for: Rolling all costs into the loan amount

3. Separate Payment (:separate_payment)

The fee is collected as a separate first payment before regular amortization begins.

Best for: Collecting fees upfront separately from the loan repayment

Interest Calculation Methods

Simple Interest (:simple)

Interest accrues daily on the remaining principal balance. As you pay down the principal, interest payments decrease over time.

Formula: Daily interest = (Principal Balance x Annual Rate) / 365

Best for: Traditional amortizing loans, consumer loans

Precomputed Interest (:precomputed)

Total interest is calculated upfront based on the original principal and divided equally across all payments. Interest per payment stays constant regardless of principal reduction.

Best for: Fixed payment structures, certain consumer loan types

Federal Bank Holidays

When bank_days_only: true, the schedule automatically skips weekends and US Federal Reserve holidays:

  • New Year's Day, MLK Jr. Day, Presidents' Day, Memorial Day, Juneteenth, Independence Day, Labor Day, Columbus Day, Veterans Day, Thanksgiving, Christmas

Weekend observation rules are handled automatically by the holidays gem.

Usage Examples

30-Year Mortgage

schedule = Amortizy::AmortizationSchedule.new(
  start_date: "2026-01-15",
  principal: 350_000.00,
  term_months: 360,
  annual_rate: 6.5,
  frequency: :monthly
)

puts "Monthly payment: $#{'%.2f' % schedule.payment_amount}"
puts "Total interest: $#{'%.2f' % schedule.total_interest}"

Weekly Loan with Grace Period

schedule = Amortizy::AmortizationSchedule.new(
  start_date: "2026-01-15",
  principal: 75_000.00,
  term_months: 12,
  annual_rate: 15.0,
  frequency: :weekly,
  grace_period_days: 7
)

schedule.generate

Interest-Only with Bank Days

schedule = Amortizy::AmortizationSchedule.new(
  start_date: "2026-01-15",
  principal: 100_000.00,
  term_months: 18,
  annual_rate: 10.0,
  frequency: :weekly,
  interest_only_periods: 8,
  bank_days_only: true
)

schedule.generate

Complex Commercial Loan

schedule = Amortizy::AmortizationSchedule.new(
  start_date: "2026-01-15",
  principal: 250_000.00,
  term_months: 18,
  annual_rate: 14.5,
  frequency: :daily,
  origination_fee: 25_000.00,
  additional_fee: 5_000.00,
  additional_fee_label: "Processing Fee",
  additional_fee_treatment: :distributed,
  bank_days_only: true,
  interest_only_periods: 20,
  grace_period_days: 5,
  interest_method: :simple
)

schedule.generate(output: :csv, csv_path: "commercial_loan.csv")

Command Line Interface

Amortizy includes an interactive CLI tool:

amortizy

The CLI provides:

  1. Run default examples (demonstrates simple vs precomputed interest)
  2. Enter parameters manually with interactive prompts
  3. Automatic CSV generation option

Requirements

  • Ruby 3.0 or higher
  • holidays gem (~> 8.0) - automatically installed as a dependency

Development

After checking out the repo, run:

# Install dependencies
bundle install

# Run tests
bundle exec rspec

# Run linter
bundle exec rubocop

# Run interactive console
bin/console

Testing

The test suite includes 70 RSpec examples covering:

  • Initialization and validation
  • Payment calculations for all four frequencies
  • Dual input model (term_months vs num_payments)
  • Effective principal calculations
  • Schedule generation and full amortization
  • Interest methods (simple vs precomputed)
  • Grace periods and interest-only periods
  • Bank day functionality and federal holiday detection
  • Fee treatments (distributed, add to principal, separate payment)
  • Monthly date drift prevention with bank_days_only
  • CSV generation
  • Public API (schedule, summary, convenience methods)
  • Deep freeze immutability
  • Edge cases (year boundaries, month-end clamping, input validation)

Run the test suite:

bundle exec rspec

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/zapatify/amortizy.

How to Contribute

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Make your changes
  4. Add tests for new functionality
  5. Ensure all tests pass (bundle exec rspec)
  6. Commit your changes (git commit -m 'Add amazing feature')
  7. Push to your branch (git push origin feature/amazing-feature)
  8. Open a Pull Request

License

The gem is available as open source under the terms of the MIT License.

Changelog

See CHANGELOG.md for version history and release notes.

Author

Rich Zapata - @zapatify