diff --git a/AUTHORS b/AUTHORS index e8b6a56cbc..9da6cb8d58 100644 --- a/AUTHORS +++ b/AUTHORS @@ -141,3 +141,4 @@ Zubin Henner Бродяной Александр Nicolay Hvidsten Simon Neutert +Andrei Andriichuk diff --git a/CHANGELOG.md b/CHANGELOG.md index fa25dd0921..763ae49cd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - **Potential breaking change**: Fix USDC decimals places from 2 to 6 - Fix typo in ILS currency +- Refactor `Money::Allocation.generate` method: Rename `whole_amounts` to `rounding_mode` for flexible rounding ## 6.19.0 diff --git a/lib/money/money/allocation.rb b/lib/money/money/allocation.rb index 768147f7cc..dd89770c79 100644 --- a/lib/money/money/allocation.rb +++ b/lib/money/money/allocation.rb @@ -2,10 +2,10 @@ class Money class Allocation - # Splits a given amount in parts. The allocation is based on the parts' proportions - # or evenly if parts are numerically specified. + # Allocates a specified amount into parts based on their proportions or distributes + # it evenly when the number of parts is specified numerically. # - # The results should always add up to the original amount. + # The total of the allocated amounts will always equal the original amount. # # The parts can be specified as: # Numeric — performs the split between a given number of parties evenly @@ -13,11 +13,12 @@ class Allocation # # @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. + # @param rounding_mode [Boolean, Integer] Specifies the rounding mode. If true, rounds to whole amounts. + # If an integer, rounds to that decimal precision. Defaults to true (whole amounts). # # @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) + def self.generate(amount, parts, rounding_mode = true) parts = if parts.is_a?(Numeric) Array.new(parts, 1) elsif parts.all?(&:zero?) @@ -35,6 +36,8 @@ def self.generate(amount, parts, whole_amounts = true) result = [] remaining_amount = amount + round_to_whole = rounding_mode.is_a?(TrueClass) + round_to_precision = rounding_mode.is_a?(Integer) until parts.empty? do parts_sum = parts.inject(0, :+) @@ -43,7 +46,14 @@ def self.generate(amount, parts, whole_amounts = true) current_split = 0 if parts_sum > 0 current_split = remaining_amount * part / parts_sum - current_split = current_split.truncate if whole_amounts + current_split = + if round_to_whole + current_split.truncate + elsif round_to_precision + current_split.round(rounding_mode) + else + current_split + end end result.unshift current_split diff --git a/sig/lib/money/money/allocation.rbs b/sig/lib/money/money/allocation.rbs index 4de5045c77..c796fe8a1a 100644 --- a/sig/lib/money/money/allocation.rbs +++ b/sig/lib/money/money/allocation.rbs @@ -7,9 +7,16 @@ class Money # 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 + # Numeric — performs the split between a given number of parties evenly # Array — allocates the amounts proportionally to the given array # - def self.generate: (untyped amount, (Numeric | Array[Numeric]) parts, ?bool whole_amounts) -> untyped + # @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 rounding_mode [bool | Integer] Specifies the rounding mode. If true, rounds to whole numbers. + # If an integer, rounds to that decimal precision. Defaults to true (whole numbers). + # + # @return [Array[Numeric]] An array containing the allocated amounts. + # @raise [ArgumentError] If parts is empty or not provided. + def self.generate: (Numeric amount, (Numeric | Array[Numeric]) parts, ?(bool | Integer) rounding_mode) -> Array[Numeric] end -end \ No newline at end of file +end diff --git a/spec/money/allocation_spec.rb b/spec/money/allocation_spec.rb index 215c95c392..c82c194c19 100644 --- a/spec/money/allocation_spec.rb +++ b/spec/money/allocation_spec.rb @@ -1,8 +1,8 @@ # encoding: utf-8 describe Money::Allocation do - describe 'given number as argument' do - it 'raises an error when invalid argument is given' do + describe 'given number as argument' do + it 'raises an error when invalid argument is given' do expect { described_class.generate(100, 0) }.to raise_error(ArgumentError) expect { described_class.generate(100, -1) }.to raise_error(ArgumentError) end @@ -151,5 +151,39 @@ expect(result.reduce(&:+)).to eq(amount) expect(result).to eq([-61566, -61565, -61565, -61565, -61565, -61565, -61565, -60953, -52091, -52091, -52091, -52091]) end + + context 'when specified precision' do + let(:amount) { 246.4 } + let(:allocations) do + [ + 81.29, 81.29, 81.29, 81.29, + 234.8, 234.8, 234.8, 234.8, + 90.36, 90.36, 90.36, 90.36, + 90.36, 90.36, 90.36, 90.36, + 90.36, 90.36, 90.36, 90.36, + 90.36, 90.36, 90.36, 90.36, + 90.36, 90.36, 90.36, 90.36, + 90.36 + ] + end + + it 'allocates with required precision' do + result = described_class.generate(amount, allocations, 16) + expect(result.reduce(&:+)).to eq(amount) + + expected = %w[ + 6.3347130857200688 6.3347130857200689 6.3347130857200688 6.3347130857200688 + 18.2973383260803562 18.2973383260803563 18.2973383260803563 18.2973383260803563 + 7.0415140167999191 7.041514016799919 7.0415140167999191 7.041514016799919 + 7.0415140167999191 7.041514016799919 7.0415140167999191 7.041514016799919 + 7.0415140167999191 7.041514016799919 7.0415140167999191 7.041514016799919 + 7.041514016799919 7.041514016799919 7.041514016799919 7.041514016799919 + 7.041514016799919 7.041514016799919 7.041514016799919 7.041514016799919 + 7.041514016799919 + ].map { |element| BigDecimal(element) } + + expect(result).to eq(expected) + end + end end end