Packages
better_weighted_random
0.1.0
Weighted random pick library optimised for quick take_one and take_n operations.
Current section
Files
Jump to
Current section
Files
lib/weighted_random/search_table.ex
defmodule WeightedRandom.SearchTable do
@moduledoc """
Search table maps value ranges to some result (ex. score).
Example of such table: [{:neg_inf, 0}, {-30, 4}, {-20, 8}, {-10, 10}, {10, 7}, {20, 0}]
It will give such results with 'search' function:
-50 -> 0
-30 -> 0
-29 -> 4
-15 -> 8
-10 -> 8
0 -> 10
10 -> 7
15 -> 7
30 -> 0
"""
@type t :: [{:neg_inf, any()} | list({number(), any()})] | %__MODULE__{}
@enforce_keys [:search_tree]
defstruct [:search_tree]
@doc """
Compile search table into structure optimised for quick search.
"""
@spec compile([{:neg_inf, any()} | list({number(), any()})]) :: %__MODULE__{}
def compile([{:neg_inf, _} | _] = table) do
search_tree = build_subtree(table)
%__MODULE__{search_tree: search_tree}
end
@spec decompile(%__MODULE__{}) :: [{:neg_inf, any()} | list({number(), any()})]
def decompile(%__MODULE{search_tree: search_tree}) do
decompile_impl(search_tree, [])
end
@doc """
Search for value in ranges.
## Examples
iex> SearchTable.search([{:neg_inf, 1}, {0, 5}], 0)
5
iex> SearchTable.search([{:neg_inf, 1}, {0, 5}], -9)
1
iex> SearchTable.search([{:neg_inf, 1}, {0, 5}], 3)
5
iex> SearchTable.search([{:neg_inf, 1}, {-7, 5}, {7, 3}], -7)
1
iex> SearchTable.search([{:neg_inf, 1}, {-7, 5}, {7, 3}], 0)
5
iex> SearchTable.search([{:neg_inf, 1}, {-7, 5}, {7, 3}], 7)
3
iex> SearchTable.search([{:neg_inf, 1}, {-7, 5}, {7, 3}], 10)
3
"""
@spec search(__MODULE__.t(), number()) :: any()
def search(table_or_compiled, search_value)
def search(%__MODULE{search_tree: {_, element, _, _} = search_tree}, search_value) do
search_subtree(search_tree, search_value, element)
end
def search([{:neg_inf, initial} | table], search_value) do
Enum.reduce_while(table, initial, fn {lower_bound, new_value}, current_value ->
if is_less_lower_bound(lower_bound, search_value) do
{:halt, current_value}
else
{:cont, new_value}
end
end)
end
defp build_subtree([]), do: nil
defp build_subtree([{lower_bound, element}]), do: {lower_bound, element, nil, nil}
defp build_subtree(table) do
{left_table, [{lower_bound, element} | right_table]} = Enum.split(table, round(length(table) / 2) - 1)
left_subtree = build_subtree(left_table)
right_subtree = build_subtree(right_table)
{lower_bound, element, left_subtree, right_subtree}
end
defp search_subtree(nil, _search_value, current_element), do: current_element
defp search_subtree({lower_bound, new_element, left_subtree, right_subtree}, search_value, current_element) do
if is_less_lower_bound(lower_bound, search_value) do
search_subtree(left_subtree, search_value, current_element)
else
search_subtree(right_subtree, search_value, new_element)
end
end
defp is_less_lower_bound(:neg_inf, _value), do: false
defp is_less_lower_bound(lower_bound, value) do
(lower_bound >= 0 and value < lower_bound) or (lower_bound < 0 and value <= lower_bound)
end
defp decompile_impl(nil, res), do: res
defp decompile_impl({lower_bound, element, left_subtree, right_subtree}, res) do
left_res = decompile_impl(left_subtree, res)
mid_res = [{lower_bound, element} | left_res]
decompile_impl(right_subtree, mid_res)
end
end