Packages

HyperMap is an Elixir library which provides a Map that can be accessed through different keys.

Current section

Files

Jump to
hypermap lib hypermap.ex
Raw

lib/hypermap.ex

defmodule HyperMap do
@moduledoc """
HyperMap provides a multiple-key Map.
A use case could be to store a phonebook from people and then
looking it up by either their first name or their last name without
having to do a full lookup.
"""
defstruct [
values: nil,
num_keys: nil,
opts: nil
]
@doc """
`new` creates a new HyperMap. The `keys` argument is a list defining
the "name" of each key. It can be any term.
For now no `opts` are allowed.
Example: `hmap = HyperMap.new([:first_name, :last_name])`
"""
@spec new(keys :: [term()], opts :: Keyword.t) :: map
def new(keys, opts \\ []) do
if length(keys) < 2, do:
raise "inserting a value with less than 2 keys. You should use a Map"
%__MODULE__{opts: opts,
values: empty_values(keys),
num_keys: length(keys)}
end
@doc """
`put` adds a `value` for the given `keys` to the HyperMap.
Example `HyperMap.put(hmap, "555-4246031", [{:first_name, "Joe"}, {:last_name, "Doe"}])`
If the keys are atoms they can be given as a keyword
`HyperMap.put(hmap, "555-4246031", first_name: "Joe", last_name: "Doe")`
"""
@spec put(hypermap :: map, value :: term(), keys :: [tuple()]) :: map
def put(%__MODULE__{} = hmap, value, keys) when is_list(keys) do
if hmap.num_keys != length(keys), do:
raise "inserting a value with a different number of keys " <>
"than #{hmap.num_keys}"
add_item = &(&1 ++ [{keys, value}])
update_in(hmap.values,
fn vals ->
Enum.reduce(keys, vals,
fn ({key, key_val}, map) ->
unless Map.has_key?(map, key),
do: raise "unknown key #{inspect key}"
update_in(map, [key],
&(Map.update(&1, key_val, [{keys, value}], add_item)))
end)
end)
end
@doc """
`from_list` initializes a HyperMap from a list a `values` for the given
`keys`.
Example:
`hypermap = HyperMap.from_list([:first_name, :last_name],
[{"John", "Doe", "555-4246031"},
{"Mary", "Doe", "555-4246049"}])`
"""
@spec from_list(keys :: [term()], values :: [tuple()]) :: map
def from_list(keys, values) when is_list(keys) and is_list(values) do
Enum.reduce(values, __MODULE__.new(keys),
fn (value, hmap) when is_tuple(value) ->
unless tuple_size(value) - 1 == length(keys), do:
raise "incompatible tuple size with keys"
put(hmap,
elem(value, tuple_size(value) - 1),
List.zip([keys, value]))
end)
end
@doc """
`fetch` looks-up a value in the HyperMap using the key valued as second argument.
To look up a value by lastname we could do: `fetch(hypermap, last_name: "Doe")`
To look up vy first name: `fetch(hypermap, first_name: "John")`
"""
@spec fetch(hypermap :: map, key_value :: [tuple]) :: map
def fetch(%__MODULE__{} = hmap, [{key, key_val}]) do
unless Map.has_key?(hmap.values, key),
do: raise "unknown key #{inspect key}"
hmap
|> Map.get(:values)
|> Map.get(key)
|> Map.get(key_val)
end
defp empty_values(keys) do
Enum.reduce(keys, %{}, &Map.put(&2, &1, %{}))
end
end