lib/ex_array.ex

defmodule ExArray do
  @moduledoc """
  A wrapper module for Erlang's array.
  """

  defstruct content: nil

  @behaviour Access

  ## Types

  @type t :: %__MODULE__{content: :array.array()}
  @type index :: non_neg_integer
  @type element :: term
  @type opts :: opt | [opt]
  @type opt :: {:fixed, boolean} | :fixed | {:default, any} | {:size, non_neg_integer} | non_neg_integer
  @type orddict :: [{index, element}]

  ## Public functions

  @doc """
  Creates a new, extendible array with initial size zero.
  The default value is the atom nil, not undefined.
  """
  @spec new() :: t
  def new() do
    %__MODULE__{content: :array.new({:default, nil})}
  end

  @doc """
  Creates a new fixed array according to the given options.
  By default, the array is extendible and has initial size zero.
  The default value is the atom nil, if not specified.

  `options` is a single term or a list of terms, selected from the following:

  * `n : non_neg_integer` or `{:size, n : non_neg_integer}`
      * Specifies the initial size of the array; this also implies `{:fixed, true}`.
        If `n` is not a nonnegative integer, the call raises `ArgumentError`.
  * `:fixed` or `{:fixed, true}`
      * Creates a fixed-size array.
  * `{:fixed, false}`
      * Creates an extendible (non fixed-size) array.
  * `{:default, value}`
      * Sets the default value for the array to `value`.
  """
  @spec new(opts) :: t
  def new(options) do
    if is_list(options) do
      %__MODULE__{content: :array.new([{:default, nil} | options])}
    else
      %__MODULE__{content: :array.new([{:default, nil}, options])}
    end
  end

  @doc """
  Check if two arrays are equal using ===.
  """
  @spec equal?(t, t) :: boolean
  def equal?(%__MODULE__{content: c1}, %__MODULE__{content: c2}) do
    s1 = :array.size(c1)
    s2 = :array.size(c2)

    cond do
      s1 != s2 ->
        false

      s1 <= 0 ->
        true

      true ->
        Range.new(0, s1 - 1) |> Enumerable.reduce({:cont, true}, fn idx, _acc -> equal?(c1, c2, idx) end) |> elem(1)
    end
  end

  @doc """
  Gets the value used for uninitialized entries.
  """
  @spec default(t) :: any
  def default(%__MODULE__{content: c}), do: :array.default(c)

  @doc """
  Fixes the size of the array. This prevents it from growing automatically upon insertion.
  """
  @spec fix(t) :: t
  def fix(%__MODULE__{content: c} = arr), do: %__MODULE__{arr | content: :array.fix(c)}

  @doc """
  Folds the elements of the array using the given function and initial accumulator value.
  The elements are visited in order from the lowest index to the highest.

  If `fun` is not a function, the call raises `ArgumentError`.
  """
  @spec foldl(t, acc, (index, element, acc -> acc)) :: acc when acc: var
  def foldl(%__MODULE__{content: c}, acc, fun), do: :array.foldl(fun, acc, c)

  @doc """
  Folds the elements of the array right-to-left using the given function and initial accumulator value.
  The elements are visited in order from the highest index to the lowest.

  If `fun` is not a function, the call raises `ArgumentError`.
  """
  @spec foldr(t, acc, (index, element, acc -> acc)) :: acc when acc: var
  def foldr(%__MODULE__{content: c}, acc, fun), do: :array.foldr(fun, acc, c)

  @doc """
  Equivalent to `from_list(list, nil)`.
  """
  @spec from_list(list) :: t
  def from_list(list), do: %__MODULE__{content: :array.from_list(list, nil)}

  @doc """
  Converts a list to an extendible array.
  `default` is used as the value for uninitialized entries of the array.

  If `list` is not a proper list, the call raises `ArgumentError`.
  """
  @spec from_list(list, any) :: t
  def from_list(list, default), do: %__MODULE__{content: :array.from_list(list, default)}

  @doc """
  Equivalent to `from_orddict(orddict, nil)`.
  """
  @spec from_orddict(orddict) :: t
  def from_orddict(orddict), do: %__MODULE__{content: :array.from_orddict(orddict, nil)}

  @doc """
  Converts an ordered list of pairs `{index, value}` to a corresponding extendible array.
  `default` is used as the value for uninitialized entries of the array.

  If `orddict` is not a proper, ordered list of pairs whose first elements are nonnegative integers,
  the call raises `ArgumentError`.
  """
  @spec from_orddict(orddict, any) :: t
  def from_orddict(orddict, default), do: %__MODULE__{content: :array.from_orddict(orddict, default)}

  @doc """
  Converts an Erlang's array to an array.
  All properties (size, elements, default value, fixedness) of the original array are preserved.

  If `erl_arr` is not an Erlang's array, the call raises `ArgumentError`.
  """
  @spec from_erlang_array(:array.array()) :: t
  def from_erlang_array(erl_arr) do
    if :array.is_array(erl_arr) do
      %__MODULE__{content: erl_arr}
    else
      raise ArgumentError
    end
  end

  @doc """
  Gets the value of entry `idx`. If `idx` is not a nonnegative integer, or if the array has
  fixed size and `idx` is larger than the maximum index, the call raises `ArgumentError`.
  """
  @spec get(t, index) :: element
  def get(%__MODULE__{content: c}, idx), do: :array.get(idx, c)

  @doc """
  Returns `true` if `arr` appears to be an array, otherwise `false`.
  Note that the check is only shallow; there is no guarantee that `arr` is a well-formed array
  representation even if this function returns `true`.
  """
  @spec is_array(t) :: boolean
  def is_array(arr) do
    case arr do
      %__MODULE__{content: c} -> :array.is_array(c)
      _ -> false
    end
  end

  @doc """
  Checks if the array has fixed size. Returns `true` if the array is fixed, otherwise `false`.
  """
  @spec is_fix(t) :: boolean
  def is_fix(%__MODULE__{content: c}), do: :array.is_fix(c)

  @doc """
  Maps the given function onto each element of the array.
  The elements are visited in order from the lowest index to the highest.

  If `fun` is not a function, the call raises `ArgumentError`.
  """
  @spec map(t, (index, element -> any)) :: t
  def map(%__MODULE__{content: c} = arr, fun), do: %__MODULE__{arr | content: :array.map(fun, c)}

  @doc """
  Makes the array resizable.
  """
  @spec relax(t) :: t
  def relax(%__MODULE__{content: c} = arr), do: %__MODULE__{arr | content: :array.relax(c)}

  @doc """
  Resets entry `idx` to the default value for the array.
  If the value of entry `idx` is the default value the array will be returned unchanged.
  Reset will never change size of the array. Shrinking can be done explicitly by calling `resize/2`.

  If `idx` is not a nonnegative integer, or if the array has fixed size and `idx` is
  larger than the maximum index, the call raises `ArgumentError`.
  """
  @spec reset(t, index) :: t
  def reset(%__MODULE__{content: c} = arr, idx), do: %__MODULE__{arr | content: :array.reset(idx, c)}

  @doc """
  Changes the size of the array to that reported by `sparse_size/1`.
  If the given array has fixed size, the resulting array will also have fixed size.
  """
  @spec resize(t) :: t
  def resize(%__MODULE__{content: c} = arr), do: %__MODULE__{arr | content: :array.resize(c)}

  @doc """
  Changes the size of the array.
  If `size` is not a nonnegative integer, the call raises `ArgumentError`.
  If the given array has fixed size, the resulting array will also have fixed size.
  """
  @spec resize(t, non_neg_integer) :: t
  def resize(%__MODULE__{content: c} = arr, size), do: %__MODULE__{arr | content: :array.resize(size, c)}

  @doc """
  Sets entry `idx` of the array to `val`.
  If `idx` is not a nonnegative integer, or if the array has fixed size and `idx` is
  larger than the maximum index, the call raises `ArgumentError`.
  """
  @spec set(t, index, element) :: t
  def set(%__MODULE__{content: c} = arr, idx, val), do: %__MODULE__{arr | content: :array.set(idx, val, c)}

  @doc """
  Gets the number of entries in the array.
  Entries are numbered from 0 to `size(array)-1`; hence, this is also the index of
  the first entry that is guaranteed to not have been previously set.
  """
  @spec size(t) :: non_neg_integer
  def size(%__MODULE__{content: c}), do: :array.size(c)

  @doc """
  Folds the elements of the array using the given function and initial accumulator value,
  skipping default-valued entries.
  The elements are visited in order from the lowest index to the highest.

  If `fun` is not a function, the call raises `ArgumentError`.
  """
  @spec sparse_foldl(t, acc, (index, element, acc -> acc)) :: acc when acc: var
  def sparse_foldl(%__MODULE__{content: c}, acc, fun), do: :array.sparse_foldl(fun, acc, c)

  @doc """
  Folds the elements of the array right-to-left using the given function and initial accumulator value,
  skipping default-valued entries.
  The elements are visited in order from the highest index to the lowest.

  If `fun` is not a function, the call raises `ArgumentError`.
  """
  @spec sparse_foldr(t, acc, (index, element, acc -> acc)) :: acc when acc: var
  def sparse_foldr(%__MODULE__{content: c}, acc, fun), do: :array.sparse_foldr(fun, acc, c)

  @doc """
  Maps the given function onto each element of the array, skipping default-valued entries.
  The elements are visited in order from the lowest index to the highest.

  If `fun` is not a function, the call raises `ArgumentError`.
  """
  @spec sparse_map(t, (non_neg_integer, element -> any)) :: t
  def sparse_map(%__MODULE__{content: c} = arr, fun), do: %__MODULE__{arr | content: :array.sparse_map(fun, c)}

  @doc """
  Gets the number of entries in the array up until the last non-default valued entry.
  In other words, returns `idx+1` if `idx` is the last non-default valued entry in the array,
  or zero if no such entry exists.
  """
  @spec sparse_size(t) :: non_neg_integer
  def sparse_size(%__MODULE__{content: c}), do: :array.sparse_size(c)

  @doc """
  Converts the array to a list, skipping default-valued entries.
  """
  @spec sparse_to_list(t) :: list
  def sparse_to_list(%__MODULE__{content: c}), do: :array.sparse_to_list(c)

  @doc """
  Converts the array to an ordered list of pairs `{index, value}`, skipping default-valued entries.
  """
  @spec sparse_to_orddict(t) :: [{index, element}]
  def sparse_to_orddict(%__MODULE__{content: c}), do: :array.sparse_to_orddict(c)

  @doc """
  Converts the array to its underlying Erlang's array.
  """
  @spec to_erlang_array(t) :: :array.array()
  def to_erlang_array(%__MODULE__{content: c}), do: c

  @doc """
  Converts the array to a list.
  """
  @spec to_list(t) :: list
  def to_list(%__MODULE__{content: c}), do: :array.to_list(c)

  @doc """
  Converts the array to an ordered list of pairs `{index, value}`.
  """
  @spec to_orddict(t) :: [{index, element}]
  def to_orddict(%__MODULE__{content: c}), do: :array.to_orddict(c)

  @impl Access
  @spec fetch(t(), index()) :: :error | {:ok, any}
  def fetch(%__MODULE__{} = arr, idx) do
    if -1 < idx and idx < size(arr) do
      {:ok, get(arr, idx)}
    else
      :error
    end
  end

  @impl Access
  @spec get_and_update(t(), index(), (any() -> {any(), any()})) :: {any(), t()}
  def get_and_update(%__MODULE__{} = arr, idx, fun) do
    {get, update} = fun.(get(arr, idx))
    {get, ExArray.set(arr, idx, update)}
  end

  @impl Access
  @spec pop(t(), index()) :: {any(), t()}
  def pop(%__MODULE__{} = arr, idx) do
    if -1 < idx and idx < size(arr) do
      value = get(arr, idx)
      {value, reset(arr, idx)}
    else
      {nil, arr}
    end
  end

  ## Private functions

  @spec equal?(:array.array(), :array.array(), non_neg_integer()) :: {:cont | :halt, boolean()}
  defp equal?(c1, c2, idx) do
    if :array.get(idx, c1) === :array.get(idx, c2) do
      {:cont, true}
    else
      {:halt, false}
    end
  end
end