From f95602831abcb5e2a85bb7bd473fcdc45a43a85a Mon Sep 17 00:00:00 2001 From: Till Grosch Date: Tue, 5 Mar 2024 22:03:06 +0100 Subject: [PATCH 1/3] Improves Precision and Simplifies Allocation Logic - Converts all numeric parameters to BigDecimal when any is a BigDecimal, Float, or Rational, enhancing calculation precision. - Removes obsolete round-robin penny distribution for a more straightforward allocation approach. --- lib/money/money/allocation.rb | 56 +++++++++++++++++------------------ 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/lib/money/money/allocation.rb b/lib/money/money/allocation.rb index 6e47b2d279..542bc8cc92 100644 --- a/lib/money/money/allocation.rb +++ b/lib/money/money/allocation.rb @@ -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 — allocates the amounts proportionally to the given array + # @param amount [Numeric] The total amount to be allocated. + # @param parts [Numeric, Array] 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] 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) @@ -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 @@ -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 From d3c8f19845116b17266b0dddcb83831c2364b3af Mon Sep 17 00:00:00 2001 From: Till Grosch Date: Tue, 5 Mar 2024 23:10:50 +0100 Subject: [PATCH 2/3] Adds author --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 78e72b95d6..636e48a27a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -123,6 +123,7 @@ Thomas E Enebo Thomas Weymuth Ticean Bennett Tien Nguyen +Till Grosch Tim Hart Tim Krins Tobias Luetke From 4dfce9e551df7e3657b9d2bd881ad51d86bce052 Mon Sep 17 00:00:00 2001 From: Till Grosch Date: Wed, 6 Mar 2024 09:49:30 +0100 Subject: [PATCH 3/3] Fixes spec due to regression in df6525f3 --- spec/money/formatting_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/money/formatting_spec.rb b/spec/money/formatting_spec.rb index 1842195d95..d8da80c77f 100644 --- a/spec/money/formatting_spec.rb +++ b/spec/money/formatting_spec.rb @@ -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