Skip to content

Commit b36b723

Browse files
authored
Add Money.div/2 for single-value division with rounding (#245)
* Add Money.div/2 for single-value division with rounding Also update Money.divide/2 docs and add tests for Money.div/2. * Refactor Money.div to handle all zero divisors
1 parent 3aca134 commit b36b723

File tree

2 files changed

+198
-3
lines changed

2 files changed

+198
-3
lines changed

lib/money.ex

Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
defmodule Money do
2-
import Kernel, except: [abs: 1, round: 1]
2+
import Kernel, except: [abs: 1, round: 1, div: 2]
33

44
@moduledoc """
55
Defines a `Money` struct along with convenience methods for working with currencies.
@@ -510,19 +510,57 @@ defmodule Money do
510510

511511
@spec divide(t, integer) :: [t]
512512
@doc ~S"""
513-
Divides up `Money` by an amount
513+
Divides up `Money` into a list of `Money` structs split as evenly as possible.
514+
515+
This function splits a Money amount into the specified number of parts, returning
516+
a list of Money structs. When the amount cannot be divided evenly, the remainder
517+
is distributed by adding 1 to the first N Money structs in the returned list,
518+
where N is the remainder amount. Negative denominators are supported and will flip
519+
the sign of all amounts.
520+
521+
## Warning
522+
523+
**Memory Usage**: This function creates a list containing `denominator` number of
524+
Money structs. For large denominators (e.g., > 100,000), this can consume
525+
significant memory and potentially lead to out-of-memory (OOM) errors.
526+
Consider using `Money.div/2` instead if you only need a single divided amount
514527
515528
## Examples
516529
530+
# Even division
517531
iex> Money.divide(Money.new(100, :USD), 2)
518532
[%Money{amount: 50, currency: :USD}, %Money{amount: 50, currency: :USD}]
519533
534+
# Uneven division - remainder distributed to first struct
520535
iex> Money.divide(Money.new(101, :USD), 2)
521536
[%Money{amount: 51, currency: :USD}, %Money{amount: 50, currency: :USD}]
522537
538+
# Multiple parts with remainder distribution
539+
iex> Money.divide(Money.new(100, :USD), 3)
540+
[%Money{amount: 34, currency: :USD}, %Money{amount: 33, currency: :USD}, %Money{amount: 33, currency: :USD}]
541+
542+
# Negative amounts
543+
iex> Money.divide(Money.new(-7, :USD), 2)
544+
[%Money{amount: -4, currency: :USD}, %Money{amount: -3, currency: :USD}]
545+
546+
# Negative denominators flip the sign
547+
iex> Money.divide(Money.new(-7, :USD), -2)
548+
[%Money{amount: 4, currency: :USD}, %Money{amount: 3, currency: :USD}]
549+
550+
# Large denominators can consume significant memory - USE WITH CAUTION!
551+
# Money.divide(Money.new(1000000, :USD), 100000) # Creates 100,000 Money structs (~10MB)!
552+
553+
## See Also
554+
555+
For simple division that returns a single Money struct, use `Money.div/2`.
556+
523557
"""
558+
def divide(%Money{}, 0) do
559+
raise ArithmeticError, "division by zero"
560+
end
561+
524562
def divide(%Money{amount: amount, currency: cur}, denominator) when is_integer(denominator) do
525-
value = div(amount, denominator)
563+
value = Kernel.div(amount, denominator)
526564
rem = rem(amount, denominator)
527565
do_divide(cur, value, rem, denominator, [])
528566
end
@@ -551,6 +589,71 @@ defmodule Money do
551589
defp decrement_abs(n) when n >= 0, do: n - 1
552590
defp decrement_abs(n) when n < 0, do: n + 1
553591

592+
@spec div(t, integer | float | Decimal.t()) :: t
593+
@doc """
594+
Divides a `Money` struct by a number and returns a single `Money` struct.
595+
596+
The divisor can be a number (integer or float) or a Decimal. The calculation is performed
597+
in integer arithmetic with half-up rounding.
598+
599+
## Examples
600+
601+
iex> Money.div(Money.new(100, :USD), 2)
602+
%Money{amount: 50, currency: :USD}
603+
604+
iex> Money.div(Money.new(151, :USD), 2)
605+
%Money{amount: 76, currency: :USD}
606+
607+
iex> Money.div(Money.new(100, :USD), 3)
608+
%Money{amount: 33, currency: :USD}
609+
610+
iex> Money.div(Money.new(-151, :USD), 2)
611+
%Money{amount: -76, currency: :USD}
612+
613+
iex> Money.div(Money.new(100, :USD), Decimal.new("1.5"))
614+
%Money{amount: 67, currency: :USD}
615+
616+
iex> Money.div(Money.new(151, :USD), Decimal.new("2"))
617+
%Money{amount: 76, currency: :USD}
618+
619+
Division by zero raises an ArithmeticError:
620+
621+
iex> Money.div(Money.new(100, :USD), 0)
622+
** (ArithmeticError) division by zero
623+
624+
"""
625+
def div(%Money{}, divisor) when divisor == +0.0 or divisor == -0.0 or divisor == 0 do
626+
raise ArithmeticError, "division by zero"
627+
end
628+
629+
def div(%Money{amount: amount, currency: cur}, divisor) when is_integer(divisor) do
630+
result = amount / divisor
631+
rounded_amount = Kernel.round(result)
632+
Money.new(rounded_amount, cur)
633+
end
634+
635+
def div(%Money{amount: amount, currency: cur}, divisor) when is_float(divisor) do
636+
result = amount / divisor
637+
rounded_amount = Kernel.round(result)
638+
Money.new(rounded_amount, cur)
639+
end
640+
641+
if Code.ensure_loaded?(Decimal) do
642+
def div(%Money{amount: amount, currency: cur}, %Decimal{} = divisor) do
643+
if Decimal.equal?(divisor, Decimal.new("0")) do
644+
raise ArithmeticError, "division by zero"
645+
else
646+
result =
647+
amount
648+
|> Decimal.div(divisor)
649+
|> Decimal.round(0, Decimal.Context.get().rounding)
650+
|> Decimal.to_integer()
651+
652+
Money.new(result, cur)
653+
end
654+
end
655+
end
656+
554657
@spec to_string(t, Keyword.t()) :: String.t()
555658
@doc ~S"""
556659
Converts a `Money` struct to a string representation

test/money_test.exs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,98 @@ defmodule MoneyTest do
257257
]
258258
end
259259

