Immutable money value object with integer subunit storage and multi-currency formatting
gem install philiprehberger-moneyImmutable money value object with integer subunit storage and multi-currency formatting
Add to your Gemfile:
gem "philiprehberger-money"
Or install directly:
gem install philiprehberger-money
require "philiprehberger/money"
price = Philiprehberger::Money.new(1999, :usd)
price.amount # => 19.99
price.to_s # => "$19.99"
require "philiprehberger/money"
a = Philiprehberger::Money.new(1000, :usd)
b = Philiprehberger::Money.new(500, :usd)
sum = a + b # => $15.00
diff = a - b # => $5.00
mult = a * 2 # => $20.00
quot = a / 3 # => $3.33 (banker's rounding)
neg = -a # => -$10.00
abs = neg.abs # => $10.00
require "philiprehberger/money"
usd = Philiprehberger::Money.new(1999, :usd)
usd.format # => "$19.99"
usd.format(code: true) # => "$19.99 USD"
usd.format(symbol: false) # => "19.99"
eur = Philiprehberger::Money.new(1999, :eur)
eur.format # => "€19,99"
jpy = Philiprehberger::Money.new(2000, :jpy)
jpy.format # => "¥2,000"
require "philiprehberger/money"
Philiprehberger::Money.parse("$1,234.56") # => Money(123456, :usd)
Philiprehberger::Money.parse("1.234,56 EUR") # => Money(123456, :eur)
Philiprehberger::Money.parse("¥2000", currency: :jpy) # => Money(2000, :jpy)
Philiprehberger::Money.parse("-$19.99") # => Money(-1999, :usd)
require "philiprehberger/money"
price = Philiprehberger::Money.new(10000, :usd) # $100.00
price.percent(15) # => $15.00 (15% tip)
price.add_percent(20) # => $120.00 (with 20% tax)
price.subtract_percent(25) # => $75.00 (25% discount)
require "philiprehberger/money"
net = Philiprehberger::Money.new(10000, :usd) # $100.00
result = net.tax_breakdown(0.2)
result[:net].to_s # => "$100.00"
result[:tax].to_s # => "$20.00"
result[:gross].to_s # => "$120.00"
require "philiprehberger/money"
min = Philiprehberger::Money.new(500, :usd) # $5.00
max = Philiprehberger::Money.new(2000, :usd) # $20.00
Philiprehberger::Money.new(1000, :usd).clamp(min, max).to_s # => "$10.00" (within range)
Philiprehberger::Money.new(100, :usd).clamp(min, max).to_s # => "$5.00" (clamped to min)
Philiprehberger::Money.new(5000, :usd).clamp(min, max).to_s # => "$20.00" (clamped to max)
require "philiprehberger/money"
total = Philiprehberger::Money.new(1000, :usd)
shares = total.allocate([0.5, 0.3, 0.2])
shares.map(&:cents) # => [500, 300, 200]
# Remainder cents are distributed fairly
odd = Philiprehberger::Money.new(100, :usd)
parts = odd.allocate([1, 1, 1])
parts.map(&:cents) # => [34, 33, 33]
parts.sum(&:cents) # => 100
require "philiprehberger/money"
total = Philiprehberger::Money.new(1000, :usd)
parts = total.split(3)
parts.map(&:cents) # => [334, 333, 333]
parts.sum(&:cents) # => 1000
require "philiprehberger/money"
# Default: banker's rounding (half to even)
Philiprehberger::Money.from_amount(0.025, :usd) # => 2 cents
# Standard rounding (half up)
Philiprehberger::Money.from_amount(0.025, :usd, rounding: :half_up) # => 3 cents
# Always round up
Philiprehberger::Money.from_amount(0.021, :usd, rounding: :ceil) # => 3 cents
# Always round down
Philiprehberger::Money.from_amount(0.029, :usd, rounding: :floor) # => 2 cents
require "philiprehberger/money"
Philiprehberger::Money::Currency.register(
code: "XAU",
name: "Gold Troy Ounce",
symbol: "Au",
subunit_to_unit: 100,
symbol_first: true
)
gold = Philiprehberger::Money.from_amount(1950.50, :xau)
gold.format # => "Au1,950.50"
require "philiprehberger/money"
usd = Philiprehberger::Money.new(1000, :usd)
eur = usd.convert_to(:eur, rate: 0.85)
eur.cents # => 850
eur.currency.code # => :eur
require "philiprehberger/money"
# Set up exchange rates
Philiprehberger::Money::ExchangeRate.store.set(:USD, :EUR, 0.85)
Philiprehberger::Money::ExchangeRate.store.set(:USD, :GBP, 0.73)
# Convert using the store (no need to pass a rate)
usd = Philiprehberger::Money.new(1000, :usd)
eur = usd.exchange_to(:EUR)
eur.cents # => 850
# Inverse rates are resolved automatically
eur_amount = Philiprehberger::Money.new(850, :eur)
back_to_usd = eur_amount.exchange_to(:USD)
# Sum mixed-currency amounts into a target currency
moneys = [
Philiprehberger::Money.new(1000, :usd),
Philiprehberger::Money.new(850, :eur)
]
total = Philiprehberger::Money.sum(moneys, target_currency: :USD)
total.cents # => 2000
# Clear all rates
Philiprehberger::Money::ExchangeRate.store.clear
require "philiprehberger/money"
price = Philiprehberger::Money.new(123, :usd) # $1.23
price.round_to_nearest(5).cents # => 125 ($1.25)
price.round_to_nearest(10).cents # => 120 ($1.20)
price.round_to_nearest(25).cents # => 125 ($1.25)
require "philiprehberger/money"
# JPY uses 0 decimal places (exponent 0)
jpy = Philiprehberger::Money.new(2000, :jpy)
jpy.currency.exponent # => 0
jpy.round.cents # => 2000 (no-op at default precision)
# USD uses 2 decimal places (exponent 2)
usd = Philiprehberger::Money.from_amount(1.234, :usd)
usd.currency.exponent # => 2
usd.round(0).cents # => 100 ($1.00, rounded to whole units)
usd.round(1).cents # => 120 ($1.20, rounded to nearest tenth)
usd.round.cents # => usd.cents (default precision = currency exponent)
price = Philiprehberger::Money.new(1999, :usd)
price.to_h
# => { cents: 1999, amount: 19.99, currency: "USD", formatted: "$19.99" }
case Philiprehberger::Money.new(1999, :usd)
in { currency: :usd, amount: Float => amt }
puts "USD amount: #{amt}"
end
require "philiprehberger/money"
a = Philiprehberger::Money.new(1000, :usd)
b = Philiprehberger::Money.new(2000, :usd)
a < b # => true
a == b # => false
a.zero? # => false
Money class methods| Method | Description |
|---|---|
.new(cents, currency_code) | Create from integer subunits and currency code |
.from_amount(amount, currency_code, rounding:) | Create from decimal amount with configurable rounding |
.parse(string, currency:) | Parse a formatted money string into a Money object |
.sum(moneys, target_currency:) | Sum a collection of Money objects into a target currency |
Money instance methods| Method | Description |
|---|---|
#cents | Integer subunit amount |
#currency | Currency object with code, symbol, and formatting rules |
#amount | BigDecimal representation of the amount |
#to_f | Float representation of the amount |
#rounding_mode | The rounding mode used for arithmetic |
#+(other) | Add two same-currency Money objects |
#-(other) | Subtract two same-currency Money objects |
#*(numeric) | Multiply by a number (uses stored rounding mode) |
#/(numeric) | Divide by a number (uses stored rounding mode) |
#-@ | Negate the amount |
#abs | Absolute value |
#percent(n) | Return n% of the amount |
#add_percent(n) | Return money + n% |
#subtract_percent(n) | Return money - n% |
#tax_breakdown(rate) | Return hash with net, tax, and gross Money objects |
#clamp(min, max) | Constrain value within same-currency bounds |
#allocate(ratios) | Split by ratios using largest remainder method |
#split(n) | Split equally among n parts |
#format(symbol:, code:, thousands:) | Format as string with options |
#to_s | Format with default options |
#convert_to(target_code, rate:) | Convert to another currency |
#exchange_to(target_code) | Convert using the ExchangeRate store |
#round_to_nearest(increment) | Round to nearest N subunits |
#round(precision = nil) | Round to N decimal places (defaults to currency exponent) |
#zero? | True if amount is zero |
#positive? | True if amount is positive |
#negative? | True if amount is negative |
#<=>(other) | Compare same-currency amounts |
#to_h | Hash with cents, amount, currency, formatted |
#deconstruct_keys(keys) | Pattern matching support for case/in |
#eql?(other) | Value equality check |
#hash | Hash for use as hash key |
ExchangeRate methods| Method | Description |
|---|---|
.store | Returns the singleton exchange rate store |
.reset! | Resets the store (clears all rates) |
#set(from, to, rate) | Set a conversion rate between two currencies |
#get(from, to) | Get a conversion rate (resolves inverse automatically) |
#clear | Remove all stored rates |
#rates_count | Number of stored rates |
Currency class methods| Method | Description |
|---|---|
.find(code) | Look up a currency by code |
.register(code:, name:, symbol:, ...) | Register a custom currency |
Currency instance methods| Method | Description |
|---|---|
#code | ISO 4217 currency code (lowercase symbol, e.g. :usd) |
#name | Full currency name |
#symbol | Currency symbol |
#subunit_to_unit | Number of subunits per unit (100 for cents, 1 for zero-decimal) |
#exponent | Number of decimal places for the currency (e.g. 2 for USD, 0 for JPY) |
#symbol_first | Whether the symbol appears before the amount |
#decimal_separator | Character separating decimals |
#thousands_separator | Character separating thousands |
bundle install
bundle exec rspec
bundle exec rubocop
If you find this project useful: