lib/complex.ex

defmodule Complex do
  @moduledoc """
  Elixir library implementing complex numbers and math.

  The library adds new type of `t:complex/0` numbers and basic math operations for them. Complex numbers can also interact with integers and floats. Actually this library expands existing functions, so they can work with complex numbers too. Number operations available:

  * addition
  * subtraction
  * multiplication
  * division
  * exponentiation
  * absolute value
  * trigonometric functions

  ## Some examples

      iex> use Complex
      Complex

      iex> ~o(1+2i)
      1.0+2.0i

      iex> ~o(1+2i) * ib
      -2.0+1.0i

      iex> Complex.Trig.sin(~o(-11-2i))
      3.762158846210887-0.016051388809949604i

      iex> to_polar(~o(7-9i))
      {11.40175425099138, -0.9097531579442097}

      iex> ~o(1+2i) ** ~o(3+4i)
      0.129009594074467+0.03392409290517014i

  """

  defmacro __using__(_opts) do
    quote do
      import Kernel, except: [-: 1, +: 2, -: 2, *: 2, /: 2, **: 2, abs: 1]
      import ComplexMath

      import Complex,
        only: [is_complex: 1, sigil_o: 2]
    end
  end

  defstruct [:re, :im]

  @type complex() :: %Complex{re: float(), im: float()}
  @typedoc false
  @type operator() :: :+ | :- | :* | :/ | :**

  @doc """
  Returns `true` if `term` is a complex number, otherwise returns `false`.

  Allowed in guard tests.
  """
  defguard is_complex(term) when is_struct(term, Complex)

  @doc """
  Parses a string into a complex number.

  If successful, returns either a `t:complex/0` or `t:float/0`; otherwise returns `:error`.

  ## Examples

      iex> Complex.parse "1+13i"
      1.0+13.0i

      iex> Complex.parse "2.3-1i"
      2.3-1.0i

      iex> Complex.parse "42"
      :error

  """
  @spec parse(String.t()) :: complex() | float() | :error
  def parse(string) do
    case Regex.run(~r/([\+\-]?[0-9\.]+)((?:\+|-)[0-9\.]+)i/, string) do
      [_, re, im] ->
        case Float.parse(im) do
          {0.0, _} ->
            {float, _} = Float.parse(re)
            float

          {im, _} ->
            {re, _} = Float.parse(re)
            %Complex{re: re, im: im}
        end

      _ ->
        :error
    end
  end

  @doc """
  Handles the sigil `~o` for complex numbers.

  It returns a `t:complex/0` or `t:float/0`.

  ## Examples

      iex> ~o(1+4i)
      1.0+4.0i

      iex> ~o(-3.1+5i)
      -3.1+5.0i

  """
  @spec sigil_o(String.t(), list()) :: complex() | float() | :error
  def sigil_o(string, _modifiers), do: parse(string)

  @doc """
  Imaginary unit.

  When no arguments passed, returns imaginary unit; otherwise returns i*b.

  ## Examples

      iex> ib
      i

      iex> ib(-5)
      -5i

  """
  @spec ib(number()) :: %Complex{im: 1, re: number()}
  def ib(b \\ 1), do: %Complex{re: 0, im: b}

  @doc """
  Complex conjugate.

  Returns the complex conjugate of a complex number.

  ## Examples

      iex> conj(~o(5+7i))
      5.0-7.0i

      iex> conj(~o(5-7i))
      5.0+7.0i

  """
  @spec conj(complex()) :: complex()
  def conj(z) when is_complex(z) do
    cond do
      z.im > 0 ->
        %Complex{re: z.re, im: -z.im}

      z.im < 0 ->
        %Complex{re: z.re, im: abs(z.im)}
    end
  end

  @doc """
  Polar coordinates.

  Returns polar coordinates of a complex number as `{radius, angle}`.

  ## Examples

      iex> to_polar(~o(7-9i))
      {11.40175425099138, -0.9097531579442097}

  """
  @spec to_polar(complex()) :: {float, float}
  def to_polar(z) when is_complex(z) do
    a = z.re
    b = z.im
    r = :math.sqrt(a ** 2 + b ** 2)
    theta = :math.atan(b / a)

    {r, theta}
  end

  defp polar_power(z, power) when is_complex(z) do
    {r, theta} = to_polar(z)

    ComplexMath.*(
      r ** power,
      ComplexMath.+(
        :math.cos(power * theta),
        ComplexMath.*(:math.sin(power * theta), ib())
      )
    )
  end

  @doc false
  @spec op(number(), number(), operator()) :: number()
  def op(a, b, op) when is_number(a) and is_number(b) do
    {res, _} =
      [inspect(a), op, inspect(b)]
      |> Enum.join(" ")
      |> Code.eval_string()

    res
  end

  @spec op(number(), complex(), operator()) :: complex() | number()
  def op(a, z, op) when is_number(a) and is_complex(z) do
    case op do
      :+ ->
        %Complex{re: a + z.re, im: z.im}

      :- ->
        %Complex{re: a - z.re, im: z.im}

      :* ->
        %Complex{re: a * z.re, im: a * z.im}

      :/ ->
        z = %Complex{re: a * z.re, im: a * -z.im}
        x = z.re ** 2 + z.im ** 2
        %Complex{re: z.re / x, im: z.im / x}

      :** ->
        ComplexMath.*(
          a ** z.re,
          ComplexMath.+(
            :math.cos(z.im * :math.log(abs(a))),
            ib(:math.sin(z.im * :math.log(abs(a))))
          )
        )
    end
    |> result()
  end

  @spec op(complex(), number(), operator()) :: complex() | number()
  def op(z, b, op) when is_complex(z) and is_number(b) do
    case op do
      :+ ->
        %Complex{re: z.re + b, im: z.im}

      :- ->
        %Complex{re: z.re - b, im: z.im}

      :* ->
        %Complex{re: z.re * b, im: z.im * b}

      :/ ->
        %Complex{re: z.re / b, im: z.im / b}

      :** when not is_float(b) ->
        cond do
          b > 0 ->
            Enum.reduce(1..b, 1, fn _, res ->
              ComplexMath.*(res, z)
            end)

          b < 0 ->
            ComplexMath./(
              1,
              Enum.reduce(1..abs(b), 1, fn _, res ->
                ComplexMath.*(res, z)
              end)
            )

          true ->
            1
        end

      :** ->
        polar_power(z, b)
    end
    |> result()
  end

  @spec op(complex(), complex(), operator()) :: complex() | number()
  def op(z1, z2, op) when is_complex(z1) and is_complex(z2) do
    case op do
      :+ ->
        %Complex{re: z1.re + z2.re, im: z1.im + z2.im}

      :- ->
        %Complex{re: z1.re - z2.re, im: z1.im - z2.im}

      :* ->
        %Complex{
          re: z1.re * z2.re - z1.im * z2.im,
          im: z1.re * z2.im + z1.im * z2.re
        }

      :/ ->
        %Complex{
          re: (z1.re * z2.re + z1.im * z2.im) / (z2.re ** 2 + z2.im ** 2),
          im: (z1.im * z2.re - z1.re * z2.im) / (z2.re ** 2 + z2.im ** 2)
        }

      :** ->
        {r, theta} = to_polar(z1)

        power =
          ComplexMath.+(
            ComplexMath.*(:math.log(r), z2),
            ComplexMath.*(ib(theta), z2)
          )

        ComplexMath.**(:math.exp(1), power)
    end
    |> result()
  end

  @doc false
  @spec op(complex(), :abs) :: number()
  def op(z, :abs) when is_complex(z), do: :math.sqrt(op(z, conj(z), :*))
  @spec op(number(), :abs) :: number()
  def op(a, :abs) when is_number(a), do: abs(a)

  defp result(z) do
    if z.im == 0 do
      z.re
    else
      z
    end
  end
end