defmodule InPlace.BitSet do @moduledoc """ BitSet is close in functionality to MapSet with integer values a s members. The main difference is that BitSet has lower and upper bounds for set values that have to be defined at the time of creation (see new/2). """ alias InPlace.Array import Bitwise import InPlace.BitUtils def new(lower_bound, upper_bound) when is_integer(lower_bound) and is_integer(upper_bound) and lower_bound <= upper_bound do {:bit_vector, atomics} = bit_vector = :bit_vector.new(upper_bound - lower_bound + 1) %{ lower_bound: lower_bound, upper_bound: upper_bound, bit_vector: bit_vector, offset: -lower_bound, last_index: :atomics.info(atomics).size, size: Array.new(1, 0), minmax: Array.new(2) |> tap(fn arr -> Array.put(arr, 1, Array.inf()) Array.put(arr, 2, Array.negative_inf()) end) } end def put( %{ bit_vector: bit_vector, size: size, offset: offset, lower_bound: lower_bound, upper_bound: upper_bound } = set, element ) when is_integer(element) do if element < lower_bound or element > upper_bound do throw(:value_out_of_bounds) end position = value_to_offset(offset, element) if member_impl(set, position) do :ok else :bit_vector.set(bit_vector, position) update_min(set, element) update_max(set, element) Array.update(size, 1, fn current_size -> current_size + 1 end) end end def delete(%{offset: offset, bit_vector: bit_vector, size: size} = set, element) when is_integer(element) do position = value_to_offset(offset, element) if !member_impl(set, position) do :ok else :bit_vector.clear(bit_vector, position) maybe_tighten_min(set, element) maybe_tighten_max(set, element) Array.update(size, 1, fn current_size -> current_size - 1 end) end end def member?(%{offset: offset} = set, element) when is_integer(element) do offset_value = value_to_offset(offset, element) member_impl(set, offset_value) end defp member_impl(%{bit_vector: bit_vector} = _set, offset_value) do :bit_vector.get(bit_vector, offset_value) == 1 end defp offset_to_value(offset, value) do value - offset end defp value_to_offset(offset, value) do value + offset end def iterate(set, acc, reducer) when is_function(reducer, 2) do iterate_impl(set, acc, first_position(set), reducer) end defp iterate_impl(_set, acc, nil, _reducer) do acc end defp iterate_impl(set, acc, position, reducer) do case reducer.(value_at_position(set, position), acc) do {:halt, acc2} -> acc2 {:cont, acc2} -> iterate_impl(set, acc2, next_position(set, position), reducer) acc2 -> iterate_impl(set, acc2, next_position(set, position), reducer) end end def value_at_position(set, {block_idx, block_offset, _}) do value_at_position(set, block_idx, block_offset) end def value_at_position(set, {block_idx, block_offset}) do value_at_position(set, block_idx, block_offset) end def value_at_position(%{offset: offset} = _set, block_idx, block_offset) do offset_to_value( offset, (block_idx - 1) * 64 + block_offset ) end defp first_position(set) do if size(set) > 0 do next_position(set, 1, :lsb) end end defp last_block(%{offset: value_offset} = set) do case max(set) do nil -> 0 max_value -> value_address(max_value + value_offset) |> elem(0) end end defp get_block(%{bit_vector: {:bit_vector, atomics}} = _set, block_idx) do ## Note: we do not use Array.get/2 here because of possible mix-up ## between 0s and how Array differentiates between 0s and `nil`s :atomics.get(atomics, block_idx) end @doc """ Find next position for {block_idx, block_offset} """ def next_position(set, {block_idx, block_offset} = _value_position) do next_position(set, {block_idx, block_offset, nil}) end def next_position(set, {block_idx, block_offset, block_value} = _value_position) do next_position(set, block_idx, block_offset, block_value) end def next_position(set, block_idx, block_offset) do next_position(set, block_idx, block_offset, nil) end def next_position(set, block_idx, block_offset, block_value) do if block_idx <= last_block(set) do next_position_impl(set, block_idx, block_offset, block_value) end end ## The beginning of a new block def next_position_impl(set, block_idx, :lsb, _block_value) do case get_block(set, block_idx) do 0 -> next_position(set, block_idx + 1, :lsb) block_value -> case lsb(block_value) do nil -> next_position(set, block_idx + 1, :lsb) lsb -> {block_idx, lsb, block_value} end end end def next_position_impl( set, block_idx, block_offset, block_value ) do case block_value || get_block(set, block_idx) do 0 -> ## nothing in this block, try next one. next_position(set, block_idx + 1, :lsb) block_value -> ## Reset all leftmost bits up to block_offset ## Take LSB - this will give an offset of next bit set to 1 shift = block_offset + 1 block_tail = block_value >>> shift case block_tail |> lsb() do nil -> next_position(set, block_idx + 1, :lsb) new_block_offset -> {block_idx, new_block_offset + shift, block_value} end end end ## Find the index of atomics where the `n` value resides defp block_index(value) do div(value, 64) + 1 end ## Given the value, find block index (that is a position in bit vector) ## and an offset of the value relative to the beginning of the block def value_address(value) do {block_index(value), rem(value, 64)} end def to_list(set) do iterate(set, [], fn val, acc -> [val | acc] end) |> Enum.reverse() end def size(%{size: size} = _set) do Array.get(size, 1) end def empty?(set) do size(set) == 0 end def subset?(set1, set2) do size1 = size(set1) size2 = size(set2) if size1 > size2 do false else iterate(set1, true, fn val, _acc -> (member?(set2, val) && {:cont, true}) || {:halt, false} end) end end def equal?(set1, set2) do size(set1) == size(set2) && subset?(set1, set2) end def disjoint?(set1, set2) do size1 = size(set1) size2 = size(set2) if size1 == 0 || size2 == 0 do true else {smaller, bigger} = (size1 < size2 && {set1, set2}) || {set2, set1} iterate(smaller, true, fn value, _ -> (member?(bigger, value) && {:halt, false}) || {:cont, true} end) end end def intersection(set1, set2) do lb = min(set1.lower_bound, set2.lower_bound) ub = max(set1.upper_bound, set2.upper_bound) {smaller, bigger} = (size(set1) < size(set2) && {set1, set2}) || {set2, set1} new(lb, ub) |> tap(fn intersection_set -> iterate(smaller, nil, fn val, _ -> member?(bigger, val) && put(intersection_set, val) end) end) end def union(set1, set2) do lb = min(set1.lower_bound, set2.lower_bound) ub = max(set1.upper_bound, set2.upper_bound) new(lb, ub) |> tap(fn union_set -> iterate(set1, nil, fn val, _ -> put(union_set, val) end) iterate(set2, nil, fn val, _ -> put(union_set, val) end) end) end def difference(set1, set2) do filter(set1, fn val -> !member?(set2, val) end) end def symmetric_difference(set1, set2) do lb = min(set1.lower_bound, set2.lower_bound) ub = max(set1.upper_bound, set2.upper_bound) new(lb, ub) |> tap(fn sym_diff_set -> iterate(set1, nil, fn val, _ -> put(sym_diff_set, val) end) iterate(set2, nil, fn val, _ -> member?(sym_diff_set, val) && delete(sym_diff_set, val) || put(sym_diff_set, val) end) end) end def min(set) do if size(set) > 0 do min_impl(set) end end defp min_impl(%{minmax: minmax} = _set) do Array.get(minmax, 1) end defp update_min(%{minmax: minmax} = set, value) do if min_impl(set) > value do Array.put(minmax, 1, value) end :ok end defp maybe_tighten_min(%{minmax: minmax} = set, removed_value) do if min_impl(set) == removed_value do Array.put(minmax, 1, find_min(set, removed_value) || Array.inf()) end end defp find_min( %{offset: value_offset, last_index: last_idx} = set, starting_value ) do {min_block_idx, _min_block_offset} = value_address(starting_value + value_offset) _new_min_value = Enum.reduce_while(min_block_idx..last_idx, nil, fn block_idx, acc -> case get_block(set, block_idx) do 0 -> {:cont, acc} block_value -> {:halt, value_at_position(set, block_idx, lsb(block_value))} end end) end def max(set) do if size(set) > 0 do max_impl(set) end end defp max_impl(%{minmax: minmax} = _set) do Array.get(minmax, 2) end defp update_max(%{minmax: minmax} = set, value) do if max_impl(set) < value do Array.put(minmax, 2, value) end :ok end defp maybe_tighten_max(%{minmax: minmax} = set, removed_value) do if max_impl(set) == removed_value do Array.put(minmax, 2, find_max(set, removed_value) || Array.negative_inf()) end :ok end defp find_max(%{offset: value_offset} = set, starting_value) do {max_block_idx, _max_block_offset} = value_address(starting_value + value_offset) _new_max_value = Enum.reduce_while(1..max_block_idx, nil, fn block_idx, acc -> case get_block(set, block_idx) do 0 -> {:cont, acc} block_value -> {:halt, value_at_position(set, block_idx, msb(block_value))} end end) end def next(%{offset: value_offset} = set, element) do current_position = value_address(element + value_offset) case next_position(set, current_position) do nil -> nil {block_id, block_offset, _block_value} = _next_pos -> value_at_position(set, block_id, block_offset) end end def reduce(set, acc, reducer) when is_function(reducer, 2) do iterate(set, acc, reducer) end def filter(set, filter_fun) when is_function(filter_fun, 1) do new(set.lower_bound, set.upper_bound) |> tap(fn f_set -> iterate(set, nil, fn val, _ -> filter_fun.(val) && put(f_set, val) end) end) end end