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