Skip to content

Commit

Permalink
Merge pull request #29 from Shopify/allocate-support-rational
Browse files Browse the repository at this point in the history
Don't coerce Rationals to floats in allocate
  • Loading branch information
jahfer committed Feb 10, 2016
2 parents e8e5d90 + 0d35001 commit 2e188a6
Show file tree
Hide file tree
Showing 2 changed files with 29 additions and 20 deletions.
44 changes: 24 additions & 20 deletions lib/money/money.rb
Original file line number Diff line number Diff line change
Expand Up @@ -169,20 +169,17 @@ def fraction(rate)
# Money.new(5, "USD").allocate([0.3,0.7)) #=> [Money.new(2), Money.new(3)]
# Money.new(100, "USD").allocate([0.33,0.33,0.33]) #=> [Money.new(34), Money.new(33), Money.new(33)]
def allocate(splits)
allocations = splits.inject(0.0) {|sum, i| sum += i }
raise ArgumentError, "splits add to more than 100%" if allocations > 1.0
allocations = splits.inject(0) { |sum, n| sum + value_to_decimal(n) }

left_over = cents

amounts = splits.collect do |ratio|
fraction = (cents * ratio / allocations).floor
left_over -= fraction
fraction
if (allocations - BigDecimal("1")) > Float::EPSILON
raise ArgumentError, "splits add to more than 100%"
end

left_over.times { |i| amounts[i % amounts.length] += 1 }
amounts, left_over = amounts_from_splits(allocations, splits)

return amounts.collect { |cents| Money.from_cents(cents) }
left_over.to_i.times { |i| amounts[i % amounts.length] += 1 }

amounts.collect { |cents| Money.from_cents(cents) }
end

# Split money amongst parties evenly without losing pennies.
Expand All @@ -209,17 +206,24 @@ def split(num)
end

private
# poached from Rails
def value_to_decimal(value)
# Using .class is faster than .is_a? and
# subclasses of BigDecimal will be handled
# in the else clause
if value.class == BigDecimal
value
elsif value.respond_to?(:to_d)
value.to_d

def value_to_decimal(num)
if num.respond_to?(:to_d)
num.is_a?(Rational) ? num.to_d(16) : num.to_d
else
value.to_s.to_d
BigDecimal.new(num.to_s)
end
end

def amounts_from_splits(allocations, splits)
left_over = cents

amounts = splits.collect do |ratio|
(cents * ratio / allocations).floor.tap do |frac|
left_over -= frac
end
end

[amounts, left_over]
end
end
5 changes: 5 additions & 0 deletions spec/money_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,11 @@
specify "#allocate requires total to be less then 1" do
expect { Money.new(0.05).allocate([0.5,0.6]) }.to raise_error(ArgumentError)
end

specify "#allocate will use rationals if provided" do
splits = [128400,20439,14589,14589,25936].map{ |num| Rational(num, 203953) } # sums to > 1 if converted to float
expect(Money.new(2.25).allocate(splits)).to eq([Money.new(1.42), Money.new(0.23), Money.new(0.16), Money.new(0.16), Money.new(0.28)])
end
end

describe "split" do
Expand Down

0 comments on commit 2e188a6

Please sign in to comment.