Skip to content

Instantly share code, notes, and snippets.

@edudobay
Last active March 17, 2024 15:05
Show Gist options
  • Save edudobay/7b5454c3dc391b6cf8e9ddcf64c48fe2 to your computer and use it in GitHub Desktop.
Save edudobay/7b5454c3dc391b6cf8e9ddcf64c48fe2 to your computer and use it in GitHub Desktop.
Example Money/Currency classes in Ruby 3.x
require 'bigdecimal'
class CurrencyMismatch < RuntimeError
end
class Currency
def initialize(symbol, decimals)
@symbol = symbol
@decimals = decimals
end
def quantize(value)
BigDecimal(value, @decimals)
end
def format_value(value)
raise TypeError.new("Cannot format #{value.class}") unless value.is_a?(BigDecimal)
format_str = "%.#{decimals}f"
sprintf(format_str, value)
end
def to_s
"Currency(#{symbol}, decimals=#{decimals})"
end
attr_reader :symbol
attr_reader :decimals
@@default = nil
@@currencies = {}
def self.register(currency)
raise TypeError("Currency must be #{Currency.class}, got #{currency.class}") unless currency.is_a?(Currency)
@@currencies[currency.symbol.to_sym] = currency
end
def self.register_all(currencies)
currencies.each { |c| self.register(c) }
end
def self.all_registered = @@currencies.values
def self.registered?(symbol) = not @@currencies[symbol.to_sym].nil?
def self.named(symbol) = @@currencies[symbol.to_sym]
def self.resolve(currency)
case currency
when Currency
currency
when String, Symbol
self.named(currency)
else
raise TypeError("Cannot resolve currency from #{currency.class}")
end
end
def self.default
raise "Default currency not set" if @@default.nil?
@@default
end
def self.default=(currency)
currency = self.resolve(currency)
raise TypeError("Default currency must be instance of Currency, got #{currency.class}") unless currency.is_a?(Currency)
@@default = currency
end
end
class Money
include Comparable
def initialize(value, currency)
raise TypeError("Cannot create Money from #{value.class}, #{currency.class}") unless value.is_a?(BigDecimal) && currency.is_a?(Currency)
@value = value
@currency = currency
end
def self.register_currency_shortcuts(currencies)
currencies.each do |currency|
define_singleton_method(currency.symbol.to_sym) { |value| Money.make(value, currency) }
end
end
def self.make(value, currency)
currency = Currency.resolve(currency)
Money.new(currency.quantize(value), currency)
end
def self.from_int(value, currency)
currency = Currency.resolve(currency)
value = BigDecimal(value) * 10 ** (-currency.decimals)
Money.new(value, currency)
end
def formatted_value = currency.format_value(value)
def currency_symbol = @currency.symbol
def to_s
"Money(#{formatted_value} #{currency_symbol})"
end
attr_reader :value
attr_reader :currency
def same_currency?(other)
currency == other.currency
end
private def assert_same_currency(other)
raise CurrencyMismatch.new("Cannot operate different currencies: #{currency_symbol} != #{other.currency_symbol}") unless same_currency?(other)
end
def +(other)
assert_same_currency(other)
Money.make(@value + other.value, @currency)
end
def -(other)
assert_same_currency(other)
Money.make(@value - other.value, @currency)
end
def *(other)
raise TypeError("Cannot multiply by #{other.class}") unless (
other.is_a?(Integer) || other.is_a?(Float) || other.is_a?(BigDecimal)
)
Money.make(@value * other, @currency)
end
def <=>(other)
assert_same_currency(other)
@value <=> other.value
end
end
# ------------------------------------------------------------------------------
# Examples
# ------------------------------------------------------------------------------
Currency.register_all([
Currency.new(:BRL, 2),
Currency.new(:CAD, 2),
Currency.new(:JPY, 0),
])
Currency.default = :BRL
Money.register_currency_shortcuts(Currency.all_registered)
puts Money.BRL(150)
puts Money.CAD(150)
puts Money.from_int(200_00, :BRL)
puts Money.CAD(150) * 0.317
puts Money.JPY(150)
puts Money.BRL(150) > Money.BRL(20)
# Errors
puts Money.BRL(150) + Money.CAD(20)
puts Money.BRL(150) > Money.CAD(20)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment