I’m trying to figure out how to use Decimal.is_decimal/1 as a guard, so I can write a function that works if the parameters are passed in as strings or decimals. (e.g. if passed in as strings, I convert to decimals first). I was thinking I could use guard clause to accomplish this but I’m getting compiler errors saying is_decimal() is not allowed. I tried creating my own guard as per below but that’s not working either. Is this something allowed by the language or should I take a different approach. Thanks!
defmodule Mondo.Guards do
defguard check_decimal(number) when %Decimal{coef: foo, exp: _, sign: _} = number
end
defmodule Mondo.Util do
def precise_exchange_rate(qty, subtotal)
when check_decimal(qty) and check_decimal(subtotal) do
if(Decimal.eq?(qty, 0), do: nil, else: Decimal.div(subtotal, qty))
end
def precise_exchange_rate(qty, subtotal) when is_bitstring(qty) and is_bitstring(subtotal) do
decimal_qty = if qty == "", do: 0, else: Decimal.new(qty)
decimal_subtotal = if subtotal == "", do: 0, else: Decimal.new(subtotal)
if(Decimal.eq?(qty, 0), do: nil, else: Decimal.div(decimal_subtotal, decimal_qty))
end
end
The canonical way to do what you’re after is to pattern match rather than use a guard. And as you’ve indentified, you can’t pattern match in guards. Using your example:
defmodule Mondo.Util do
def precise_exchange_rate(%Decimal{} = qty, %Decimal{} = subtotal) do
if(Decimal.eq?(qty, 0), do: nil, else: Decimal.div(subtotal, qty))
end
def precise_exchange_rate(qty, subtotal) when is_binary(qty) and is_binary(subtotal) do
decimal_qty = if qty == "", do: 0, else: Decimal.new(qty)
decimal_subtotal = if subtotal == "", do: 0, else: Decimal.new(subtotal)
if(Decimal.eq?(qty, 0), do: nil, else: Decimal.div(decimal_subtotal, decimal_qty))
end
end
I also suggest you use is_binary/1 rather than is_bitstring/1 since strings in Elixir are binaries (divisible by 8) whereas bit strings can be any number of bits.
And just a couple of other thoughts about composing multiple function heads for readability. Perhaps my opinion only but I find this easier to parse in my mind:
@zero Decimal.new(0)
def precise_exchange_rate("", subtotal), do: precise_exchange_rate("0", subtotal)
def precise_exchange_rate(qty, ""), do: precise_exchange_rate(qty, "0")
def precise_exchange_rate(qty, subtotal) when is_binary(qty) and is_binary(subtotal) do
precise_exchange_rate(Decimal.new(qty), Decimal.new(subtotal))
end
def precise_exchange_rate(%Decimal{} = qty, %Decimal{} = subtotal) do
if(Decimal.eq?(qty, @zero), do: nil, else: Decimal.div(subtotal, qty))
end
defmodule Foo do
require Decimal
def check_decimal(term) when Decimal.is_decimal(term) do
{:ok, term}
end
def check_decimal(term) do
{:error, term}
end
end
I think the code is self-explanatory, but if you have any questions, please ask.
I made a few changes:
swapped the arguments, to mimic the order in div/2
the function accepts integers and decimals as well.
defmodule Mondo.Util do
import Decimal, only: [is_decimal: 1]
defguardp is_valid_decimal(term) when is_binary(term) or is_integer(term) or is_decimal(term)
@spec precise_exchange_rate(subtotal :: decimal, quantity :: decimal) :: Decimal.t() | nil
when decimal: Decimal.decimal() | Decimal.t()
def precise_exchange_rate(subtotal, quantity)
when is_valid_decimal(subtotal) and is_valid_decimal(quantity) do
quantity_decimal = to_decimal(quantity)
unless Decimal.eq?(quantity_decimal, 0) do
Decimal.div(to_decimal(subtotal), quantity_decimal)
end
end
defp to_decimal(""),
do: Decimal.new(0)
defp to_decimal(term) when is_binary(term) or is_integer(term),
do: Decimal.new(term)
defp to_decimal(term) when is_decimal(term),
do: term
end