Skip to content

Instantly share code, notes, and snippets.

@raxoft
Last active August 29, 2015 14:21
Show Gist options
  • Save raxoft/1e717d7dcaab6949ab03 to your computer and use it in GitHub Desktop.
Save raxoft/1e717d7dcaab6949ab03 to your computer and use it in GitHub Desktop.
Simple Money helper. Showcases how to integrate custom types into Ruby, coerce them into other types and define binary operators properly. Example of an extensive unit test is included as well.
# Simple Money helper.
#
# Written by Patrik Rak in 2014.
# Money class for convenient working with amounts internally stored in cents.
class Money
# Helper class for handling left hand side arguments of binary operators.
class Scalar
include Comparable
attr_reader :value
def initialize( value )
@value = value
end
def <=> other
cmp = ( other <=> value ) and -cmp
end
def + other
other + value
end
def - other
- ( other - value )
end
def * other
other * value
end
def / other #/
raise TypeError, "#{other.class} can't divide #{value.class}"
end
end
include Comparable
attr_reader :cents
# Create Money object for given value in cents.
def initialize( cents )
@cents = cents.to_i
end
# Create new Money object for given amount.
# Input can be either floating point value in dollars stored as Float or String,
# or integer value in cents stored as any other type responding to to_i.
def self.new( value )
case value
when String
new( Float( value ) )
when Float
super( ( value * 100 ).round )
else
super
end
end
# Compute object hash.
def hash
cents.hash
end
# Compare two Money objects for identity.
def eql? other
other.is_a?( Money ) and cents == other.cents
end
# Compare money with other objects.
def <=> other
case other
when Money, Integer
to_i <=> other.to_i
else
nil
end
end
# Make it possible to use Money as the right hand side with other types.
def coerce( other )
case other
when Numeric
[ Scalar.new( other ), self ]
else
raise TypeError, "#{self.class} can't be coerced into #{other.class}"
end
end
# Define the negation operator.
def -@
Money.new( -to_i )
end
# Define the addition operator.
def + other
case other
when Money, Integer
Money.new( to_i + other.to_i )
else
raise TypeError, "#{other.class} can't be added to #{self.class}"
end
end
# Define the subtraction operator.
def - other
case other
when Money, Integer
Money.new( to_i - other.to_i )
else
raise TypeError, "#{other.class} can't be subtracted from #{self.class}"
end
end
# Define the multiplication operator.
def * other
case other
when Integer
Money.new( to_i * other.to_i )
when Float
Money.new( ( to_i * other.to_f ).round.to_i )
else
raise TypeError, "#{other.class} can't multiply #{self.class}"
end
end
# Define the division operator.
def / other #/
case other
when Integer, Float
Money.new( ( to_i / other.to_f ).round.to_i )
else
raise TypeError, "#{other.class} can't divide #{self.class}"
end
end
# Get the floating point amount.
def amount
cents / 100.0
end
alias dollars amount
# Get the integer value.
def to_i
cents
end
# Get the floating point value.
def to_f
amount
end
# Format as floating point string.
def to_s
"%.2f" % amount
end
end
# Shortcut for creating new Money object.
def Money( value )
Money.new( value )
end
# Run the test if this file is run directly.
if $0 == __FILE__
require 'bacon'
describe Money do
should 'represent money in cents' do
Money( 1000 ).should == 1000
Money( 0 ).should == 0
Money( -99 ).should == -99
Money( 1.0 ).should == 100
Money( 0.0 ).should == 0
Money( -10.12 ).should == -1012
Money( '123' ).should == 12300
Money( '99.99' ).should == 9999
Money( '-13.01' ).should == -1301
Money( ' -5.32 ' ).should == -532
Money( nil ).should == 0
end
should 'raise on parsing errors' do
->{ Money( '' ) }.should.raise ArgumentError
->{ Money( 'xyz' ) }.should.raise ArgumentError
->{ Money( '123x' ) }.should.raise ArgumentError
->{ Money( '$123' ) }.should.raise ArgumentError
->{ Money( '- 5' ) }.should.raise ArgumentError
end
should 'be convertible to various formats' do
m = Money( '-1234567.89' )
m.to_i.should == -123456789
m.to_f.should == -1234567.89
m.to_s.should == '-1234567.89'
m.cents.should == -123456789
m.dollars.should == -1234567.89
m.amount.should == -1234567.89
end
should 'round trip without loosing precision' do
for cents in -1000..1000
m = Money( cents )
m.should == m
m.should == cents
m.should == Money( m )
m.should == Money( m.to_i )
m.should == Money( m.to_f )
m.should == Money( m.to_s )
end
end
should 'be comparable' do
Money.should.include Comparable
Money( 1 ).should == Money( 1 )
Money( 1 ).should.not != Money( 1 )
Money( 1 ).should != Money( 2 )
Money( 2 ).should != Money( 1 )
Money( 1 ).should.not == Money( 2 )
Money( 2 ).should.not == Money( 1 )
Money( 1 ).should.be <= Money( 1 )
Money( 1 ).should.be <= Money( 2 )
Money( 1 ).should.be >= Money( 1 )
Money( 2 ).should.be >= Money( 1 )
Money( 1 ).should.not.be >= Money( 2 )
Money( 2 ).should.not.be <= Money( 1 )
Money( 1 ).should.not.be < Money( 1 )
Money( 1 ).should.be < Money( 2 )
Money( 1 ).should.not.be > Money( 1 )
Money( 2 ).should.be > Money( 1 )
Money( 1 ).should.not.be > Money( 2 )
Money( 2 ).should.not.be < Money( 1 )
end
should 'be comparable with integer types' do
Money( 1 ).should == 1
1.should == Money( 1 )
Money( 1 ).should.not != 1
1.should.not != Money( 1 )
Money( 1 ).should.be < 2
Money( 1 ).should.not.be > 2
2.should.be > Money( 1 )
2.should.not.be < Money( 1 )
end
should 'not be comparable with other types' do
for value in [ 1.0, 2.5, "1.0", "5", nil ]
Money( 100 ).should.not == value
Money( 100 ).should != value
->{ Money( 100 ) <= value }.should.raise ArgumentError
->{ Money( 100 ) >= value }.should.raise ArgumentError
value.should.not == Money( 100 )
value.should != Money( 100 )
->{ value <= Money( 100 ) }.should.raise ArgumentError, NoMethodError
->{ value >= Money( 100 ) }.should.raise ArgumentError, NoMethodError
end
end
should 'support eql? operator' do
Money( 10 ).should.eql Money( 10 )
Money( 10 ).should.not.eql Money( 20 )
Money( 10 ).should.not.eql 10
10.should.not.eql Money( 10 )
end
should 'work as a hash key' do
h = { Money( 10 ) => 1, 10 => 2 }
h.keys.should == [ Money( 10 ), 10 ]
h.keys.should == [ 10, Money( 10 ) ]
h.keys.should == [ 10, 10 ]
h.keys.should.eql? [ Money( 10 ), 10 ]
h.keys.should.not.eql? [ 10, Money( 10 ) ]
h.keys.should.not.eql? [ 10, 10 ]
h.values.should == [ 1, 2 ]
h = { Money( 10 ) => 1, Money( 10 ) => 2 }
h.keys.should.eql? [ Money( 10 ) ]
h.values.should == [ 2 ]
end
should 'support simple operators' do
( -Money( 10 ) ).should.eql Money( -10 )
( Money( 10 ) + Money( 20 ) ).should.eql Money( 30 )
( Money( 10 ) + 20 ).should.eql Money( 30 )
( 10 + Money( 20 ) ).should.eql Money( 30 )
->{ Money( 10 ) + 1.5 }.should.raise TypeError
->{ 1.5 + Money( 10 ) }.should.raise TypeError
( Money( 10 ) - Money( 20 ) ).should.eql Money( -10 )
( Money( 10 ) - 20 ).should.eql Money( -10 )
( 10 - Money( 20 ) ).should.eql Money( -10 )
->{ Money( 10 ) - 1.5 }.should.raise TypeError
->{ 1.5 - Money( 10 ) }.should.raise TypeError
->{ Money( 10 ) * Money( 20 ) }.should.raise TypeError
( Money( 10 ) * 20 ).should.eql Money( 200 )
( 10 * Money( 20 ) ).should.eql Money( 200 )
( Money( 10 ) * 1.5 ).should.eql Money( 15 )
( Money( 10 ) * 1.99999 ).should.eql Money( 20 )
( 1.5 * Money( 20 ) ).should.eql Money( 30 )
->{ Money( 10 ) / Money( 20 ) }.should.raise TypeError
( Money( 20 ) / 10 ).should.eql Money( 2 )
( Money( 20000 ) / 101 ).should.eql Money( 198 )
->{ 10 / Money( 20 ) }.should.raise TypeError
( Money( 10 ) / 2.0 ).should.eql Money( 5 )
( Money( 10 ) / 1.9999 ).should.eql Money( 5 )
->{ 0.5 / Money( 20 ) }.should.raise TypeError
end
end
end
# EOF #
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment