Skip to content
Merged
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
109 changes: 106 additions & 3 deletions lib/money.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
defmodule Money do
import Kernel, except: [abs: 1, round: 1]
import Kernel, except: [abs: 1, round: 1, div: 2]

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

@spec divide(t, integer) :: [t]
@doc ~S"""
Divides up `Money` by an amount
Divides up `Money` into a list of `Money` structs split as evenly as possible.

This function splits a Money amount into the specified number of parts, returning
a list of Money structs. When the amount cannot be divided evenly, the remainder
is distributed by adding 1 to the first N Money structs in the returned list,
where N is the remainder amount. Negative denominators are supported and will flip
the sign of all amounts.

## Warning

**Memory Usage**: This function creates a list containing `denominator` number of
Money structs. For large denominators (e.g., > 100,000), this can consume
significant memory and potentially lead to out-of-memory (OOM) errors.
Consider using `Money.div/2` instead if you only need a single divided amount

## Examples

# Even division
iex> Money.divide(Money.new(100, :USD), 2)
[%Money{amount: 50, currency: :USD}, %Money{amount: 50, currency: :USD}]

# Uneven division - remainder distributed to first struct
iex> Money.divide(Money.new(101, :USD), 2)
[%Money{amount: 51, currency: :USD}, %Money{amount: 50, currency: :USD}]

# Multiple parts with remainder distribution
iex> Money.divide(Money.new(100, :USD), 3)
[%Money{amount: 34, currency: :USD}, %Money{amount: 33, currency: :USD}, %Money{amount: 33, currency: :USD}]

# Negative amounts
iex> Money.divide(Money.new(-7, :USD), 2)
[%Money{amount: -4, currency: :USD}, %Money{amount: -3, currency: :USD}]

# Negative denominators flip the sign
iex> Money.divide(Money.new(-7, :USD), -2)
[%Money{amount: 4, currency: :USD}, %Money{amount: 3, currency: :USD}]

# Large denominators can consume significant memory - USE WITH CAUTION!
# Money.divide(Money.new(1000000, :USD), 100000) # Creates 100,000 Money structs (~10MB)!

## See Also

For simple division that returns a single Money struct, use `Money.div/2`.

"""
def divide(%Money{}, 0) do
raise ArithmeticError, "division by zero"
end

def divide(%Money{amount: amount, currency: cur}, denominator) when is_integer(denominator) do
value = div(amount, denominator)
value = Kernel.div(amount, denominator)
rem = rem(amount, denominator)
do_divide(cur, value, rem, denominator, [])
end
Expand Down Expand Up @@ -551,6 +589,71 @@ defmodule Money do
defp decrement_abs(n) when n >= 0, do: n - 1
defp decrement_abs(n) when n < 0, do: n + 1

@spec div(t, integer | float | Decimal.t()) :: t
@doc """
Divides a `Money` struct by a number and returns a single `Money` struct.

The divisor can be a number (integer or float) or a Decimal. The calculation is performed
in integer arithmetic with half-up rounding.

## Examples

iex> Money.div(Money.new(100, :USD), 2)
%Money{amount: 50, currency: :USD}

iex> Money.div(Money.new(151, :USD), 2)
%Money{amount: 76, currency: :USD}

iex> Money.div(Money.new(100, :USD), 3)
%Money{amount: 33, currency: :USD}

iex> Money.div(Money.new(-151, :USD), 2)
%Money{amount: -76, currency: :USD}

iex> Money.div(Money.new(100, :USD), Decimal.new("1.5"))
%Money{amount: 67, currency: :USD}

iex> Money.div(Money.new(151, :USD), Decimal.new("2"))
%Money{amount: 76, currency: :USD}

Division by zero raises an ArithmeticError:

iex> Money.div(Money.new(100, :USD), 0)
** (ArithmeticError) division by zero

"""
def div(%Money{}, divisor) when divisor == +0.0 or divisor == -0.0 or divisor == 0 do
raise ArithmeticError, "division by zero"
end

def div(%Money{amount: amount, currency: cur}, divisor) when is_integer(divisor) do
result = amount / divisor
rounded_amount = Kernel.round(result)
Money.new(rounded_amount, cur)
end

def div(%Money{amount: amount, currency: cur}, divisor) when is_float(divisor) do
result = amount / divisor
rounded_amount = Kernel.round(result)
Money.new(rounded_amount, cur)
end

if Code.ensure_loaded?(Decimal) do
def div(%Money{amount: amount, currency: cur}, %Decimal{} = divisor) do
if Decimal.equal?(divisor, Decimal.new("0")) do
raise ArithmeticError, "division by zero"
else
result =
amount
|> Decimal.div(divisor)
|> Decimal.round(0, Decimal.Context.get().rounding)
|> Decimal.to_integer()

