defmodule Bsearch do @moduledoc """ Module that contains the functions to use the Binary search using tuples. """ @doc """ Returns true of false of whether a tuple contains a certain value. ## Examples iex> Bsearch.member?({1, 2, 3}, 1) true iex> Bsearch.member?({1, 2, 3}, 2) true iex> Bsearch.member?({1, 2, 3}, 3) true iex> Bsearch.member?({1, 2, 3}, 0) false iex> Bsearch.member?({1, 2, 3}, 4) false """ @spec member?(tuple(), any()) :: boolean() def member?({}, _), do: false def member?(tuple, value) when is_tuple(tuple) do with first_value <- elem(tuple, 0), false <- value == first_value, true <- value > first_value, high_index <- tuple_size(tuple) - 1, last_value <- elem(tuple, high_index), false <- value == last_value, true <- value < last_value do _member(tuple, value, 1, high_index - 1) else value -> value end end defp _member(_, _, low_index, high_index) when low_index > high_index, do: false defp _member(tuple, value, low_index, high_index) do mid_index = div(low_index + high_index, 2) mid_value = elem(tuple, mid_index) cond do value < mid_value -> _member(tuple, value, low_index, mid_index - 1) value > mid_value -> _member(tuple, value, mid_index + 1, high_index) value == mid_value -> true end end @doc """ Returns the index of the position of a value inside the tuple, if the value is not present, it will return an tuple with error status. ## Examples iex> Bsearch.find_index({1, 2, 3}, 1) {:ok, 0} iex> Bsearch.find_index({1, 2, 3}, 2) {:ok, 1} iex> Bsearch.find_index({1, 2, 3}, 3) {:ok, 2} iex> Bsearch.find_index({1, 2, 3}, 0) {:error, :not_found} iex> Bsearch.find_index({1, 2, 3}, 4) {:error, :not_found} """ @spec find_index(tuple(), any()) :: {:ok, non_neg_integer()} | {:error, :not_found} def find_index({}, _), do: {:error, :not_found} def find_index(tuple, value) when is_tuple(tuple) do with {:gt, _} <- {compare(value, elem(tuple, 0)), 0}, high_index <- tuple_size(tuple) - 1, {:lt, _} <- {compare(value, elem(tuple, high_index)), high_index} do _find_index(tuple, value, 1, high_index - 1) else {:eq, index} -> {:ok, index} _ -> {:error, :not_found} end end @spec _find_index(tuple(), any(), non_neg_integer(), pos_integer()) :: {:ok, non_neg_integer()} | {:error, :not_found} defp _find_index(_, _, low_index, high_index) when low_index > high_index, do: {:error, :not_found} defp _find_index(tuple, value, low_index, high_index) do mid_index = div(low_index + high_index, 2) mid_value = elem(tuple, mid_index) case compare(value, mid_value) do :gt -> _find_index(tuple, value, mid_index + 1, high_index) :lt -> _find_index(tuple, value, low_index, mid_index - 1) _ -> {:ok, mid_index} end end @spec compare(any(), any()) :: :gt | :lt | :eq defp compare(value1, value2) when value1 > value2, do: :gt defp compare(value1, value2) when value1 < value2, do: :lt defp compare(_, _), do: :eq @doc """ Binary search only works on ordered tuples, so this function normalizes a tuple or a list ## Examples iex> Bsearch.normalize([3, 2, 1]) {1, 2, 3} iex> Bsearch.normalize([2, 3, 2]) {2, 3} iex> Bsearch.normalize({3, 2, 1}) {1, 2, 3} iex> Bsearch.normalize({3, 3, 1}) {1, 3} """ @spec normalize(list() | tuple()) :: tuple() def normalize({}), do: {} def normalize([]), do: {} def normalize({one}), do: {one} def normalize([one]), do: {one} def normalize(list) when is_list(list) do list |> Enum.sort() |> Enum.uniq() |> List.to_tuple() end def normalize(tuple) when is_tuple(tuple) do tuple |> Tuple.to_list() |> normalize end end