260+
test "test div" do
261+
# Basic division
262+
assert Money.div(Money.new(100, :USD), 2) == usd(50)
263+
assert Money.div(Money.new(200, :USD), 4) == usd(50)
264+
265+
# Half-up rounding with positive numbers
266+
# 75.5 rounds up to 76
267+
assert Money.div(Money.new(151, :USD), 2) == usd(76)
268+
# 74.5 rounds up to 75
269+
assert Money.div(Money.new(149, :USD), 2) == usd(75)
270+
# 33.33... rounds to 33
271+
assert Money.div(Money.new(100, :USD), 3) == usd(33)
272+
# 33.66... rounds to 34
273+
assert Money.div(Money.new(101, :USD), 3) == usd(34)
274+
# 34.0 stays 34
275+
assert Money.div(Money.new(102, :USD), 3) == usd(34)
276+
# 34.33... rounds to 34
277+
assert Money.div(Money.new(103, :USD), 3) == usd(34)
278+
# 34.66... rounds to 35
279+
assert Money.div(Money.new(104, :USD), 3) == usd(35)
280+
# 35.0 stays 35
281+
assert Money.div(Money.new(105, :USD), 3) == usd(35)
282+
283+
# Half-up rounding with negative numbers
284+
# -75.5 rounds to -76
285+
assert Money.div(Money.new(-151, :USD), 2) == usd(-76)
286+
# -74.5 rounds to -75
287+
assert Money.div(Money.new(-149, :USD), 2) == usd(-75)
288+
# -33.33... rounds to -33
289+
assert Money.div(Money.new(-100, :USD), 3) == usd(-33)
290+
# -33.66... rounds to -34
291+
assert Money.div(Money.new(-101, :USD), 3) == usd(-34)
292+
293+
# Division by negative numbers
294+
assert Money.div(Money.new(151, :USD), -2) == usd(-76)
295+
assert Money.div(Money.new(-151, :USD), -2) == usd(76)
296+
297+
# Division with float divisor
298+
assert Money.div(Money.new(100, :USD), 2.0) == usd(50)
299+
# 66.66... rounds to 67
300+
assert Money.div(Money.new(100, :USD), 1.5) == usd(67)
301+
# 40.0 stays 40
302+
assert Money.div(Money.new(100, :USD), 2.5) == usd(40)
303+
304+
# Edge cases
305+
# 0.5 rounds up to 1
306+
assert Money.div(Money.new(1, :USD), 2) == usd(1)
307+
# -0.5 rounds to -1
308+
assert Money.div(Money.new(-1, :USD), 2) == usd(-1)
309+
# 0 divided by anything is 0
310+
assert Money.div(Money.new(0, :USD), 5) == usd(0)
311+
312+
# Division with Decimal divisor
313+
assert Money.div(Money.new(100, :USD), Decimal.new("2")) == usd(50)
314+
assert Money.div(Money.new(100, :USD), Decimal.new("2.0")) == usd(50)
315+
# 66.66... rounds to 67 with half-up
316+
assert Money.div(Money.new(100, :USD), Decimal.new("1.5")) == usd(67)
317+
# 40.0 stays 40
318+
assert Money.div(Money.new(100, :USD), Decimal.new("2.5")) == usd(40)
319+
# Half-up rounding: 75.5 rounds up to 76
320+
assert Money.div(Money.new(151, :USD), Decimal.new("2")) == usd(76)
321+
# 33.33... rounds to 33
322+
assert Money.div(Money.new(100, :USD), Decimal.new("3")) == usd(33)
323+
# Negative numbers with Decimal
324+
assert Money.div(Money.new(-151, :USD), Decimal.new("2")) == usd(-76)
325+
assert Money.div(Money.new(151, :USD), Decimal.new("-2")) == usd(-76)
326+
327+
# Division by zero should raise ArithmeticError
328+
assert_raise ArithmeticError, "division by zero", fn ->
329+
Money.div(Money.new(100, :USD), 0)
330+
end
331+
332+
assert_raise ArithmeticError, "division by zero", fn ->
333+
Money.div(Money.new(100, :USD), 0.0)
334+
end
335+
336+
assert_raise ArithmeticError, "division by zero", fn ->
337+
Money.div(Money.new(100, :USD), Decimal.new("0"))
338+
end
339+
340+
assert_raise ArithmeticError, "division by zero", fn ->
341+
Money.div(Money.new(100, :USD), Decimal.new("0.0"))
342+
end
343+
end
344+
345+
test "test divide error cases" do
346+
# Division by zero should raise ArithmeticError
347+
assert_raise ArithmeticError, "division by zero", fn ->
348+
Money.divide(Money.new(100, :USD), 0)
349+
end
350+
end
351+
260352
test "test to_string" do
261353
assert Money.to_string(usd(500)) == "$5.00"
262354
assert Money.to_string(eur(1234)) == "€12.34"

0 commit comments

Comments
 (0)