Packages

Bitsets implementation

Current section

Files

Jump to
ex_bitset lib ex_bitset.ex
Raw

lib/ex_bitset.ex

defmodule ExBitset do
@moduledoc """
ExBitset provides the functionality to create/query bitarray sets.
To define a set constructor you first need to specify all possible
set elements. This is done via the defbitset macro:
```
defmodule Roles
import ExBitset, only: [defbitset: 1]
defbitset [:admin, :owner, :writer, :viewer, :guest]
end
```
Then, create a new set specifying all of the elements in it:
```
roles = ExBitset.new(Roles, [:admin, :owner])
```
To then query the set, you can use any of the following functions:
```
ExBitset.member?(roles, :owner) # => true
roles2 = ExBitset.new(Roles, [:admin, :writer])
ExBitset.union(roles, roles2) |> Enum.to_list() # => [:admin, :owner, :writer]
ExBitset.intersection(roles, roles2) |> Enum.to_list() # => [:admin]
ExBitset.subtract(role, roles2) |> Enum.to_list() # => [:owner]
# OR alternatively using Enum functions
Enum.join(roles, ", ") # => "admin, owner"
Enum.map(roles, &to_string/1) # => ["admin", "owner"]
```
"""
use Bitwise
defstruct [:mod, :value]
alias ExBitset.UndefinedItemError
@opaque t() :: %__MODULE__{
mod: module(),
value: non_neg_integer()
}
@type item() :: term()
@spec new(module(), [item()]) :: t()
def new(mod, items \\ []) do
base_items = mod.__bitset__()
value = Enum.reduce(items, 0, &(Bitwise.bsl(1, find_index(base_items, &1, mod)) + &2))
%__MODULE__{mod: mod, value: value}
end
@spec member?(t(), term()) :: boolean()
def member?(%__MODULE__{mod: mod, value: value}, item) do
index = find_index(mod.__bitset__(), item, mod)
(bsl(1, index) &&& value) > 0
end
@spec count(t()) :: non_neg_integer()
def count(%__MODULE__{value: value}) do
Stream.unfold(value, fn
0 -> nil
n -> {(n &&& 1) > 0, bsr(n, 1)}
end)
|> Enum.count(& &1)
end
@spec domain(t()) :: [item()]
def domain(%__MODULE__{mod: mod}) do
mod.__bitset__()
end
@spec to_list(t()) :: [item()]
def to_list(%__MODULE__{mod: mod, value: value}) do
base_items = mod.__bitset__()
Stream.unfold(value, fn
0 -> nil
n -> {(n &&& 1) > 0, bsr(n, 1)}
end)
|> Stream.with_index()
|> Stream.filter(fn {val, _index} -> val end)
|> Stream.map(fn {_val, index} -> Enum.at(base_items, index) end)
|> Enum.to_list()
end
@spec to_int(t()) :: non_neg_integer()
def to_int(%__MODULE__{value: value}) do
value
end
@spec from_int(module(), non_neg_integer()) :: t()
def from_int(mod, value) do
%__MODULE__{mod: mod, value: value}
end
@spec to_binary(t()) :: binary()
def to_binary(%__MODULE__{value: value}) do
:binary.encode_unsigned(value)
end
@spec from_binary(module(), binary()) :: t()
def from_binary(mod, bin) do
%__MODULE__{mod: mod, value: :binary.decode_unsigned(bin)}
end
@spec union(t(), t()) :: t()
def union(%__MODULE__{mod: mod, value: val1}, %__MODULE__{mod: mod, value: val2}) do
%__MODULE__{mod: mod, value: val1 ||| val2}
end
@spec intersection(t(), t()) :: t()
def intersection(%__MODULE__{mod: mod, value: val1}, %__MODULE__{mod: mod, value: val2}) do
%__MODULE__{mod: mod, value: val1 &&& val2}
end
@spec subtract(t(), t()) :: t()
def subtract(%__MODULE__{mod: mod, value: val1}, %__MODULE__{mod: mod, value: val2}) do
%__MODULE__{mod: mod, value: val1 &&& ~~~val2}
end
defp find_index(items, item, mod) do
case Enum.find_index(items, &(&1 == item)) do
nil -> raise UndefinedItemError, item: item, mod: mod
index -> index
end
end
defmacro defbitset(set_domain) do
quote do
def __bitset__ do
unquote(set_domain)
end
end
end
defimpl Enumerable, for: ExBitset do
@impl true
def count(bitset) do
{:ok, ExBitset.count(bitset)}
end
@impl true
def member?(bitset, element) do
{:ok, ExBitset.member?(bitset, element)}
end
@impl true
def reduce(bitset, acc, fun) do
Enumerable.reduce(ExBitset.to_list(bitset), acc, fun)
end
@impl true
def slice(bitset) do
set_domain = ExBitset.set_domain(bitset)
{:ok, ExBitset.count(bitset), fn start, length -> Enum.slice(set_domain, start, length) end}
end
end
defimpl Inspect, for: ExBitset do
import Inspect.Algebra
@impl true
def inspect(bitset, opts) do
modname = bitset.mod |> Module.split() |> List.last()
concat(["#ExBitset::#{modname}<", to_doc(ExBitset.to_list(bitset), opts), ">"])
end
end
end