Money.new(result, cur)
end
end
end

@spec to_string(t, Keyword.t()) :: String.t()
@doc ~S"""
Converts a `Money` struct to a string representation
Expand Down
92 changes: 92 additions & 0 deletions test/money_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,98 @@ defmodule MoneyTest do
]
end

test "test div" do
# Basic division
assert Money.div(Money.new(100, :USD), 2) == usd(50)
assert Money.div(Money.new(200, :USD), 4) == usd(50)

# Half-up rounding with positive numbers
# 75.5 rounds up to 76
assert Money.div(Money.new(151, :USD), 2) == usd(76)
# 74.5 rounds up to 75
assert Money.div(Money.new(149, :USD), 2) == usd(75)
# 33.33... rounds to 33
assert Money.div(Money.new(100, :USD), 3) == usd(33)
# 33.66... rounds to 34
assert Money.div(Money.new(101, :USD), 3) == usd(34)
# 34.0 stays 34
assert Money.div(Money.new(102, :USD), 3) == usd(34)
# 34.33... rounds to 34
assert Money.div(Money.new(103, :USD), 3) == usd(34)
# 34.66... rounds to 35
assert Money.div(Money.new(104, :USD), 3) == usd(35)
# 35.0 stays 35
assert Money.div(Money.new(105, :USD), 3) == usd(35)

# Half-up rounding with negative numbers
# -75.5 rounds to -76
assert Money.div(Money.new(-151, :USD), 2) == usd(-76)
# -74.5 rounds to -75
assert Money.div(Money.new(-149, :USD), 2) == usd(-75)
# -33.33... rounds to -33
assert Money.div(Money.new(-100, :USD), 3) == usd(-33)
# -33.66... rounds to -34
assert Money.div(Money.new(-101, :USD), 3) == usd(-34)

# Division by negative numbers
assert Money.div(Money.new(151, :USD), -2) == usd(-76)
assert Money.div(Money.new(-151, :USD), -2) == usd(76)

# Division with float divisor
assert Money.div(Money.new(100, :USD), 2.0) == usd(50)
# 66.66... rounds to 67
assert Money.div(Money.new(100, :USD), 1.5) == usd(67)
# 40.0 stays 40
assert Money.div(Money.new(100, :USD), 2.5) == usd(40)

# Edge cases
# 0.5 rounds up to 1
assert Money.div(Money.new(1, :USD), 2) == usd(1)
# -0.5 rounds to -1
assert Money.div(Money.new(-1, :USD), 2) == usd(-1)
# 0 divided by anything is 0
assert Money.div(Money.new(0, :USD), 5) == usd(0)

# Division with Decimal divisor
assert Money.div(Money.new(100, :USD), Decimal.new("2")) == usd(50)
assert Money.div(Money.new(100, :USD), Decimal.new("2.0")) == usd(50)
# 66.66... rounds to 67 with half-up
assert Money.div(Money.new(100, :USD), Decimal.new("1.5")) == usd(67)
# 40.0 stays 40
assert Money.div(Money.new(100, :USD), Decimal.new("2.5")) == usd(40)
# Half-up rounding: 75.5 rounds up to 76
assert Money.div(Money.new(151, :USD), Decimal.new("2")) == usd(76)
# 33.33... rounds to 33
assert Money.div(Money.new(100, :USD), Decimal.new("3")) == usd(33)
# Negative numbers with Decimal
assert Money.div(Money.new(-151, :USD), Decimal.new("2")) == usd(-76)
assert Money.div(Money.new(151, :USD), Decimal.new("-2")) == usd(-76)

# Division by zero should raise ArithmeticError
assert_raise ArithmeticError, "division by zero", fn ->
Money.div(Money.new(100, :USD), 0)
end

assert_raise ArithmeticError, "division by zero", fn ->
Money.div(Money.new(100, :USD), 0.0)
end

assert_raise ArithmeticError, "division by zero", fn ->
Money.div(Money.new(100, :USD), Decimal.new("0"))
end

assert_raise ArithmeticError, "division by zero", fn ->
Money.div(Money.new(100, :USD), Decimal.new("0.0"))
end
end

test "test divide error cases" do
# Division by zero should raise ArithmeticError
assert_raise ArithmeticError, "division by zero", fn ->
Money.divide(Money.new(100, :USD), 0)
end
end

test "test to_string" do
assert Money.to_string(usd(500)) == "$5.00"
assert Money.to_string(eur(1234)) == "€12.34"
Expand Down
Loading