defmodule BiMultiMap do
@moduledoc """
Bi-directional multimap implementation backed by two multimaps.
Entries in bimap do not follow any order.
BiMultiMaps do not impose any restriction on the key and value type: anything
can be a key in a bimap, and also anything can be a value.
BiMultiMaps differ from `BiMap`s by disallowing duplicates only among key-value
pairs, not among keys and values separately. This means it is possible to store
`[(A, B), (A, C)]` or `[(X, Z), (Y, Z)]` in BiMultiMap.
Keys and values are compared using the exact-equality operator (`===`).
## Example
iex> mm = BiMultiMap.new(a: 1, b: 2, b: 1)
#BiMultiMap<[a: 1, b: 1, b: 2]>
iex> BiMultiMap.get(mm, :a)
[1]
iex> BiMultiMap.get_keys(mm, 1)
[:a, :b]
iex> BiMultiMap.put(mm, :a, 3)
#BiMultiMap<[a: 1, a: 3, b: 1, b: 2]>
## Protocols
`BiMultiMap` implements `Enumerable`, `Collectable` and `Inspect` protocols.
"""
@typedoc "Key type"
@type k :: term
@typedoc "Value type"
@type v :: term
@typedoc false
@opaque internal_keys(k, v) :: %{optional(k) => MapSet.t(v)}
@typedoc false
@opaque internal_values(k, v) :: %{optional(v) => MapSet.t(k)}
@typedoc false
@opaque internal_size :: non_neg_integer
@type t(k, v) :: %BiMultiMap{
keys: internal_keys(k, v),
values: internal_values(k, v),
size: internal_size
}
@type t :: t(term, term)
defstruct keys: %{}, values: %{}, size: 0
@doc """
Creates a new bimultimap.
## Examples
iex> BiMultiMap.new
#BiMultiMap<[]>
"""
@spec new :: t
def new, do: %BiMultiMap{}
@doc """
Creates a bimultimap from `enumerable` of key-value pairs.
Duplicated pairs are removed; the latest one prevails.
## Examples
iex> BiMultiMap.new([a: 1, a: 2])
#BiMultiMap<[a: 1, a: 2]>
"""
@spec new(Enum.t()) :: t
def new(enumerable)
def new(%BiMultiMap{} = bimultimap), do: bimultimap
def new(enum) do
Enum.reduce(enum, new(), fn pair, bimultimap ->
BiMultiMap.put(bimultimap, pair)
end)
end
@doc """
Creates a bimultimap from `enumerable` via transform function returning
key-value pairs.
## Examples
iex> BiMultiMap.new([1, 2, 1], fn x -> {x, x * 2} end)
#BiMultiMap<[{1, 2}, {2, 4}]>
"""
@spec new(Enum.t(), (term -> {k, v})) :: t
def new(enumerable, transform)
def new(enum, f) do
Enum.reduce(enum, new(), fn term, bimultimap ->
BiMultiMap.put(bimultimap, f.(term))
end)
end
@doc """
Returns the number of elements in `bimultimap`.
The size of a bimultimap is the number of key-value pairs that the map
contains.
## Examples
iex> BiMultiMap.size(BiMultiMap.new)
0
iex> bimultimap = BiMultiMap.new([a: "foo", a: "bar"])
iex> BiMultiMap.size(bimultimap)
2
"""
@spec size(t) :: non_neg_integer
def size(bimultimap)
def size(%BiMultiMap{size: size}), do: size
@doc """
Returns `key ➜ [value]` mapping of `bimultimap`.
## Examples
iex> bimultimap = BiMultiMap.new([a: "foo", b: "bar", b: "moo"])
iex> BiMultiMap.left(bimultimap)
%{a: ["foo"], b: ["bar", "moo"]}
"""
@spec left(t) :: %{k => [v]}
def left(bimultimap)
def left(%BiMultiMap{keys: keys}) do
for {k, vs} <- keys, into: %{} do
{k, MapSet.to_list(vs)}
end
end
@doc """
Returns `value ➜ key` mapping of `bimultimap`.
## Examples
iex> bimultimap = BiMultiMap.new([a: "foo", b: "bar", c: "bar"])
iex> BiMultiMap.right(bimultimap)
%{"foo" => [:a], "bar" => [:b, :c]}
"""
@spec right(t) :: %{v => [k]}
def right(bimultimap)
def right(%BiMultiMap{values: values}) do
for {v, ks} <- values, into: %{} do
{v, MapSet.to_list(ks)}
end
end
@doc """
Returns all unique keys from `bimultimap`.
## Examples
iex> bimultimap = BiMultiMap.new([a: 1, b: 2, b: 3])
iex> BiMultiMap.keys(bimultimap)
[:a, :b]
"""
@spec keys(t) :: [k]
def keys(bimultimap)
def keys(%BiMultiMap{keys: keys}), do: Map.keys(keys)
@doc """
Returns all unique values from `bimultimap`.
## Examples
iex> bimultimap = BiMultiMap.new([a: 1, b: 2, c: 2])
iex> BiMultiMap.values(bimultimap)
[1, 2]
"""
@spec values(t) :: [v]
def values(bimultimap)
def values(%BiMultiMap{values: values}), do: Map.keys(values)
@doc """
Checks if `bimultimap` contains `{key, value}` pair.
## Examples
iex> bimultimap = BiMultiMap.new([a: "foo", a: "moo", b: "bar"])
iex> BiMultiMap.member?(bimultimap, :a, "foo")
true
iex> BiMultiMap.member?(bimultimap, :a, "moo")
true
iex> BiMultiMap.member?(bimultimap, :a, "bar")
false
"""
@spec member?(t, k, v) :: boolean
def member?(bimultimap, key, value)
def member?(%BiMultiMap{keys: keys}, key, value) do
Map.has_key?(keys, key) and value in keys[key]
end
@doc """
Convenience shortcut for `member?/3`.
"""
@spec member?(t, {k, v}) :: boolean
def member?(bimultimap, kv)
def member?(bimultimap, {key, value}), do: member?(bimultimap, key, value)
@doc """
Checks if `bimultimap` contains `key`.
## Examples
iex> bimultimap = BiMultiMap.new([a: "foo", b: "bar"])
iex> BiMultiMap.has_key?(bimultimap, :a)
true
iex> BiMultiMap.has_key?(bimultimap, :x)
false
"""
@spec has_key?(t, k) :: boolean
def has_key?(bimultimap, key)
def has_key?(%BiMultiMap{keys: keys}, left) do
Map.has_key?(keys, left)
end
@doc """
Checks if `bimultimap` contains `value`.
## Examples
iex> bimultimap = BiMultiMap.new([a: "foo", b: "bar"])
iex> BiMultiMap.has_value?(bimultimap, "foo")
true
iex> BiMultiMap.has_value?(bimultimap, "moo")
false
"""
@spec has_value?(t, v) :: boolean
def has_value?(bimultimap, value)
def has_value?(%BiMultiMap{values: values}, value) do
Map.has_key?(values, value)
end
@doc """
Checks if two bimultimaps are equal.
Two bimultimaps are considered to be equal if they contain the same keys and
those keys are bound with the same values.
## Examples
iex> Map.equal?(BiMultiMap.new([a: 1, b: 2, b: 3]), BiMultiMap.new([b: 2, b: 3, a: 1]))
true
iex> Map.equal?(BiMultiMap.new([a: 1, b: 2, b: 3]), BiMultiMap.new([b: 1, b: 3, a: 2]))
false
"""
@spec equal?(t, t) :: boolean
def equal?(bimultimap1, bimultimap2)
def equal?(%BiMultiMap{keys: keys1}, %BiMultiMap{keys: keys2}) do
Map.equal?(keys1, keys2)
end
@doc """
Gets all values for specific `key` in `bimultimap`
If `key` is present in `bimultimap` with values `values`, then `values` are
returned. Otherwise, `default` is returned (which is `[]` unless specified
otherwise).
## Examples
iex> BiMultiMap.get(BiMultiMap.new(), :a)
[]
iex> bimultimap = BiMultiMap.new([a: 1, c: 1, c: 2])
iex> BiMultiMap.get(bimultimap, :a)
[1]
iex> BiMultiMap.get(bimultimap, :b)
[]
iex> BiMultiMap.get(bimultimap, :b, 3)
3
iex> BiMultiMap.get(bimultimap, :c)
[1, 2]
"""
@spec get(t, k, any) :: [v] | any
def get(bimultimap, key, default \\ [])
def get(%BiMultiMap{keys: keys}, key, default) do
case Map.fetch(keys, key) do
{:ok, values} -> MapSet.to_list(values)
:error -> default
end
end
@doc """
Gets all keys for specific `value` in `bimultimap`
This function is exact mirror of `get/3`.
## Examples
iex> BiMultiMap.get_keys(BiMultiMap.new, 1)
[]
iex> bimultimap = BiMultiMap.new([a: 1, c: 3, d: 3])
iex> BiMultiMap.get_keys(bimultimap, 1)
[:a]
iex> BiMultiMap.get_keys(bimultimap, 2)
[]
iex> BiMultiMap.get_keys(bimultimap, 2, :b)
:b
iex> BiMultiMap.get_keys(bimultimap, 3)
[:c, :d]
"""
@spec get_keys(t, v, any) :: [k] | any
def get_keys(bimultimap, value, default \\ [])
def get_keys(%BiMultiMap{values: values}, value, default) do
case Map.fetch(values, value) do
{:ok, keys} -> MapSet.to_list(keys)
:error -> default
end
end
@doc """
Fetches all values for specific `key` in `bimultimap`
If `key` is present in `bimultimap` with values `values`, then `{:ok, values}`
is returned. Otherwise, `:error` is returned.
## Examples
iex> BiMultiMap.fetch(BiMultiMap.new(), :a)
:error
iex> bimultimap = BiMultiMap.new([a: 1, c: 1, c: 2])
iex> BiMultiMap.fetch(bimultimap, :a)
{:ok, [1]}
iex> BiMultiMap.fetch(bimultimap, :b)
:error
iex> BiMultiMap.fetch(bimultimap, :c)
{:ok, [1, 2]}
"""
@spec fetch(t, k) :: {:ok, [v]} | :error
def fetch(bimultimap, key)
def fetch(%BiMultiMap{keys: keys}, key) do
case Map.fetch(keys, key) do
{:ok, values} -> {:ok, MapSet.to_list(values)}
:error -> :error
end
end
@doc """
Fetches all values for specific `key` in `bimultimap`
Raises `ArgumentError` if the key is absent.
## Examples
iex> bimultimap = BiMultiMap.new([a: 1, c: 1, c: 2])
iex> BiMultiMap.fetch!(bimultimap, :a)
[1]
iex> BiMultiMap.fetch!(bimultimap, :c)
[1, 2]
"""
@spec fetch!(t, k) :: [v]
def fetch!(bimultimap, key)
def fetch!(bimultimap, key) do
case fetch(bimultimap, key) do
{:ok, values} -> values
:error -> raise ArgumentError, "key #{inspect(key)} not found in: #{inspect(bimultimap)}"
end
end
@doc """
Fetches all keys for specific `value` in `bimultimap`
This function is exact mirror of `fetch/2`.
## Examples
iex> BiMultiMap.fetch_keys(BiMultiMap.new, 1)
:error
iex> bimultimap = BiMultiMap.new([a: 1, c: 3, d: 3])
iex> BiMultiMap.fetch_keys(bimultimap, 1)
{:ok, [:a]}
iex> BiMultiMap.fetch_keys(bimultimap, 2)
:error
iex> BiMultiMap.fetch_keys(bimultimap, 3)
{:ok, [:c, :d]}
"""
@spec fetch_keys(t, v) :: {:ok, [k]} | :error
def fetch_keys(bimultimap, value)
def fetch_keys(%BiMultiMap{values: values}, value) do
case Map.fetch(values, value) do
{:ok, keys} -> {:ok, MapSet.to_list(keys)}
:error -> :error
end
end
@doc """
Fetches all keys for specific `value` in `bimultimap`
Raises `ArgumentError` if the key is absent. This function is exact mirror of `fetch!/2`.
## Examples
iex> bimultimap = BiMultiMap.new([a: 1, c: 3, d: 3])
iex> BiMultiMap.fetch_keys!(bimultimap, 1)
[:a]
iex> BiMultiMap.fetch_keys!(bimultimap, 3)
[:c, :d]
"""
@spec fetch_keys!(t, v) :: [k]
def fetch_keys!(bimultimap, value)
def fetch_keys!(bimultimap, value) do
case fetch_keys(bimultimap, value) do
{:ok, keys} ->
keys
:error ->
raise ArgumentError, "value #{inspect(value)} not found in: #{inspect(bimultimap)}"
end
end
@doc """
Inserts `{key, value}` pair into `bimultimap`.
If `{key, value}` is already in `bimultimap`, it is deleted.
## Examples
iex> bimultimap = BiMultiMap.new
#BiMultiMap<[]>
iex> bimultimap = BiMultiMap.put(bimultimap, :a, 1)
#BiMultiMap<[a: 1]>
iex> bimultimap = BiMultiMap.put(bimultimap, :a, 2)
#BiMultiMap<[a: 1, a: 2]>
iex> BiMultiMap.put(bimultimap, :b, 2)
#BiMultiMap<[a: 1, a: 2, b: 2]>
"""
@spec put(t, k, v) :: t
def put(
%BiMultiMap{keys: keys, values: values, size: size} = bimultimap,
key,
value
) do
{upd, keys} = put_side(keys, key, value)
{^upd, values} = put_side(values, value, key)
size =
if upd do
size + 1
else
size
end
%{bimultimap | keys: keys, values: values, size: size}
end
defp put_side(keys, key, value) do
Map.get_and_update(keys, key, fn
nil -> {true, MapSet.new([value])}
set -> {!MapSet.member?(set, value), MapSet.put(set, value)}
end)
end
@doc """
Convenience shortcut for `put/3`
"""
@spec put(t, {k, v}) :: t
def put(bimultimap, kv)
def put(bimultimap, {key, value}), do: put(bimultimap, key, value)
@doc """
Deletes `{key, value}` pair from `bimultimap`.
If the `key` does not exist, or `value` does not match, returns `bimultimap`
unchanged.
## Examples
iex> bimultimap = BiMultiMap.new([a: 1, b: 2, c: 2])
iex> BiMultiMap.delete(bimultimap, :b, 2)
#BiMultiMap<[a: 1, c: 2]>
iex> BiMultiMap.delete(bimultimap, :c, 3)
#BiMultiMap<[a: 1, b: 2, c: 2]>
"""
@spec delete(t, k, v) :: t
def delete(
%BiMultiMap{keys: keys, values: values, size: size} = bimultimap,
key,
value
) do
{upd, keys} = delete_side(keys, key, value)
{^upd, values} = delete_side(values, value, key)
size =
if upd do
size - 1
else
size
end
%{bimultimap | keys: keys, values: values, size: size}
end
defp delete_side(keys, key, value) do
case Map.fetch(keys, key) do
{:ok, set} ->
upd = MapSet.member?(set, value)
set = MapSet.delete(set, value)
keys =
if MapSet.size(set) == 0 do
Map.delete(keys, key)
else
put_in(keys[key], set)
end
{upd, keys}
:error ->
{false, keys}
end
end
@doc """
Deletes `{key, _}` pair from `bimultimap`.
If the `key` does not exist, returns `bimultimap` unchanged.
## Examples
iex> bimultimap = BiMultiMap.new([a: 1, b: 2, b: 3])
iex> BiMultiMap.delete_key(bimultimap, :b)
#BiMultiMap<[a: 1]>
iex> BiMultiMap.delete_key(bimultimap, :c)
#BiMultiMap<[a: 1, b: 2, b: 3]>
"""
@spec delete_key(t, k) :: t
def delete_key(%BiMultiMap{keys: keys} = bimultimap, key) do
case Map.fetch(keys, key) do
{:ok, values} ->
Enum.reduce(values, bimultimap, fn value, map ->
delete(map, key, value)
end)
:error ->
bimultimap
end
end
@doc """
Deletes `{_, value}` pair from `bimultimap`.
If the `value` does not exist, returns `bimultimap` unchanged.
## Examples
iex> bimultimap = BiMultiMap.new([a: 1, b: 2, c: 1])
iex> BiMultiMap.delete_value(bimultimap, 1)
#BiMultiMap<[b: 2]>
iex> BiMultiMap.delete_value(bimultimap, 3)
#BiMultiMap<[a: 1, b: 2, c: 1]>
"""
@spec delete_value(t, v) :: t
def delete_value(%BiMultiMap{values: values} = bimultimap, value) do
case Map.fetch(values, value) do
{:ok, keys} ->
Enum.reduce(keys, bimultimap, fn key, map ->
delete(map, key, value)
end)
:error ->
bimultimap
end
end
@doc """
Convenience shortcut for `delete/3`.
"""
@spec delete(t, {k, v}) :: t
def delete(bimultimap, kv)
def delete(bimultimap, {key, value}), do: delete(bimultimap, key, value)
@doc """
Returns list of unique key-value pairs in `bimultimap`.
## Examples
iex> bimultimap = BiMultiMap.new([a: "foo", b: "bar"])
iex> BiMultiMap.to_list(bimultimap)
[a: "foo", b: "bar"]
"""
@spec to_list(t) :: [{k, v}]
def to_list(bimultimap)
def to_list(%BiMultiMap{keys: keys}) do
for {k, vs} <- keys, v <- vs do
{k, v}
end
end
defimpl Enumerable do
def reduce(bimultimap, acc, fun) do
Enumerable.List.reduce(BiMultiMap.to_list(bimultimap), acc, fun)
end
def member?(bimultimap, val) do
{:ok, BiMultiMap.member?(bimultimap, val)}
end
def count(bimultimap) do
{:ok, BiMultiMap.size(bimultimap)}
end
def slice(_bimultimap) do
{:error, __MODULE__}
end
end
defimpl Collectable do
def into(original) do
{original,
fn
bimultimap, {:cont, pair} -> BiMultiMap.put(bimultimap, pair)
bimultimap, :done -> bimultimap
_, :halt -> :ok
end}
end
end
defimpl Inspect do
import Inspect.Algebra
def inspect(bimultimap, opts) do
concat([
"#BiMultiMap<",
Inspect.List.inspect(BiMultiMap.to_list(bimultimap), opts),
">"
])
end
end
end