Repository navigation
Add explicit decimal precision to Money #534
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
base: main
Are you sure you want to change the base?
Changes from all commits
1a42eef
48107d4
44356bf
f481b2c
3d0a48a
be381e2
8f7bec4
7edd80a
f2aa337
0f0a877
1360351
5313bee
aa129ba
a94edb6
130b8d8
47704b1
37e8ab4
1d4edaa
a0354c5
da0b0c3
eddc1e9
c694424
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -90,7 +90,13 @@ def allocate(splits, strategy = nil) | |
| amounts[order[i]][:whole_subunits] += 1 | ||
| end | ||
|
|
||
| amounts.map { |amount| Money.from_subunits(amount[:whole_subunits], currency) } | ||
| amounts.map do |amount| | ||
| Helpers.money_from_units( | ||
| amount[:whole_subunits], | ||
| currency, | ||
| decimal_precision: allocation_decimal_precision, | ||
| ) | ||
| end | ||
| end | ||
|
|
||
| # Allocates money between different parties up to the maximum amounts specified. | ||
|
|
@@ -114,30 +120,48 @@ def allocate(splits, strategy = nil) | |
| # #=> [Money.new(5), Money.new(2)] | ||
| def allocate_max_amounts(maximums) | ||
| allocation_currency = extract_currency(maximums + [__getobj__]) | ||
| maximums = maximums.map { |max| max.to_money(allocation_currency) } | ||
| maximums_total = maximums.reduce(Money.new(0, allocation_currency), :+) | ||
| money_values = maximums.grep(Money) + [__getobj__] | ||
| money_values = money_values.reject { |money| money.no_currency? && !money.explicit_decimal_precision? } | ||
| precision = if money_values.any?(&:explicit_decimal_precision?) | ||
| (money_values.map(&:decimal_precision) + [allocation_currency.minor_units]).max | ||
| end | ||
| maximums = maximums.map { |max| coerce_maximum(max, allocation_currency, precision) } | ||
| maximums_units = maximums.map do |maximum| | ||
| if precision | ||
| (maximum.value * 10**precision).floor | ||
| else | ||
| maximum.subunits | ||
| end | ||
| end | ||
| maximums_total_units = maximums_units.sum | ||
|
|
||
| splits = maximums.map do |max_amount| | ||
| next(Rational(0)) if maximums_total.zero? | ||
| Money.rational(max_amount, maximums_total) | ||
| splits = maximums_units.map do |max_units| | ||
| next(Rational(0)) if maximums_total_units.zero? | ||
| Rational(max_units, maximums_total_units) | ||
| end | ||
|
|
||
| total_allocatable = [maximums_total.subunits, subunits].min | ||
| total_allocatable = [maximums_total_units, Helpers.money_to_units(__getobj__, decimal_precision: precision)].min | ||
|
|
||
| subunits_amounts, left_over = amounts_from_splits(1, splits, total_allocatable) | ||
| subunits_amounts.map! { |amount| amount[:whole_subunits] } | ||
|
|
||
| subunits_amounts.each_with_index do |amount, index| | ||
| break if left_over <= 0 | ||
|
|
||
| max_amount = maximums[index].value * allocation_currency.subunit_to_unit | ||
| max_amount = maximums_units[index] | ||
| next if amount >= max_amount | ||
|
|
||
| left_over -= 1 | ||
| subunits_amounts[index] += 1 | ||
| end | ||
|
|
||
| subunits_amounts.map { |cents| Money.from_subunits(cents, allocation_currency) } | ||
| subunits_amounts.map do |amount| | ||
| Helpers.money_from_units( | ||
| amount, | ||
| allocation_currency, | ||
| decimal_precision: precision, | ||
| ) | ||
| end | ||
| end | ||
|
|
||
| private | ||
|
|
@@ -153,7 +177,13 @@ def extract_currency(money_array) | |
| currencies.first || NULL_CURRENCY | ||
| end | ||
|
|
||
| def amounts_from_splits(allocations, splits, subunits_to_split = subunits) | ||
| def coerce_maximum(maximum, allocation_currency, precision) | ||
| return maximum.to_money(allocation_currency) if maximum.is_a?(Money) | ||
|
|
||
| Money.new(maximum, allocation_currency, decimal_precision: precision) | ||
| end | ||
|
|
||
| def amounts_from_splits(allocations, splits, subunits_to_split = allocation_units) | ||
| raise ArgumentError, "All splits values must be of type Rational." unless all_rational?(splits) | ||
|
|
||
| left_over = subunits_to_split | ||
|
|
@@ -175,6 +205,14 @@ def all_rational?(splits) | |
| splits.all? { |split| split.is_a?(Rational) } | ||
| end | ||
|
|
||
| def allocation_decimal_precision | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The same logic, "the explicit precision, or # allocator.rb
def allocation_decimal_precision
decimal_precision if explicit_decimal_precision?
end
# money.rb
def precision_argument
decimal_precision if explicit_decimal_precision?
end
# splitter.rb
decimal_precision = @money.decimal_precision if @money.explicit_decimal_precision?
# helpers.rb
def money_to_units(money, decimal_precision: money.explicit_decimal_precision? ? money.decimal_precision : nil)Why this matters Suggested fix def explicit_decimal_precision
@decimal_precision
endThen delete # allocator.rb
def allocation_units
Helpers.money_to_units(__getobj__)
end
# money.rb, for example
Money.new(-value, currency, decimal_precision: explicit_decimal_precision) |
||
| decimal_precision if explicit_decimal_precision? | ||
| end | ||
|
|
||
| def allocation_units(money = __getobj__) | ||
| Helpers.money_to_units(money, decimal_precision: allocation_decimal_precision) | ||
| end | ||
|
|
||
| # Given a list of decimal numbers, return a list ordered by which is nearest to the next whole number. | ||
| # For instance, given inputs [1.1, 1.5, 1.9] the correct ranking is 2, 1, 0. This is because 1.9 is nearly 2. | ||
| # Note that we are not ranking by absolute size, we only care about the distance between our input number and | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,7 +5,7 @@ module Converters | |
| class Converter | ||
| def to_subunits(money) | ||
| raise ArgumentError, "money cannot be nil" if money.nil? | ||
| (money.value * subunit_to_unit(money.currency)).to_i | ||
| (money.value * subunit_to_unit(money.currency)).round.to_i | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This changes behavior for every user, not only for values with explicit precision. Example (no Money.new("1.235", "BHD").subunits(format: :legacy_dollar)
# main: 123
# this PR: 124BHD has 3 minor units, but the legacy dollar converter always uses 100. So the result used to be cut off (123), and now it is rounded (124). Why this matters Suggested fix def to_subunits(money)
raise ArgumentError, "money cannot be nil" if money.nil?
units = money.value * subunit_to_unit(money.currency)
money.explicit_decimal_precision? ? units.round.to_i : units.to_i
endNote: with rounding at construction, |
||
| end | ||
|
|
||
| def from_subunits(subunits, currency) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The maximums are rounded down (
floor, line 129), but the receiver is rounded to the nearest unit here. Sototal_allocatablecan be larger than the receiver.Example
Why this matters
allocate_max_amountsshould never hand out more than the amount being split. This is the same "money out of nothing" problem as insplit.Suggested fix
With values rounded when they are built, the start of the method gets much shorter:
A null-currency value without explicit precision returns
nilfromexplicit_decimal_precision, so the separaterejectstep is no longer needed.flooronly has to be applied to caps given as plain numbers or strings, becauseMoneycaps are already whole units.