Last active
August 29, 2015 14:21
-
-
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.
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
# 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