lib/matrix_reloaded/vector.ex

defmodule MatrixReloaded.Vector do
  @moduledoc """
  Provides a set of functions to work with vectors.

  Mostly functions is written for a row vectors. So if you'll need a similar
  functionality even for a column vectors you can use `transpose` function
  on row vector.
  """
  alias MatrixReloaded.Matrix

  @type t :: [number]
  @type column() :: [[number]]
  @type subvector :: number | t()

  @doc """
  Create a row vector of the specified size. Default values of vector
  is set to `0`. This value can be changed.

  Returns list of numbers.

  ## Examples

      iex> MatrixReloaded.Vector.row(4)
      [0, 0, 0, 0]

      iex> MatrixReloaded.Vector.row(4, 3.9)
      [3.9, 3.9, 3.9, 3.9]

  """

  @spec row(pos_integer, number) :: t()
  def row(size, val \\ 0) do
    List.duplicate(val, size)
  end

  @doc """
  Create a column vector of the specified size. Default values of vector
  is set to `0`. This value can be changed.

  Returns list of list number.

  ## Examples

      iex> MatrixReloaded.Vector.col(3)
      [[0], [0], [0]]

      iex> MatrixReloaded.Vector.col(3, 4)
      [[4], [4], [4]]

  """

  @spec col(pos_integer, number) :: column()
  def col(size, val \\ 0) do
    val |> List.duplicate(size) |> Enum.chunk_every(1)
  end

  @doc """
  Convert (transpose) a row vector to column and vice versa.

  ## Examples

      iex> MatrixReloaded.Vector.transpose([1, 2, 3])
      [[1], [2], [3]]

      iex(23)> MatrixReloaded.Vector.transpose([[1], [2], [3]])
      [1, 2, 3]

  """
  @spec transpose(t() | column()) :: column() | t()
  def transpose([hd | _] = vec) when is_list(hd) do
    List.flatten(vec)
  end

  def transpose(vec) do
    Enum.chunk_every(vec, 1)
  end

  @doc """
  Create row vector of alternating sequence of numbers.

  ## Examples

      iex> MatrixReloaded.Vector.row(5) |> MatrixReloaded.Vector.alternate_seq(1)
      [1, 0, 1, 0, 1]

      iex> MatrixReloaded.Vector.row(7) |> MatrixReloaded.Vector.alternate_seq(1, 3)
      [1, 0, 0, 1, 0, 0, 1]

  """

  @spec alternate_seq(t(), number, pos_integer) :: t()
  def alternate_seq(vec, val, step \\ 2) do
    Enum.map_every(vec, step, fn x -> x + val end)
  end

  @doc """
  Addition of two a row vectors. These two vectors must have a same size.
  Otherwise you get an error message.

  Returns result, it means either tuple of `{:ok, vector}` or `{:error, "msg"}`.

  ## Examples

      iex> MatrixReloaded.Vector.add([1, 2, 3], [4, 5, 6])
      {:ok, [5, 7, 9]}

  """

  @spec add(t(), t()) :: Result.t(String.t(), t())
  def add([hd1 | _], [hd2 | _]) when is_list(hd1) or is_list(hd2) do
    Result.error("Vectors must be row type!")
  end

  def add(vec1, vec2) do
    if size(vec1) == size(vec2) do
      [vec1, vec2]
      |> List.zip()
      |> Enum.map(fn {x, y} -> x + y end)
      |> Result.ok()
    else
      Result.error("Size both vectors must be same!")
    end
  end

  @doc """
  Subtraction of two a row vectors. These two vectors must have a same size.
  Otherwise you get an error message.

  Returns result, it means either tuple of `{:ok, vector}` or `{:error, "msg"}`.

  ## Examples

      iex> MatrixReloaded.Vector.sub([1, 2, 3], [4, 5, 6])
      {:ok, [-3, -3, -3]}

  """

  @spec sub(t(), t()) :: Result.t(String.t(), t())
  def sub([hd1 | _], [hd2 | _]) when is_list(hd1) or is_list(hd2) do
    Result.error("Vectors must be row type!")
  end

  def sub(vec1, vec2) do
    if size(vec1) == size(vec2) do
      [vec1, vec2]
      |> List.zip()
      |> Enum.map(fn {x, y} -> x - y end)
      |> Result.ok()
    else
      Result.error("Size both vectors must be same!")
    end
  end

  @doc """
  Scalar product of two a row vectors. These two vectors must have a same size.
  Otherwise you get an error message.

  Returns result, it means either tuple of `{:ok, number}` or `{:error, "msg"}`.

  ## Examples

      iex> MatrixReloaded.Vector.dot([1, 2, 3], [4, 5, 6])
      {:ok, 32}

  """

  @spec dot(t(), t()) :: Result.t(String.t(), number)
  def dot([hd1 | _], [hd2 | _]) when is_list(hd1) or is_list(hd2) do
    Result.error("Vectors must be row type!")
  end

  def dot(vec1, vec2) do
    if size(vec1) == size(vec2) do
      [vec1, vec2]
      |> List.zip()
      |> Enum.map(fn {x, y} -> x * y end)
      |> Enum.sum()
      |> Result.ok()
    else
      Result.error("Size both vectors must be same!")
    end
  end

  @doc """
  Inner product of two a row vectors. It produces a row vector where each
  element `i, j` is the product of elements `i, j` of the original two
  row vectors. These two vectors must have a same size. Otherwise you get
  an error message.

  Returns result, it means either tuple of `{:ok, vector}` or `{:error, "msg"}`.

  ## Examples

      iex> MatrixReloaded.Vector.inner_product([1, 2, 3], [4, 5, 6])
      {:ok, [4, 10, 18]}

  """

  @spec inner_product(t(), t()) :: Result.t(String.t(), t())
  def inner_product([hd1 | _], [hd2 | _]) when is_list(hd1) or is_list(hd2) do
    Result.error("Vectors must be row type!")
  end

  def inner_product(vec1, vec2) do
    if size(vec1) == size(vec2) do
      [vec1, vec2]
      |> List.zip()
      |> Enum.map(fn {x, y} -> x * y end)
      |> Result.ok()
    else
      Result.error("Size both vectors must be same!")
    end
  end

  @doc """
  Outer product of two a row vectors. It produces a matrix of dimension
  `{m, n}` where `m` and `n` are length (size) of row vectors. If input
  vectors aren't a row type you get an error message.

  Returns result, it means either tuple of `{:ok, matrix}` or `{:error, "msg"}`.

  ## Examples

      iex> MatrixReloaded.Vector.outer_product([1, 2, 3, 4], [1, 2, 3])
      {:ok,
          [
            [1, 2, 3],
            [2, 4, 6],
            [3, 6, 9],
            [4, 8, 12]
          ]
        }

  """

  @spec outer_product(t(), t()) :: Result.t(String.t(), Matrix.t())
  def outer_product([hd1 | _], [hd2 | _]) when is_list(hd1) or is_list(hd2) do
    Result.error("Vectors must be row type!")
  end

  def outer_product(vec1, vec2) do
    if 1 < size(vec1) and 1 < size(vec2) do
      vec1
      |> Enum.map(fn el -> mult_by_num(vec2, el) end)
      |> Result.ok()
    else
      Result.error("Vectors must contain at least two values!")
    end
  end

  @doc """
  Multiply a vector by number.

  ## Examples

      iex> MatrixReloaded.Vector.row(3, 2) |> MatrixReloaded.Vector.mult_by_num(3)
      [6, 6, 6]

      iex> MatrixReloaded.Vector.col(3, 2) |> MatrixReloaded.Vector.mult_by_num(3)
      [[6], [6], [6]]

  """

  @spec mult_by_num(t() | column(), number) :: t() | column()
  def mult_by_num([hd | _] = vec, val) when is_list(hd) do
    vec
    |> transpose()
    |> mult_by_num(val)
    |> transpose()
  end

  def mult_by_num(vec, val) do
    Enum.map(vec, fn x -> x * val end)
  end

  @doc """
  Update row vector by given a row subvector (list) of numbers or just by one number.
  The vector elements you want to change are given by the index. The index is a
  non-negative integer and determines the position of the element in the vector.

  Returns result, it means either tuple of `{:ok, vector}` or `{:error, "msg"}`.
  ##  Example:
      iex> vec = 0..10 |> Enum.to_list()
      iex> MatrixReloaded.Vector.update(vec, [0, 0, 0], 5)
      {:ok, [0, 1, 2, 3, 4, 0, 0, 0, 8, 9, 10]}

      iex> vec = 0..10 |> Enum.to_list()
      iex> MatrixReloaded.Vector.update(vec, 0, 5)
      {:ok, [0, 1, 2, 3, 4, 0, 6, 7, 8, 9, 10]}
  """
  @spec update(t(), number() | t(), non_neg_integer()) ::
          Result.t(String.t(), t())

  def update(vec, subvec, index)
      when is_list(vec) and is_number(subvec) and is_integer(index) do
    update(vec, [subvec], index)
  end

  def update(vec, subvec, index)
      when is_list(vec) and is_list(subvec) and is_integer(index) do
    len_vec = Kernel.length(vec)

    vec
    |> is_index_ok?(index)
    |> Result.and_then(&is_index_at_vector?(&1, len_vec, index))
    |> Result.and_then(&is_subvec_in_vec?(&1, len_vec, Kernel.length(subvec), index))
    |> Result.map(&make_update(&1, subvec, index))
  end

  @doc """
  Update row vector by given a row subvector (list) of numbers or by one number.
  The elements you want to change are given by the vector of indices. These
  indices must be a non-negative integers and determine the positions of
  the element in the vector.

  Returns result, it means either tuple of `{:ok, vector}` or `{:error, "msg"}`.
  ##  Example:
      iex> vec = 0..10 |> Enum.to_list()
      iex> MatrixReloaded.Vector.update_map(vec, [0, 0], [2, 7])
      {:ok, [0, 1, 0, 0, 4, 5, 6, 0, 0, 9, 10]}

      iex> vec = 0..10 |> Enum.to_list()
      iex> MatrixReloaded.Vector.update_map(vec, 0, [2, 7])
      {:ok, [0, 1, 0, 3, 4, 5, 6, 0, 8, 9, 10]}
  """
  @spec update_map(t(), number() | t(), list(non_neg_integer())) ::
          Result.t(String.t(), t())
  def update_map(vec, subvec, position_indices) do
    Enum.reduce(position_indices, {:ok, vec}, fn position, acc ->
      Result.and_then(acc, &update(&1, subvec, position))
    end)
  end

  @doc """
  The size of the vector.

  Returns a positive integer.

  ## Example:

      iex> MatrixReloaded.Vector.row(3) |> MatrixReloaded.Vector.size()
      3

      iex> MatrixReloaded.Vector.col(4, -1) |> MatrixReloaded.Vector.size()
      4

  """
  @spec size(t()) :: non_neg_integer
  def size(vec), do: length(vec)

  defp make_update(vec, subvec, index) do
    len_subvec = Kernel.length(subvec)

    vec
    |> Enum.with_index()
    |> Enum.map(fn {val, i} ->
      if i in index..(index + len_subvec - 1) do
        subvec |> Enum.at(i - index)
      else
        val
      end
    end)
  end

  defp is_index_ok?(vec, idx) when is_integer(idx) and 0 <= idx do
    Result.ok(vec)
  end

  defp is_index_ok?(_vec, idx) when is_number(idx) do
    Result.error("The index must be integer number greater or equal to zero!")
  end

  defp is_index_at_vector?(
         vec,
         len,
         index,
         method \\ :update
       )

  defp is_index_at_vector?(vec, len, idx, _method)
       when idx <= len do
    Result.ok(vec)
  end

  defp is_index_at_vector?(
         _vec,
         _len,
         _idx,
         method
       ) do
    Result.error(
      "You can not #{Atom.to_string(method)} the #{vec_or_subvec(method)}. The index is outside of vector!"
    )
  end

  defp is_subvec_in_vec?(
         vec,
         len_vec,
         len_subvec,
         index,
         method \\ :update
       )

  defp is_subvec_in_vec?(
         vec,
         len_vec,
         len_subvec,
         idx,
         _method
       )
       when len_subvec + idx <= len_vec do
    Result.ok(vec)
  end

  defp is_subvec_in_vec?(
         _vec,
         _len_vec,
         _len_subvec,
         idx,
         method
       ) do
    Result.error(
      "You can not #{Atom.to_string(method)} #{vec_or_subvec(method)} on given position #{idx}. A part of subvector is outside of matrix!"
    )
  end

  defp vec_or_subvec(:update) do
    "vector"
  end

  # defp vec_or_subvec(:get) do
  #   "subvector or element"
  # end
end