Skip to content

Commit

Permalink
Refactor Money::Allocation.generate: allow flexible rounding
Browse files Browse the repository at this point in the history
  • Loading branch information
faraquet committed Oct 11, 2024
1 parent 870bac4 commit b1cd3d0
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 11 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,4 @@ Zubin Henner
Бродяной Александр
Nicolay Hvidsten
Simon Neutert
Andrei Andriichuk
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
22 changes: 16 additions & 6 deletions lib/money/money/allocation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,23 @@

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
# 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.
# @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<Numeric>] 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?)
Expand All @@ -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, :+)
Expand All @@ -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
Expand Down
13 changes: 10 additions & 3 deletions sig/lib/money/money/allocation.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Numeric> — 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<Numeric>] 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
end
38 changes: 36 additions & 2 deletions spec/money/allocation_spec.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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

0 comments on commit b1cd3d0

Please sign in to comment.