lib/iteraptor/array.ex

defmodule Iteraptor.Array do
  @moduledoc """
  Array emulation implementing `Access` behaviour. Index in array is zero-based.

  `Array` is the "go to" array data structure in Elixir. An array can be
    constructed using `Array.new/{0,1}`:

      iex> Iteraptor.Array.new()
      #Array<[]>

      iex> Iteraptor.Array.new(2)
      #Array<[nil, nil]>

      iex> Iteraptor.Array.new([:foo, :bar])
      #Array<[:foo, :bar]>

  An array can contain any kind of elements, and elements in an array don't have
    to be of the same type. By definition, arrays have _keys_ in `0..size-1` range.
  Arrays are implicitly expandable, which means adding an element at index `100`
    to the array currently containing 1 element would increase the size of the
    array to `100`.

      iex> array = Iteraptor.Array.new([:foo])
      iex> Iteraptor.Array.set(array, 3, :bar)
      #Array<[:foo, nil, nil, :bar]>

  An `Array` is represented internally using the `%Array{}` struct. Note that,
    however, the struct fields are private and must not be accessed directly;
    use the functions in this module to perform operations on arrays.

  `Array`s can also be constructed starting from other collection-type data
  structures: for example, see `Array.new/1` or `Enum.into/2`.

      iex> Enum.into([1, 2, 3], Iteraptor.Array.new())
      #Array<[1, 2, 3]>

  `Array`s do implement `Access` behaviour.

      iex> array = Iteraptor.Array.new([%{foo: 42}, %{bar: :baz}])
      iex> get_in(array, [0, :foo])
      42

  """

  # Arrays have an underlying Map. Array elements are values of said map,
  # indices are keys.

  @type value :: term

  @type t :: %__MODULE__{map: %{required(non_neg_integer) => any()}}

  defstruct map: %{}, version: 1

  @behaviour Access

  alias Iteraptor.Array

  @doc """
  Returns a new array.

      iex> Iteraptor.Array.new()
      #Array<[]>

  Creates an array of the given length or from enumerable. Might we used to wrap
  the existing instance of `Iteraptor.Array`.

      iex> Iteraptor.Array.new(3)
      #Array<[nil, nil, nil]>

      iex> Iteraptor.Array.new([:foo, :bar, 42])
      #Array<[:foo, :bar, 42]>

  Also the transformation function might be passed via second argument.

      iex> Iteraptor.Array.new([1, 2, 3], fn x -> 2 * x end)
      #Array<[2, 4, 6]>
  """
  # @spec new(enumerable :: nil | t() | integer() | Enum.t(), transform :: (term() -> any())) :: t()
  def new(enumerable \\ nil, transform \\ nil)

  def new(nil, nil), do: %Array{}

  def new(%__MODULE__{} = array, nil), do: array

  def new(n, nil) when is_integer(n) and n >= 0,
    do: Iteraptor.Array.new(List.duplicate(nil, n))

  def new(enumerable, nil) do
    list = Enum.to_list(enumerable)

    map =
      0..(length(list) - 1)
      |> Enum.zip(list)
      |> Enum.into(%{})

    %Array{map: map}
  end

  def new(enumerable, transform) when is_function(transform, 1) do
    list =
      enumerable
      |> Enum.map(&transform.(&1))
      |> Enum.to_list()

    map =
      0..(length(list) - 1)
      |> Enum.zip(list)
      |> Enum.into(%{})

    %Array{map: map}
  end

  @doc """
  Appends another enumerable to the array.

      iex> array = Iteraptor.Array.new([1, 2, 3])
      iex> Iteraptor.Array.append(array, [4, 5])
      #Array<[1, 2, 3, 4, 5]>
  """
  @spec append(t(), any()) :: t()
  def append(%Array{map: map} = array, other) do
    index = map |> Map.keys() |> List.last() || -1

    map =
      if Enumerable.impl_for(other) do
        appender =
          other
          |> Enum.reduce({index + 1, []}, fn e, {i, acc} -> {i + 1, [{i, e} | acc]} end)
          |> elem(1)
          |> Enum.reverse()
          |> Enum.into(%{})

        Map.merge(map, appender)
      else
        Map.put(map, index + 1, other)
      end

    %Array{array | map: map}
  end

  @doc """
  Returns the `value` at `index` in `array`, or `default` if index is out of array bounds.


      iex> array = Iteraptor.Array.new([42])
      iex> Iteraptor.Array.get(array, 0)
      42
      iex> Iteraptor.Array.get(array, 2, 42)
      42
  """
  @spec get(t(), non_neg_integer(), any()) :: any()
  def get(array, index, default \\ nil)
  def get(%Array{map: map}, index, default) when index < 0 or index >= map_size(map), do: default
  def get(%Array{map: map}, index, _default), do: map[index]

  @doc """
  Pops (deletes) `value` at `index` from `array`, setting the value at the
    respective index to `nil`.
  Returns a tuple containing the value removed and the new array.

      iex> array = Iteraptor.Array.new([1, 2, 3])
      iex> {elem, array} = Iteraptor.Array.pop(array, 1)
      iex> elem
      2
      iex> array
      #Array<[1, nil, 3]>
  """
  @impl Access
  @spec pop(t(), non_neg_integer()) :: {any(), t()}
  def pop(%Array{map: map} = array, index) do
    value = map[index]
    {value, %{array | map: Map.put(map, index, nil)}}
  end

  @doc """
  Sets the `value` at `index` in `array`, expanding the array if necessary.
  Returns a new array.

      iex> array = Iteraptor.Array.new([42])
      iex> Iteraptor.Array.set(array, 0, :foo)
      #Array<[:foo]>
      iex> Iteraptor.Array.set(array, 2, :bar)
      #Array<[42, nil, :bar]>
  """
  @spec set(t(), non_neg_integer(), any()) :: t()
  def set(%Array{map: map} = array, index, value) do
    size = Array.size(array)

    map =
      if size > index do
        Map.put(map, index, value)
      else
        fill = Enum.reverse([value | List.duplicate(nil, index - size)])
        Map.merge(map, Enum.into(Enum.zip(size..index, fill), %{}))
      end

    %Array{array | map: map}
  end

  @doc """
  Trims `nil` values from the tail of the `Array`. Returns a trimmed array.

      iex> array = Iteraptor.Array.new([42, nil, nil])
      #Array<[42, nil, nil]>
      iex> Iteraptor.Array.trim(array)
      #Array<[42]>
  """
  @spec trim(array :: t()) :: t()
  def trim(%Array{map: map} = array) do
    map =
      map
      |> Enum.reverse()
      |> Enum.drop_while(fn
        {_, nil} -> true
        _ -> false
      end)
      |> Enum.reverse()
      |> Enum.into(%{})

    %Array{array | map: map}
  end

  @doc """
  Returns the number of elements in `array`.

      iex> Iteraptor.Array.size(Iteraptor.Array.new([1, 2, 3]))
      3
  """
  @spec size(t()) :: non_neg_integer()
  def size(%Array{map: map}), do: map_size(map)

  @doc """
  Converts `array` to a list.

      iex> Iteraptor.Array.to_list(Iteraptor.Array.new([1, 2, 3]))
      [1, 2, 3]
  """
  @spec to_list(t()) :: [any()]
  def to_list(%Array{map: map}),
    do: map |> Enum.sort() |> Enum.map(&elem(&1, 1))

  @doc """
  Converts a tuple given as parameter to `array`.

      iex> Iteraptor.Array.from_tuple({1, 2, 3})
      #Array<[1, 2, 3]>
  """
  @spec from_tuple(tuple :: tuple()) :: t()
  def from_tuple(tuple) when is_tuple(tuple),
    do: tuple |> Tuple.to_list() |> Array.new()

  ### Access behaviour

  @doc false
  @impl true
  def fetch(%Array{map: map}, index), do: Map.fetch(map, index)

  @doc false
  @impl true
  def get_and_update(%Array{map: map} = array, index, function) do
    case function.(map[index]) do
      :pop -> Array.pop(array, index)
      {get_value, update_value} -> {get_value, Array.set(array, index, update_value)}
    end
  end

  defimpl Enumerable do
    def count(array) do
      {:ok, Array.size(array)}
    end

    def member?(%Array{map: map}, val) do
      {:ok,
       !!Enum.find(map, fn
         {_, ^val} -> true
         {_, _} -> false
       end)}
    end

    def reduce(array, acc, fun),
      do: Enumerable.List.reduce(Array.to_list(array), acc, fun)

    if Version.compare(System.version(), "1.10.0-dev") == :lt do
      def slice(array) do
        {:ok, Array.size(array), &Enumerable.List.slice(Array.to_list(array), &1, &2)}
      end
    else
      defp slice(_list, _start, 0, _size), do: []
      defp slice(list, start, count, size) when start + count == size, do: list |> drop(start)
      defp slice(list, start, count, _size), do: list |> drop(start) |> take(count)

      defp drop(list, 0), do: list
      defp drop([_ | tail], count), do: drop(tail, count - 1)

      defp take(_list, 0), do: []
      defp take([head | tail], count), do: [head | take(tail, count - 1)]

      def slice(array) do
        size = Array.size(array)

        {:ok, size, &slice(Array.to_list(array), &1, &2, size)}
      end
    end
  end

  defimpl Collectable do
    def into(array) do
      fun = fn
        list, {:cont, x} -> [x | list]
        list, :done -> Array.append(array, Enum.reverse(list))
        _, :halt -> :ok
      end

      {[], fun}
    end
  end

  defimpl Inspect do
    import Inspect.Algebra

    def inspect(array, opts) do
      opts = %Inspect.Opts{opts | charlists: :as_lists}
      concat(["#Array<", Inspect.List.inspect(Array.to_list(array), opts), ">"])
    end
  end
end