Packages
A simple dual-entry map
Retired package: Deprecated - Sorry, it was my first package. I am redesigning, refactoring and improving code
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 reverse one with the value => key pairs. At the same time it also stores metadata about the names (ids) of the datas (called master keys).
To create a new DualMap you must use the `DualMap.new` function. You must pass to it a pair of names that will be the identifiers of the master keys.
```elixir
DualMap.new({:hostname, :ip})
```
The order of the master keys is important. If you later want to make insertions into the DualMap and you use the `DualMap.put_ordered` function the value pairs will assume that they are ordered as defined at the time of creating the DualMap with `DualMap.new`.
## Let's see some examples:
```elixir
iex> dm = DualMap.new({:hostname, :ip})
[]
iex> DualMap.put_ordered(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, :ip, "192.168.0.3")
[
{"ns1", "192.168.0.2"},
{"ns3", "192.168.0.4"}
]
```
"""
defstruct [
__data: %{},
__master_keys_map: %{},
__ordered_master_keys: []
]
@typedoc "DualMap struct"
@type t :: %__MODULE__{}
@doc """
Returns an empty DualMap struct. The order of the master keys are important for posterior operations with the struct.
## Examples
iex> DualMap.new({:hostname, :ip})
[]
"""
@spec new(master_keys :: {master_key1 :: any(), master_key2 :: any()}) :: t()
def new({master_key1, master_key2}) do
%DualMap{
__data: %{
master_key1 => %{},
master_key2 => %{},
},
__master_keys_map: %{
master_key1 => master_key2,
master_key2 => master_key1
},
__ordered_master_keys: [master_key1, master_key2]
}
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({:hostname, :ip}, {"ns1", "192.168.0.2"})
[{"ns1", "192.168.0.2"}]
# Initializing with more than one pair of values
iex> DualMap.new({:hostname, :ip}, [
{"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({master_key1 :: any(), master_key2 :: any()}, {any(), any()} | list(tuple())) :: t()
def new({_, _} = master_keys, values) when is_tuple(values) or is_list(values) do
new(master_keys)
|> put_ordered(values)
end
@doc """
Delete a pair of datas and returns the DualMap without that pair. The pair is found looking for `key` in the the internal map indexed by `master_key`.
## Examples
iex> dm = DualMap.new({:hostname, :ip}, [
{"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, :ip, "192.168.0.3")
[
{"ns1", "192.168.0.2"},
{"ns3", "192.168.0.4"}
]
"""
@spec delete(t(), master_key :: any(), key :: any()) :: t()
def delete(dual_map, master_key1, key1) do
IO.inspect dual_map.__data[master_key1][key1]
case dual_map.__data[master_key1][key1] do
key2 when not is_nil(key2) ->
master_key2 = dual_map.__master_keys_map[master_key1]
map_key1 = Map.delete(dual_map.__data[master_key1], key1)
map_key2 = Map.delete(dual_map.__data[master_key2], key2)
data = %{dual_map.__data |
master_key1 => map_key1,
master_key2 => map_key2
}
%{dual_map | __data: data}
_ ->
dual_map
end
end
@doc """
Just the same as `delete/3` but you can pass a list of keys to delete.
## Examples
iex> dm = DualMap.new({:hostname, :ip}, [
{"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.drop(dm, :ip, ["192.168.0.3", "192.168.0.2"])
[{"ns3", "192.168.0.4"}]
"""
@spec drop(t(), master_key :: any(), list()) :: t()
def drop(dual_map, master_key1, list) do
list
|> Enum.reduce(dual_map, fn (key, dm) ->
delete(dm, master_key1, key)
end)
end
@doc """
Insert or replace one or more pairs of datas in a DualMap. If the third parameters is a list of tuples, every one is inserted/replaced in the DualMap secuentialy. With this function you need pass the a master_key to indicate which value of the tuple will be interpreted as key and which one as value.
## Examples
iex> dm = DualMap.new({:hostname, :ip})
[]
# Inserting/replacing many
iex> DualMap.put(dm, :ip, [
{"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, :ip, {"192.168.0.4", "ns3"})
[{"ns3", "192.168.0.4"}]
"""
@spec put(t(), master_key :: any(), {key :: any(), value :: any()} | list(tuple())) :: t()
def put(dual_map, _, []), do: dual_map
def put(dual_map, master_key1, [{key, value} | rest]) do
dual_map = put(dual_map, master_key1, {key, value})
put(dual_map, master_key1, rest)
end
def put(dual_map, master_key1, {key, value}) do
master_key2 = dual_map.__master_keys_map[master_key1]
map_key1 = Map.put(dual_map.__data[master_key1], key, value)
map_key2 = Map.put(dual_map.__data[master_key2], value, key)
data = %{dual_map.__data |
master_key1 => map_key1,
master_key2 => map_key2
}
%{dual_map | __data: data}
end
@doc """
This function is similar to `put/3` but you does not need pass a master key because the function will assume that you are sending the keys and values in the same order as you defined when you created the DualMap with `new/1` or `new/2`.
## Examples
iex> dm = DualMap.new({:hostname, :ip})
[]
iex> DualMap.put_ordered(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"}
]
"""
@spec put_ordered(t(), {any(), any()} | list(tuple())) :: t()
def put_ordered(dual_map, {_, _} = pair) do
[master_key1, _] = dual_map.__ordered_master_keys
put(dual_map, master_key1, pair)
end
def put_ordered(dual_map, []), do: dual_map
def put_ordered(dual_map, [pair | rest]) do
dual_map = put_ordered(dual_map, pair)
put_ordered(dual_map, rest)
end
@spec put_ordered(t(), any(), any()) :: t()
def put_ordered(dual_map, value1, value2) do
put_ordered(dual_map, {value1, value2})
end
@doc """
Returns the value asociated to `key` taking the internal map indexed by `master_key`. If `key` does not exists in the map, `default` or `nil` is returned.
## Examples
iex> dm = DualMap.new({:hostname, :ip}, [
{"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, :ip, "192.168.0.4")
"ns3"
"""
@spec get(t(), master_key :: any(), key :: any(), default :: any()) :: any()
def get(dual_map, master_key, key, default \\ nil) do
Map.get(dual_map.__data[master_key], key, default)
end
@doc """
Return the complete internal map indexed by `master_key`.
## Examples
iex> dm = DualMap.new({:hostname, :ip}, [
{"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, :hostname)
%{
"ns1" => "192.168.0.2",
"ns2" => "192.168.0.3",
"ns3" => "192.168.0.4"
}
"""
@spec get_map(t(), master_key :: any()) :: map()
def get_map(dual_map, master_key) do
Map.fetch!(dual_map.__data, master_key)
end
@doc """
Return a list with all the keys of the internal map indexed by `master_key`.
## Examples
iex> dm = DualMap.new({:hostname, :ip}, [
{"ns3", "192.168.0.4"},
{"ns2", "192.168.0.3"},
{"ns1", "192.168.0.2"}
])
iex> DualMap.keys(dm, :hostname)
["ns1", "ns2", "ns3"]
"""
@spec keys(t(), master_key :: any()) :: list()
def keys(dual_map, master_key) do
Map.keys(dual_map.__data[master_key])
end
@doc """
Return a list with all the values of the internal map indexed by `master_key`.
## Examples
iex> dm = DualMap.new({:hostname, :ip}, [
{"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, :hostname)
["192.168.0.2", "192.168.0.3", "192.168.0.4"]
"""
@spec values(t(), master_key :: any()) :: list()
def values(dual_map, master_key) do
Map.values(dual_map.__data[master_key])
end
@doc """
Return a list of the pairs `{key, value}` taking the map indexed by the first master_key defined with `new/1` or `new/2`. This function is used by `inspect` to print the DualMap.
If you also pass an option `:pairs_inverted`, the list will have the pairs with key/value inverted because will take the internal map indexed by the second master_key defined with `new/1` or `new/2`.
"""
@spec to_list(t()) :: [{any(), any()}]
def to_list(dual_map, option \\ nil)
def to_list(dual_map, nil) do
[master_key, _] = dual_map.__ordered_master_keys
Map.to_list(dual_map.__data[master_key])
end
def to_list(dual_map, :pairs_inverted) do
[_, master_key] = dual_map.__ordered_master_keys
Map.to_list(dual_map.__data[master_key])
end
@doc """
Just as `get/4` but do not accept default parameter. If the key exists, will return the tuple `{:ok, value}` and if not `:error`.
## Examples
iex> dm = DualMap.new({:hostname, :ip}, [
{"ns3", "192.168.0.4"},
{"ns2", "192.168.0.3"},
{"ns1", "192.168.0.2"}
])
iex> DualMap.fetch(dm, :ip, "192.168.0.4")
{:ok, "ns3"}
iex> DualMap.fetch(dm, :ip, "192.168.0.6")
:error
"""
@spec fetch(t(), any(), any()) :: {:ok, any()} | :error
def fetch(dual_map, master_key, key) do
Map.fetch(dual_map.__data[master_key], key)
end
@doc """
Work equals to `fetch/3` but erroring out if key doesn't exists.
"""
@spec fetch!(t(), any(), any()) :: any()
def fetch!(dual_map, master_key, key) do
Map.fetch!(dual_map.__data[master_key], key)
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:
DualMap.to_list(dual_map1) == DualMap.to_list(dual_map2)
@doc """
Return de size of the DualMap counting the number of pairs.
"""
@spec count(t()) :: pos_integer()
def count(dual_map) do
[master_key, _] = dual_map.__ordered_master_keys
map_size(dual_map.__data[master_key])
end
@doc """
Checks if `key` exists within DualMap.
## Examples
iex> dm = DualMap.new({:hostname, :ip}, [
{"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
[master_key1, master_key2] = dual_map.__ordered_master_keys
match?(%{^key_value => _}, dual_map.__data[master_key1])
or
match?(%{^key_value => _}, dual_map.__data[master_key2])
end
@doc """
Checks if the pair `key_value` (tuple size 2) exists within DualMap either as key => value or as value => key.
## Examples
iex> dm = DualMap.new({:hostname, :ip}, [
{"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
[master_key1, master_key2] = dual_map.__ordered_master_keys
match?(%{^key => ^value}, dual_map.__data[master_key1])
or
match?(%{^key => ^value}, dual_map.__data[master_key2])
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