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

Enable customizable precision for allocations #1114

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
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