Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improves Precision and Simplifies Allocation Logic #1082

Merged
merged 3 commits into from
Mar 6, 2024
Merged
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
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ Thomas E Enebo
Thomas Weymuth
Ticean Bennett
Tien Nguyen
Till Grosch
Tim Hart
Tim Krins
Tobias Luetke
Expand Down
56 changes: 28 additions & 28 deletions lib/money/money/allocation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@

class Money
class Allocation
# Splits a given amount in parts without losing pennies.
# The left-over pennies will be distributed round-robin amongst the parts. This means that
# parts listed first will likely receive more pennies than the ones listed later.
# Splits a given amount in parts. The allocation is based on the parts' proportions
# or evenly if parts are numerically specified.
#
# The results should always add up to the original amount.
#
# The parts can be specified as:
# Numeric — performs the split between a given number of parties evenely
# Array<Numeric> — allocates the amounts proportionally to the given array
# @param amount [Numeric] The total amount to be allocated.
# @param parts [Numeric, Array<Numeric>] Number of parts to split into or an array (proportions for allocation)
# @param whole_amounts [Boolean] Specifies whether to allocate whole amounts only. Defaults to true.
#
# @return [Array<Numeric>] An array containing the allocated amounts.
# @raise [ArgumentError] If parts is empty or not provided.
def self.generate(amount, parts, whole_amounts = true)
parts = if parts.is_a?(Numeric)
Array.new(parts, 1)
Expand All @@ -21,7 +22,12 @@ def self.generate(amount, parts, whole_amounts = true)
parts.dup
end

raise ArgumentError, 'need at least one party' if parts.empty?
raise ArgumentError, 'need at least one part' if parts.empty?

if [amount, *parts].any? { |i| i.is_a?(BigDecimal) || i.is_a?(Float) || i.is_a?(Rational) }
amount = convert_to_big_decimal(amount)
parts.map! { |p| convert_to_big_decimal(p) }
end

result = []
remaining_amount = amount
Expand All @@ -40,29 +46,23 @@ def self.generate(amount, parts, whole_amounts = true)
remaining_amount -= current_split
end

## round-robin allocation of any remaining pennies
if result.size > 0
while remaining_amount != 0
index = 0

amount_to_distribute = [1, remaining_amount.abs].min

if remaining_amount > 0
result[index] += amount_to_distribute
remaining_amount -= amount_to_distribute
else
result[index] -= amount_to_distribute
remaining_amount += amount_to_distribute
end
result
end

index += 1
if index > result.size
index = 0
end
end
# Converts a given number to BigDecimal.
# This method supports inputs of BigDecimal, Rational, and other numeric types by ensuring they are all returned
# as BigDecimal instances for consistent handling.
#
# @param number [Numeric, BigDecimal, Rational] The number to convert.
# @return [BigDecimal] The converted number as a BigDecimal.
def self.convert_to_big_decimal(number)
if number.is_a? BigDecimal
number
elsif number.is_a? Rational
BigDecimal(number.to_f.to_s)
else
BigDecimal(number.to_s)
end

result
end
end
end
2 changes: 1 addition & 1 deletion spec/money/formatting_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -759,7 +759,7 @@

context 'when symbol_position is passed' do
it "inserts currency symbol before the amount when set to :before" do
expect(Money.new(100_00, 'CHF').format(symbol_position: :before)).to eq "CHF 100.00"
expect(Money.new(100_00, 'CHF').format(symbol_position: :before)).to eq "CHF100.00"
end

it "inserts currency symbol after the amount when set to :after" do
Expand Down
Loading