Packages
A simple dual-entry map
Retired package: Deprecated - Bad names, corrected in version 0.2.1
Current section
Files
Jump to
Current section
Files
lib/dual_map.ex
defmodule DualMap do
@moduledoc """
A DualMap is simply a dual-entry map struct that allows you to reference pairs of data using both, a key or a value. In a DualMap you can look up a value from its key or a key from its value.
In simple terms we could say that a DualMap is a map where there is no difference between key and value, both can be either one or the other.
## How does it work?
A DualMap actually stores 2 maps, a direct one with the key => value pairs, and a inveted one with the value => key pairs.
To create a new DualMap you must use the `DualMap.new` function.
```elixir
DualMap.new()
```
## Let's see some examples:
```elixir
iex> dm = DualMap.new({:hostname})
[]
iex> DualMap.put(dm, [
{"ns3", "192.168.0.4"},
{"ns2", "192.168.0.3"},
{"ns1", "192.168.0.2"}
])
[
{"ns1", "192.168.0.2"},
{"ns2", "192.168.0.3"},
{"ns3", "192.168.0.4"}
]
iex> DualMap.delete(dm, "192.168.0.3")
[
{"ns1", "192.168.0.2"},
{"ns3", "192.168.0.4"}
]
```
"""
defstruct [
__data: %{
map1: %{},
map2: %{}
}
]
@typedoc "DualMap struct"
@type t :: %__MODULE__{}
@doc """
Returns an empty DualMap struct.
## Examples
iex> DualMap.new({:hostname})
[]
"""
@spec new() :: t()
def new() do
%DualMap{
__data: %{
map1: %{},
map2: %{},
}
}
end
@doc """
Returns a DualMap struct initialized with the values indicated in the second argument. As the `new/1` function, the order of the master keys are important for posterior operations with the struct.
## Examples
# Initializing with one pair of values
iex> DualMap.new({"ns1", "192.168.0.2"})
[{"ns1", "192.168.0.2"}]
# Initializing with more than one pair of values
iex> DualMap.new([
{"ns3", "192.168.0.4"},
{"ns2", "192.168.0.3"},
{"ns1", "192.168.0.2"}
])
[
{"ns1", "192.168.0.2"},
{"ns2", "192.168.0.3"},
{"ns3", "192.168.0.4"}
]
"""
@spec new({any(), any()} | list(tuple())) :: t()
def new(values) when is_tuple(values) or is_list(values) do
new()
|> put(values)
end
@doc """
Delete one or many pair of datas and returns the DualMap without that pairs. The pair is found looking for `key` both internal maps, so that it is possible to reference the pair both by its key and by its value.
## Examples
iex> dm = DualMap.new([
{"ns3", "192.168.0.4"},
{"ns2", "192.168.0.3"},
{"ns1", "192.168.0.2"}
])
[
{"ns1", "192.168.0.2"},
{"ns2", "192.168.0.3"},
{"ns3", "192.168.0.4"}
]
iex> DualMap.delete(dm, "192.168.0.3")
[
{"ns1", "192.168.0.2"},
{"ns3", "192.168.0.4"}
]
# Or if you want to delete many...
iex> DualMap.delete(dm, ["ns2", "192.168.0.2"])
[{"ns3", "192.168.0.4"}]
"""
@spec delete(t(), keys :: any() | list()) :: t()
def delete(dual_map, [key | keys]) do
dual_map = _delete(dual_map, key)
delete(dual_map, keys)
end
def delete(dual_map, key), do: _delete(dual_map, key)
defp _delete(dual_map, key) do
{key1, key2} =
case {dual_map.__data.map1[key], dual_map.__data.map2[key]} do
{nil, nil} -> {nil, nil}
{key2, nil} -> {key, key2}
{nil, key1} -> {key1, key}
end
data = %{dual_map.__data |
map1: Map.delete(dual_map.__data.map1, key1),
map2: Map.delete(dual_map.__data.map2, key2)
}
%{dual_map | __data: data}
end
@doc """
Insert or replace one or more pairs of datas in a DualMap.
## Examples
iex> dm = DualMap.new()
[]
# Inserting/replacing many
iex> DualMap.put(dm, [
{"192.168.0.4", "ns3"},
{"192.168.0.3", "ns2"},
{"192.168.0.2", "ns1"}
])
[
{"ns1", "192.168.0.2"},
{"ns2", "192.168.0.3"},
{"ns3", "192.168.0.4"}
]
# Or inserting just one
iex> DualMap.put(dm, {"192.168.0.4", "ns3"})
[{"ns3", "192.168.0.4"}]
"""
@spec put(t(), {key :: any(), value :: any()} | list(tuple())) :: t()
def put(dual_map, []), do: dual_map
def put(dual_map, [{key, value} | rest]) do
dual_map = put(dual_map, {key, value})
put(dual_map, rest)
end
def put(dual_map, {key, value}) do
map_key1 = Map.put(dual_map.__data.map1, key, value)
map_key2 = Map.put(dual_map.__data.map2, value, key)
data = %{dual_map.__data |
map1: map_key1,
map2: map_key2
}
%{dual_map | __data: data}
end
@doc """
Returns the value asociated to `key` in any of the internal maps. If `key` does not exists, `default` or `nil` is returned.
## Examples
iex> dm = DualMap.new([
{"ns3", "192.168.0.4"},
{"ns2", "192.168.0.3"},
{"ns1", "192.168.0.2"}
])
[
{"ns1", "192.168.0.2"},
{"ns2", "192.168.0.3"},
{"ns3", "192.168.0.4"}
]
iex> DualMap.get(dm, "192.168.0.4")
"ns3"
iex> DualMap.get(dm, "ns3")
"192.168.0.4"
"""
@spec get(t(), key :: any(), default :: any()) :: any()
def get(dual_map, key, default \\ nil) do
case Map.get(dual_map.__data.map1, key) do
nil -> Map.get(dual_map.__data.map2, key, default)
value -> value
end
end
@doc """
Return the complete directed or inverted internal map.
## Examples
iex> dm = DualMap.new([
{"ns3", "192.168.0.4"},
{"ns2", "192.168.0.3"},
{"ns1", "192.168.0.2"}
])
[
{"ns1", "192.168.0.2"},
{"ns2", "192.168.0.3"},
{"ns3", "192.168.0.4"}
]
iex> DualMap.get_map(dm)
%{
"ns1" => "192.168.0.2",
"ns2" => "192.168.0.3",
"ns3" => "192.168.0.4"
}
iex> DualMap.get_map(dm, :inverted)
%{
"192.168.0.2" => "ns1",
"192.168.0.3" => "ns2",
"192.168.0.4" => "ns3"
}
"""
@spec get_map(t(), opts :: (:inverted | nil)) :: map()
def get_map(dual_map, opts \\ nil)
def get_map(dual_map, :inverted) do
dual_map.__data.map2
end
def get_map(dual_map, _) do
dual_map.__data.map1
end
@doc """
Return a list with all the keys of the directed or inverted internal map.
## Examples
iex> dm = DualMap.new([
{"ns3", "192.168.0.4"},
{"ns2", "192.168.0.3"},
{"ns1", "192.168.0.2"}
])
iex> DualMap.keys(dm)
["ns1", "ns2", "ns3"]
iex> DualMap.keys(dm, :inverted)
["192.168.0.2", "192.168.0.3", "192.168.0.4"]
"""
@spec keys(t(), opts :: (:inverted | nil)) :: list()
def keys(dual_map, opts \\ nil)
def keys(dual_map, :inverted) do
Map.keys(dual_map.__data.map2)
end
def keys(dual_map, _) do
Map.keys(dual_map.__data.map1)
end
@doc """
Return a list with all the values of the directed or inverted internal map.
## Examples
iex> dm = DualMap.new([
{"ns3", "192.168.0.4"},
{"ns2", "192.168.0.3"},
{"ns1", "192.168.0.2"}
])
[
{"ns1", "192.168.0.2"},
{"ns2", "192.168.0.3"},
{"ns3", "192.168.0.4"}
]
iex> DualMap.values(dm)
["192.168.0.2", "192.168.0.3", "192.168.0.4"]
iex> DualMap.values(dm, :inverted)
["ns1", "ns2", "ns3"]
"""
@spec values(t(), opts :: (:inverted | nil)) :: list()
def values(dual_map, opts \\ nil)
def values(dual_map, :inverted) do
Map.values(dual_map.__data.map2)
end
def values(dual_map, _) do
Map.values(dual_map.__data.map1)
end
@doc """
Return a list of the pairs `{key, value}` taking the directed internal map. This function is used by `inspect` to print the DualMap.
If you also pass the option `:pairs_inverted`, the list returned will have the pairs with key/value taking de inverted internal map.
"""
@spec to_list(t()) :: [{any(), any()}]
def to_list(dual_map, option \\ nil)
def to_list(dual_map, :pairs_inverted) do
Map.to_list(dual_map.__data.map2)
end
def to_list(dual_map, _) do
Map.to_list(dual_map.__data.map1)
end
@doc """
Just as `get/3` but do not accept default parameter. If the key exists in any of the internal maps, will return the tuple `{:ok, value}`, if not will return `:error`.
## Examples
iex> dm = DualMap.new([
{"ns3", "192.168.0.4"},
{"ns2", "192.168.0.3"},
{"ns1", "192.168.0.2"}
])
iex> DualMap.fetch(dm, "192.168.0.4")
{:ok, "ns3"}
iex> DualMap.fetch(dm, "ns3")
{:ok, "192.168.0.4"}
iex> DualMap.fetch(dm, "192.168.0.6")
:error
"""
@spec fetch(t(), any()) :: {:ok, any()} | :error
def fetch(dual_map, key) do
case Map.fetch(dual_map.__data.map1, key) do
{:ok, _} = return -> return
:error -> Map.fetch(dual_map.__data.map2, key)
end
end
@doc """
Work equals to `fetch/3` but return a value (as `get/3`) or erroring out if key doesn't exists.
"""
@spec fetch!(t(), any()) :: any()
def fetch!(dual_map, key) do
case Map.fetch(dual_map.__data.map1, key) do
:error -> Map.fetch!(dual_map.__data.map2, key)
return -> return
end
end
@doc """
Checks if two DualMaps are equal.
Two maps are considered to be equal if both internal maps contains the same keys and values.
"""
@spec equal?(t(), t()) :: boolean()
def equal?(dual_map1, dual_map2), do:
dual_map1 == dual_map2
@doc """
Return de size of the DualMap counting the number of pairs.
"""
@spec count(t()) :: pos_integer()
def count(dual_map) do
map_size(dual_map.__data.map1)
end
@doc """
Checks if `key` exists within DualMap.
## Examples
iex> dm = DualMap.new([
{"ns3", "192.168.0.4"},
{"ns2", "192.168.0.3"},
{"ns1", "192.168.0.2"}
])
[
{"ns1", "192.168.0.2"},
{"ns2", "192.168.0.3"},
{"ns3", "192.168.0.4"}
]
iex> DualMap.has?(dm, "192.168.0.4")
true
iex> DualMap.has?(dm, "ns2")
true
iex> DualMap.has?(dm, "ns5")
false
"""
@spec has?(t(), key :: any()) :: boolean()
def has?(dual_map, key_value) do
match?(%{^key_value => _}, dual_map.__data.map1)
or
match?(%{^key_value => _}, dual_map.__data.map2)
end
@doc """
Checks if the pair `key_value` (a tuple size 2) exists in any of the internal maps either as key => value or as value => key.
## Examples
iex> dm = DualMap.new([
{"ns3", "192.168.0.4"},
{"ns2", "192.168.0.3"},
{"ns1", "192.168.0.2"}
])
[
{"ns1", "192.168.0.2"},
{"ns2", "192.168.0.3"},
{"ns3", "192.168.0.4"}
]
iex> DualMap.member?(dm, {"192.168.0.4", "ns3"})
true
iex> DualMap.member?(dm, {"ns3", "192.168.0.4"})
true
iex> DualMap.member?(dm, {"ns1", "192.168.0.4"})
false
"""
@spec member?(t(), key_value :: {any(), any()}) :: boolean()
def member?(dual_map, {key, value}) do
match?(%{^key => ^value}, dual_map.__data.map1)
or
match?(%{^key => ^value}, dual_map.__data.map2)
end
end
defimpl Inspect, for: DualMap do
import Inspect.Algebra
def inspect(dual_map, opts) do
to_doc(DualMap.to_list(dual_map), opts)
end
end