defmodule Rujira.Math do
@moduledoc """
Math utilities for Rujira financial calculations
"""
@doc """
Parses any value to an integer. `nil` passes through.
Returns `{:ok, integer}`, `{:ok, nil}`, or `{:error, :invalid_integer}`.
"""
@spec to_integer(nil | integer() | String.t()) ::
{:ok, integer() | nil} | {:error, :invalid_integer}
def to_integer(nil), do: {:ok, nil}
def to_integer(value) when is_integer(value), do: {:ok, value}
def to_integer(value) when is_binary(value) do
case Integer.parse(value) do
{n, ""} -> {:ok, n}
_ -> {:error, :invalid_integer}
end
end
def to_integer(_), do: {:error, :invalid_integer}
@doc """
Parses any value to a Decimal. `nil` passes through.
Returns `{:ok, Decimal.t}`, `{:ok, nil}`, or `{:error, :invalid_decimal}`.
"""
@spec to_decimal(nil | integer() | float() | String.t() | Decimal.t()) ::
{:ok, Decimal.t() | nil} | {:error, :invalid_decimal}
def to_decimal(nil), do: {:ok, nil}
def to_decimal(%Decimal{} = value), do: {:ok, value}
def to_decimal(value) when is_integer(value), do: {:ok, Decimal.new(value)}
def to_decimal(value) when is_float(value), do: {:ok, Decimal.from_float(value)}
def to_decimal(value) when is_binary(value) do
case Decimal.parse(value) do
{d, ""} -> {:ok, d}
_ -> {:error, :invalid_decimal}
end
end
def to_decimal(_), do: {:error, :invalid_decimal}
@doc """
Multiply two numbers and round down to integer
"""
@spec mul_floor(number() | Decimal.t(), number() | Decimal.t()) :: integer()
def mul_floor(a, b) do
Decimal.new(a)
|> Decimal.mult(Decimal.new(b))
|> Decimal.round(0, :floor)
|> Decimal.to_integer()
end
@doc """
Divide two numbers and round down to integer
"""
@spec div_floor(number() | Decimal.t(), number() | Decimal.t()) :: integer()
def div_floor(a, b) do
Decimal.new(a)
|> Decimal.div(Decimal.new(b))
|> Decimal.round(0, :floor)
|> Decimal.to_integer()
end
@doc """
Safe division that returns 0 if divisor is zero
"""
@spec safe_div(number() | Decimal.t(), number() | Decimal.t()) :: Decimal.t()
def safe_div(a, b) do
b_decimal = Decimal.new(b)
if Decimal.eq?(b_decimal, Decimal.new(0)) do
Decimal.new(0)
else
Decimal.div(Decimal.new(a), b_decimal)
end
end
@doc """
Convert number from one decimal precision to another
"""
@spec normalize(number() | float() | Decimal.t(), integer(), integer()) :: Decimal.t()
def normalize(a, from \\ 0, to \\ Rujira.Amount.decimals())
def normalize(a, from, to) when is_float(a),
do: do_normalize(Decimal.from_float(a), from, to)
def normalize(a, from, to),
do: do_normalize(Decimal.new(a), from, to)
defp do_normalize(a, from, to) when to >= from do
Decimal.mult(a, Decimal.new(10 ** (to - from)))
end
defp do_normalize(a, from, to) do
Decimal.mult(a, Decimal.from_float(10 ** (to - from)))
end
@doc """
Round down to integer using floor
"""
@spec floor(number() | Decimal.t()) :: integer()
def floor(a) do
Decimal.new(a)
|> Decimal.round(0, :floor)
|> Decimal.to_integer()
end
@doc """
Average of two numbers
"""
@spec avg(number() | Decimal.t(), number() | Decimal.t()) :: Decimal.t()
def avg(a, b) do
Decimal.div(Decimal.add(Decimal.new(a), Decimal.new(b)), 2)
end
end