@@ -2053,6 +2053,87 @@ defmodule Money do
20532053 { div , remainder }
20542054 end
20552055
2056+ @ doc """
2057+ Proportionally spreads a given amount across the given portions with no remainder.
2058+
2059+ ## Arguments
2060+
2061+ * `amount` is any `t:Money.t/0`
2062+
2063+ * `portions` may be a list of `t:Money.t/0`, a list of numbers, or an integer
2064+ into which the `money` is spread
2065+
2066+ * `opts` is a keyword list of options, as defined by `Money.round/2`
2067+
2068+ Returns a %Money{} list the same length (or value of the integer), with the amount spread
2069+ as evenly as the currency's smallest unit allows. The result is derived as follows:
2070+
2071+ 1. Round the amount to the currency's default precision
2072+
2073+ 2. Calculate partial sums of the given portions
2074+
2075+ 3. Starting with the last portion, calculate the expected remaining amount then
2076+ subtract and round that portion's value from the current remaining amount.
2077+
2078+ eg. with [2, 1] as portions and $1 to spread, we calculate that 2/3 of the amount
2079+ should remain after `1` receives its portion, so we subtract the unrounded Money amount of
2080+ 0.666666, and we round the share to $0.33. Then $1.00 - 0.33 is the new remaining amount.
2081+ This approach avoids numerical instability by using the expected remaining amount,
2082+ rather than summing up values as they are doled out.
2083+
2084+ ## Examples
2085+
2086+ iex> Money.spread([Money.new(:usd, 10), Money.new(:usd, 1)], Money.new(:usd, 10))
2087+ [Money.new(:USD, "9.09"), Money.new(:USD, "0.91")]
2088+
2089+ iex> Money.spread([2.5, 1, 1], Money.new(:usd, "2.50"))
2090+ [Money.new(:USD, "1.39"), Money.new(:USD, "0.55"), Money.new(:USD, "0.56")]
2091+
2092+ iex> Money.spread(3, Money.new(:usd, 2))
2093+ [Money.new(:USD, "0.67"), Money.new(:USD, "0.66"), Money.new(:USD, "0.67")]
2094+
2095+ """
2096+ @ spec spread ( list ( Money . t ( ) ) | list ( number ( ) ) | integer ( ) , Money . t ( ) ) :: list ( Money . t ( ) )
2097+ def spread ( portions , amount , opts \\ [ ] )
2098+ def spread ( [ ] , _ , _ ) , do: [ ]
2099+
2100+ def spread ( portions , amount , opts ) when is_integer ( portions ) do
2101+ spread ( List . duplicate ( 1 , portions ) , amount , opts )
2102+ end
2103+
2104+ def spread ( [ h | _ ] = portions , % Money { } = amount , opts ) do
2105+ { shares , _ , _ } = recurse_spread ( portions , spread_zero ( h ) , round ( amount ) , opts )
2106+ shares
2107+ end
2108+
2109+ def spread ( _ , _ , _ ) , do: raise ( "Amount to spread must be Money.t()" )
2110+
2111+ defp recurse_spread ( [ ] , total , amount , _opts ) , do: { [ ] , amount , total }
2112+
2113+ defp recurse_spread ( [ head | tail ] , curr_sum , amount , opts ) do
2114+ partial_sum = spread_sum ( head , curr_sum )
2115+ { shares , remaining , total } = recurse_spread ( tail , partial_sum , amount , opts )
2116+
2117+ proportion_remaining = prop_remaining ( curr_sum , total )
2118+ unrounded_now_remaining = mult! ( amount , proportion_remaining )
2119+
2120+ share = sub! ( remaining , unrounded_now_remaining ) |> round ( opts )
2121+ now_remaining = sub! ( remaining , share )
2122+
2123+ { [ share | shares ] , now_remaining , total }
2124+ end
2125+
2126+ defp prop_remaining ( % Money { } = partial_sum , total ) ,
2127+ do: Decimal . div ( to_decimal ( partial_sum ) , to_decimal ( total ) )
2128+
2129+ defp prop_remaining ( partial_sum , total ) , do: partial_sum / total
2130+
2131+ defp spread_sum ( % Money { } = head , sum ) , do: add! ( head , sum )
2132+ defp spread_sum ( head , sum ) , do: head + sum
2133+
2134+ defp spread_zero ( % Money { } = head ) , do: zero ( head )
2135+ defp spread_zero ( _head ) , do: 0
2136+
20562137 @ doc """
20572138 Round a `Money` value into the acceptable range for the requested currency.
20582139
0 commit comments