Last active
March 17, 2024 15:05
-
-
Save edudobay/7b5454c3dc391b6cf8e9ddcf64c48fe2 to your computer and use it in GitHub Desktop.
Example Money/Currency classes in Ruby 3.x
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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