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