A Ruby library for handling money and currency operations with precision, type safety, and financial best practices. Built for BFSI (Banking, Financial Services, and Insurance) applications where accuracy and proper currency handling are critical.
- 💰 Money Value Objects - Immutable money objects with BigDecimal precision (no floating-point errors)
- 🌍 Currency Support - ISO 4217 currency codes with symbols, names, and decimal places
- 💱 Currency Conversion - Exchange rate management and multi-currency conversions
- 🔢 Multiple Rounding Strategies - Banker's rounding, floor, ceiling, and more
- 🎨 Flexible Formatting - Locale-aware currency formatting with customizable options
- ➕ Arithmetic Operations - Safe addition, subtraction, multiplication, and division
- 🔍 Type Safety - Prevents mixing different currencies in operations
- 🧊 Immutable Objects - Value objects that are frozen for thread safety
Add the lib directory to your Ruby load path:
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "lib"))Or require files directly:
require_relative "lib/money"
require_relative "lib/currency"require_relative "lib/money"
require_relative "lib/currency"
# Create money objects
hundred_dollars = Money.new(100, :USD)
fifty_euros = Money.new(50, :EUR)
# Arithmetic operations
total = hundred_dollars + Money.new(50, :USD) # => Money(150, USD)
half = hundred_dollars / 2 # => Money(50, USD)
doubled = hundred_dollars * 2 # => Money(200, USD)
# Currency conversion
require_relative "lib/exchange_rate_loader"
ExchangeRateLoader.load_default_rates
eur_money = hundred_dollars.convert_to(:EUR) # => Money(85, EUR)
# Formatting
require_relative "lib/formatters"
puts Formatters.format(hundred_dollars) # => "$100.00"usd = Currency.new(:USD)
puts usd.symbol # => "$"
puts usd.name # => "US Dollar"
puts usd.decimal_places # => 2
jpy = Currency.new(:JPY)
puts jpy.has_decimals? # => false (Yen has no decimal places)# From numeric values
money1 = Money.new(100, :USD)
money2 = Money.new(99.99, :EUR)
# From strings (useful for parsing user input)
money3 = Money.new("100.50", :USD)
# Parse from string format
money4 = Money.parse("99.99 USD") # => Money(99.99, USD)
# Create zero money
zero = Money.zero(:USD) # => Money(0, USD)hundred = Money.new(100, :USD)
fifty = Money.new(50, :USD)
# Addition
total = hundred + fifty # => Money(150, USD)
# Subtraction
difference = hundred - fifty # => Money(50, USD)
# Multiplication (scalar)
doubled = hundred * 2 # => Money(200, USD)
# Division (scalar)
half = hundred / 2 # => Money(50, USD)
# Mixing currencies raises an error
# hundred + Money.new(50, :EUR) # => ArgumentErrormoney1 = Money.new(100, :USD)
money2 = Money.new(200, :USD)
money1 < money2 # => true
money1 > money2 # => false
money1 == money2 # => false
# Sort money objects
amounts = [Money.new(300, :USD), Money.new(100, :USD), Money.new(200, :USD)]
amounts.sort # => [Money(100, USD), Money(200, USD), Money(300, USD)]money = Money.new(99.995, :USD)
money.round(:default) # => Money(100.00, USD) - Standard rounding (half up)
money.round(:banker) # => Money(100.00, USD) - Banker's rounding (half to even)
money.round(:floor) # => Money(99.99, USD) - Always round down
money.round(:ceiling) # => Money(100.00, USD) - Always round uprequire_relative "lib/exchange_rate_loader"
# Load exchange rates
ExchangeRateLoader.load_default_rates
# Convert money
usd_money = Money.new(100, :USD)
eur_money = usd_money.convert_to(:EUR) # => Money(85, EUR)
gbp_money = usd_money.convert_to(:GBP) # => Money(73, GBP)
jpy_money = usd_money.convert_to(:JPY) # => Money(11000, JPY)
# Convert back
usd_again = eur_money.convert_to(:USD) # => Money(100, USD)
# Set custom exchange rates
ExchangeRateLoader.set_rate(:USD, :BTC, 0.000025)
btc_money = usd_money.convert_to(:BTC)require_relative "lib/formatters"
money = Money.new(1234.56, :USD)
# Default formatting
Formatters.format(money) # => "$1,234.56"
# Custom options
Formatters.format(money, symbol_position: :after) # => "1,234.56 $"
Formatters.format(money, show_symbol: false) # => "1,234.56"
# Different currencies have different formats
Formatters.format(Money.new(1234.56, :EUR)) # => "€1.234,56" (European format)
Formatters.format(Money.new(1234, :JPY)) # => "¥1,234" (no decimals)transactions = [
Money.new(29.99, :USD),
Money.new(15.50, :USD),
Money.new(8.75, :USD),
]
total = transactions.reduce(Money.zero(:USD)) { |sum, money| sum + money }
puts total # => "$54.24"principal = Money.new(1000, :USD)
fee_rate = 0.025
fee = (principal * fee_rate).round(:ceiling) # Always round fees up
total = principal + feeExchangeRateLoader.load_default_rates
portfolio = {
USD: Money.new(1000, :USD),
EUR: Money.new(500, :EUR),
GBP: Money.new(300, :GBP),
}
# Convert all to USD for total value
total_usd = portfolio.values.reduce(Money.zero(:USD)) do |sum, money|
sum + money.convert_to(:USD)
endThe library supports the following ISO 4217 currencies:
- USD - US Dollar ($)
- EUR - Euro (€)
- GBP - British Pound (£)
- JPY - Japanese Yen (¥) - No decimal places
- CAD - Canadian Dollar (C$)
- AUD - Australian Dollar (A$)
- CHF - Swiss Franc (CHF)
- CNY - Chinese Yuan (¥)
Both Money and Currency are implemented as value objects:
- Immutable (frozen after creation)
- Equality based on value, not object identity
- Safe to use in hashes and sets
- Thread-safe
All monetary amounts use BigDecimal instead of Float to avoid precision errors:
# Float (WRONG for money)
0.1 + 0.2 # => 0.30000000000000004
# BigDecimal (CORRECT)
BigDecimal('0.1') + BigDecimal('0.2') # => 0.3e0The library supports multiple rounding modes for different financial contexts:
:default/:half_up- Standard rounding (0.5 rounds up):banker/:half_even- Banker's rounding (reduces bias in large datasets):floor/:down- Always round down (used in tax calculations):ceiling/:up- Always round up (used in fee calculations):truncate- Truncate toward zero
The project includes a comprehensive example file demonstrating all features:
ruby app/franca.rbfranca/
├── lib/
│ ├── currency.rb # Currency value object
│ ├── money.rb # Money value object
│ ├── exchange_rate_loader.rb # Exchange rate management
│ ├── rounding_strategies.rb # Rounding implementations
│ └── formatters.rb # Currency formatting
└── app/
└── franca.rb # Usage examples
- Precision First - Uses BigDecimal to avoid floating-point errors
- Type Safety - Prevents invalid operations (e.g., mixing currencies)
- Immutability - All value objects are frozen for safety
- Financial Best Practices - Implements proper rounding and formatting
- Extensibility - Easy to add new currencies and formatting rules
Contributions are welcome! Please feel free to submit a Pull Request.
[some day]
Note: This library is designed for educational and development purposes. For production financial systems, consider additional features like:
- Real-time exchange rate APIs
- More comprehensive currency support
- Localization/internationalization
- Performance optimizations
- Comprehensive test coverage