defmodule Arrays.Implementations.MapArray do
@moduledoc """
An array implementation based on the built-in Map structure.
"""
alias __MODULE__
defstruct contents: %{}
if Code.ensure_loaded?(FunLand.Mappable) do
Module.eval_quoted(__MODULE__,
quote do
use FunLand.Mappable
@doc """
Implementation for `FunLand.Mappable.map`.
Note that `FunLand` is an optional dependency of `Arrays` so you need to add it to your `mix.exs` dependencies manually to use it.
"""
def map(array, fun), do: Arrays.Protocol.map(array, fun)
end)
end
if Code.ensure_loaded?(FunLand.Reducible) do
Module.eval_quoted(__MODULE__,
quote do
use FunLand.Reducible, auto_enumerable: false
@impl FunLand.Reducible
@doc """
Implementation for `FunLand.Reducible.reduce`.
Note that `FunLand` is an optional dependency of `Arrays` so you need to add it to your `mix.exs` dependencies manually to use it.
"""
def reduce(%MapArray{contents: contents}, acc, fun) do
reduce(contents, acc, fun, 0, :maps.size(contents))
end
defp reduce(_contents, acc, _fun, index, index), do: acc
defp reduce(contents, acc, fun, index, max_index) do
reduce(contents, fun.(contents[index], acc), fun, index + 1, max_index)
end
end)
end
@behaviour Access
@impl Access
def fetch(%MapArray{contents: contents}, index)
when index >= 0 and index < map_size(contents) do
Map.fetch(contents, index)
end
def fetch(%MapArray{contents: contents}, index)
when index < 0 and index >= (-map_size(contents)) do
Map.fetch(contents, index + map_size(contents))
end
def fetch(%MapArray{}, _index), do: :error
@undefined_pop_message """
There is no efficient implementation possible to remove an element from a random location in an array, so `Access.pop/2` (and returning `:pop` from `Access.get_and_update/3` ) are not supported by #{inspect(__MODULE__)}. If you want to remove the last element, use `Arrays.extract/1`.
""" |> String.trim
@impl Access
def get_and_update(array = %MapArray{contents: contents}, index, function)
when index >= 0 and index < map_size(contents) do
value = contents[index]
case function.(value) do
{current_value, new_value} ->
new_contents = Map.put(contents, index, new_value)
{current_value, %MapArray{array | contents: new_contents}}
:pop ->
raise ArgumentError, @undefined_pop_message
end
end
@impl Access
def get_and_update(array = %MapArray{contents: contents}, index, function)
when index < 0 and index >= -map_size(contents) do
{value, new_contents} = Map.get_and_update(contents, index + map_size(contents), function)
{value, %MapArray{array | contents: new_contents}}
end
def get_and_update(%MapArray{}, _index, _function) do
raise ArgumentError
end
@impl true
def pop(%MapArray{}, _index) do
raise ArgumentError, @undefined_pop_message
end
@doc false
def build_slice(%MapArray{contents: contents}, start, length, into) do
for index <- start..(start + length - 1), into: into do
contents[index]
end
end
defimpl Arrays.Protocol do
alias Arrays.Implementations.MapArray
@impl true
def size(%MapArray{contents: contents}) do
map_size(contents)
end
@impl true
def map(array = %MapArray{contents: contents}, fun) do
new_contents = :maps.map(fn _key, value -> fun.(value) end, contents)
%MapArray{array | contents: new_contents}
end
@impl true
def reduce(%MapArray{contents: contents}, acc, fun) do
reduce(contents, acc, fun, 0, :maps.size(contents))
end
defp reduce(_contents, acc, _fun, index, index), do: acc
defp reduce(contents, acc, fun, index, max_index) do
reduce(contents, fun.(contents[index], acc), fun, index + 1, max_index)
end
@impl true
def reduce_right(%MapArray{contents: contents}, acc, fun) do
reduce_right(contents, acc, fun, :maps.size(contents))
end
defp reduce_right(_contents, acc, _fun, 0), do: acc
defp reduce_right(contents, acc, fun, index) do
reduce_right(contents, fun.(acc, contents[index - 1]), fun, index - 1)
end
@impl true
def get(%MapArray{contents: contents}, index)
when index >= 0 and index < map_size(contents) do
contents[index]
end
def get(%MapArray{contents: contents}, index)
when index < 0 and index > -map_size(contents) do
contents[index + map_size(contents)]
end
@impl true
def replace(array = %MapArray{contents: contents}, index, value)
when index >= 0 and index < map_size(contents) do
new_contents = Map.put(contents, index, value)
%MapArray{array | contents: new_contents}
end
def replace(array = %MapArray{contents: contents}, index, value)
when index < 0 and index >= -map_size(contents) do
new_contents = Map.put(contents, index + map_size(contents), value)
%MapArray{array | contents: new_contents}
end
@impl true
def append(array = %MapArray{contents: contents}, value) do
new_contents = Map.put(contents, map_size(contents), value)
%MapArray{array | contents: new_contents}
end
@impl true
def resize(%MapArray{}, 0, _default) do
empty([])
end
def resize(array = %MapArray{contents: contents}, size, _default) when map_size(contents) == size do
array
end
def resize(array = %MapArray{contents: contents}, new_size, _default)
when new_size < map_size(contents) do
cur_size = map_size(contents)
new_contents = Map.drop(contents, Enum.to_list(new_size..(cur_size - 1)))
%MapArray{array | contents: new_contents}
end
def resize(array = %MapArray{contents: contents}, new_size, default)
when new_size > map_size(contents) do
cur_size = map_size(contents)
new_contents = Enum.into(cur_size..(new_size - 1), contents, &{&1, default})
%MapArray{array | contents: new_contents}
end
@impl true
def extract(array = %MapArray{contents: contents}) when map_size(contents) > 0 do
index = map_size(contents) - 1
elem = contents[index]
new_contents = Map.delete(contents, index)
new_array = %MapArray{array | contents: new_contents}
{:ok, {elem, new_array}}
end
def extract(%MapArray{contents: contents}) when map_size(contents) == 0 do
{:error, :empty}
end
@impl true
def to_list(%MapArray{contents: contents}) do
:maps.values(contents)
end
@impl true
def slice(array = %MapArray{}, start, amount) do
@for.build_slice(array, start, amount, empty([]))
end
@impl true
def empty(options) when is_list(options) do
default = Keyword.get(options, :default, nil)
size = Keyword.get(options, :size, 0)
%MapArray{contents: construct(default, size)}
end
defp construct(_default, 0), do: %{}
defp construct(default, size) do
Enum.into(0..(size - 1), %{}, &{&1, default})
end
end
end