Skip to main content

lib/rujira/math.ex

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