Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions lib/money/money.rb
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,66 @@ def calculate_splits(num)
Splitter.new(self, num).split.dup
end

# Calculate the unit price based on a total quantity of units.
# A number of units to take can be optionally provided to get the sum of
# their unit prices with correct rounding.
#
# The higher-valued splits are used first (i.e., units with the "extra penny").
#
# @param quantity [Integer] total number of units
# @param take [Integer] number of unit prices to sum (default: 1)
#
# @return [Money]
#
# @example Get the unit price for 1 unit out of 3
# Money.new(1.00, 'USD').per_unit(3)
# # => Money.new(0.34, 'USD') # the higher-valued split
#
# @example Get total for 3 units out of 3 (returns original amount)
# Money.new(1.00, 'USD').per_unit(3, take: 3)
# # => Money.new(1.00, 'USD')
def per_unit(quantity, take: 1)
raise ArgumentError, "take should be positive" if take < 0
raise ArgumentError, "quantity should be positive" if quantity <= 0
raise ArgumentError, "take cannot be greater than quantity" if take > quantity

calculate_splits(quantity).sum(Money.new(0, currency)) do |value, count|
count = [take, count].min
take -= count
value * count
end
end

# Calculate the unit price based on a total quantity of units.
# Uses the lesser-valued splits first (i.e., units without the "extra penny").
#
# A number of units to take can be optionally provided to get the sum of
# their unit prices with correct rounding.
#
# @param quantity [Integer] total number of units
# @param take [Integer] number of unit prices to sum (default: 1)
#
# @return [Money]
#
# @example Get the unit price for 1 unit out of 3 (lower value)
# Money.new(1.00, 'USD').reverse_per_unit(3)
# # => Money.new(0.33, 'USD') # the lower-valued split
#
# @example Get total for 3 units out of 3 (returns original amount)
# Money.new(1.00, 'USD').reverse_per_unit(3, take: 3)
# # => Money.new(1.00, 'USD')
def reverse_per_unit(quantity, take: 1)
raise ArgumentError, "quantity should be positive" if quantity < 0
raise ArgumentError, "take should be positive" if take <= 0
raise ArgumentError, "take cannot be greater than quantity" if take > quantity

calculate_splits(quantity).reverse_each.sum(Money.new(0, currency)) do |value, count|
count = [take, count].min
take -= count
value * count
end
end

# Clamps the value to be within the specified minimum and maximum. Returns
# self if the value is within bounds, otherwise a new Money object with the
# closest min or max value.
Expand Down
74 changes: 74 additions & 0 deletions spec/splitter_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,78 @@
})
end
end

describe "per_unit" do
specify "#per_unit returns the higher-valued split by default" do
# $1 split 3 ways: 34¢, 33¢, 33¢
expect(Money.new(1.00, 'USD').per_unit(3)).to eq(Money.new(0.34, 'USD'))
end

specify "#per_unit with take returns sum of unit prices" do
# Take 2 units: 34¢ + 33¢ = 67¢
expect(Money.new(1.00, 'USD').per_unit(3, take: 2)).to eq(Money.new(0.67, 'USD'))
end

specify "#per_unit with take: quantity returns original amount" do
expect(Money.new(1.00, 'USD').per_unit(3, take: 3)).to eq(Money.new(1.00, 'USD'))
end

specify "#per_unit works with non-decimal currencies" do
# 100 JPY split 3 ways: 34, 33, 33
expect(Money.new(100, 'JPY').per_unit(3)).to eq(Money.new(34, 'JPY'))
expect(Money.new(100, 'JPY').per_unit(3, take: 2)).to eq(Money.new(67, 'JPY'))
end

specify "#per_unit raises for invalid take" do
expect { Money.new(1.00, 'USD').per_unit(3, take: -1) }.to raise_error(ArgumentError, "take should be positive")
end

specify "#per_unit raises for invalid quantity" do
expect { Money.new(1.00, 'USD').per_unit(0) }.to raise_error(ArgumentError, "quantity should be positive")
expect { Money.new(1.00, 'USD').per_unit(-1) }.to raise_error(ArgumentError, "quantity should be positive")
end

specify "#per_unit raises when take exceeds quantity" do
expect { Money.new(1.00, 'USD').per_unit(3, take: 4) }.to raise_error(ArgumentError, "take cannot be greater than quantity")
end

specify "#per_unit returns zero when take is 0" do
expect(Money.new(1.00, 'USD').per_unit(3, take: 0)).to eq(Money.new(0, 'USD'))
end
end

describe "reverse_per_unit" do
specify "#reverse_per_unit returns the lower-valued split by default" do
# $1 split 3 ways: 34¢, 33¢, 33¢ (reversed: 33¢, 33¢, 34¢)
expect(Money.new(1.00, 'USD').reverse_per_unit(3)).to eq(Money.new(0.33, 'USD'))
end

specify "#reverse_per_unit with take returns sum of lower-valued unit prices" do
# Take 2 units (from reversed): 33¢ + 33¢ = 66¢
expect(Money.new(1.00, 'USD').reverse_per_unit(3, take: 2)).to eq(Money.new(0.66, 'USD'))
end

specify "#reverse_per_unit with take: quantity returns original amount" do
expect(Money.new(1.00, 'USD').reverse_per_unit(3, take: 3)).to eq(Money.new(1.00, 'USD'))
end

specify "#reverse_per_unit works with non-decimal currencies" do
# 100 JPY split 3 ways: 34, 33, 33 (reversed: 33, 33, 34)
expect(Money.new(100, 'JPY').reverse_per_unit(3)).to eq(Money.new(33, 'JPY'))
expect(Money.new(100, 'JPY').reverse_per_unit(3, take: 2)).to eq(Money.new(66, 'JPY'))
end

specify "#reverse_per_unit raises for invalid take" do
expect { Money.new(1.00, 'USD').reverse_per_unit(3, take: 0) }.to raise_error(ArgumentError, "take should be positive")
expect { Money.new(1.00, 'USD').reverse_per_unit(3, take: -1) }.to raise_error(ArgumentError, "take should be positive")
end

specify "#reverse_per_unit raises for invalid quantity" do
expect { Money.new(1.00, 'USD').reverse_per_unit(-1) }.to raise_error(ArgumentError, "quantity should be positive")
end

specify "#reverse_per_unit raises when take exceeds quantity" do
expect { Money.new(1.00, 'USD').reverse_per_unit(3, take: 4) }.to raise_error(ArgumentError, "take cannot be greater than quantity")
end
end
end