Skip to content

Instantly share code, notes, and snippets.

@dkubb
Last active January 13, 2017 01:39
Show Gist options
  • Save dkubb/ec346807184f28c0bf45daeddcc7ba77 to your computer and use it in GitHub Desktop.
Save dkubb/ec346807184f28c0bf45daeddcc7ba77 to your computer and use it in GitHub Desktop.
A modern ruby Money class
require 'bigdecimal'
require 'bigdecimal/util'
require 'rubygems'
require 'adamantium'
require 'concord'
class Money
include Adamantium, Concord.new(:amount), Comparable
OPERAND_CLASSES = [self, Rational, BigDecimal, Integer].freeze
SCALE = 2
private_class_method :new
def self.from_numeric(numeric)
new(coerce_numeric(numeric).tap { |coerced| assert_scale(coerced) })
end
def self.coerce_numeric(numeric)
assert_valid_numeric(numeric)
numeric.to_r
end
private_class_method :coerce_numeric
def self.assert_valid_numeric(numeric)
unless OPERAND_CLASSES.any? { |klass| numeric.kind_of?(klass) }
raise TypeError,
"Operand must be #{OPERAND_CLASSES.join(', ')}, not #{numeric.class}"
end
end
private_class_method :assert_valid_numeric
def self.assert_scale(numeric)
unless numeric.eql?(numeric.round(SCALE))
raise ArgumentError, "Must have no more than #{SCALE} decimal places"
end
end
private_class_method :assert_scale
def <=>(other)
amount <=> other.amount if instance_of?(other.class)
end
# Unary methods returning Money
%i[+@ -@ abs].each do |method|
define_method(method) do
new(amount.public_send(method))
end
end
# Binary methods returning Money
%i[+ - * /].each do |method|
define_method(method) do |other|
new(amount.public_send(method, coerce_numeric(other)))
end
end
# Unary methods called on amount
%i[positive? negative? zero?].each do |method|
define_method(method) { amount.public_send(method) }
end
def nonzero?
self unless zero?
end
def to_d
(amount.numerator.to_d / amount.denominator).round(
SCALE,
BigDecimal::ROUND_HALF_EVEN
)
end
def to_s
'$%.*f' % [SCALE, to_d]
end
def coerce(other)
[self, coerce_numeric(other)]
end
protected
def to_r
amount
end
private
# Proxy methods to private binary class methods
%i[new coerce_numeric].each do |method|
define_method(method) { |*args| self.class.__send__(method, *args) }
end
end
m0 = Money.from_numeric(BigDecimal('0.01'))
m1 = m0 * 0.1.to_d * 1/0.1.to_d
m2 = 0.1.to_d * 1/0.1.to_d * m0
m3 = m0 / 30 * 30
m4 = m0 / 10
m5 = Money.from_numeric(BigDecimal('0.02')) * BigDecimal('1.25')
puts m0.to_s # => "$0.01
puts m1.to_s # => "$0.01
puts m2.to_s # => "$0.01
puts m3.to_s # => "$0.01
puts m4.to_s # => "$0.00
puts m5.to_s # => "$0.02 ($0.03 without rounding mode specified)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment