lib/solver/domain/default_domain.ex

defmodule CPSolver.DefaultDomain do
  alias CPSolver.Common

  @spec new(Enum.t()) :: :gb_sets.set(number())
  def new([]) do
    throw(:empty_domain)
  end

  def new(domain) when is_integer(domain) do
    :gb_sets.from_list([domain])
  end

  def new(domain) do
    (:gb_sets.is_set(domain) && domain) ||
      Enum.reduce(domain, :gb_sets.new(), fn v, acc -> :gb_sets.add_element(v, acc) end)
  end

  @spec size(:gb_sets.set(number())) :: non_neg_integer
  def size(domain) do
    :gb_sets.size(domain)
  end

  @spec fixed?(:gb_sets.set(number())) :: boolean
  def fixed?(domain) do
    size(domain) == 1
  end

  @spec min(:gb_sets.set(number())) :: number()
  def min(domain) do
    :gb_sets.smallest(domain)
  end

  @spec max(domain :: :gb_sets.set(number())) :: number()
  def max(domain) do
    :gb_sets.largest(domain)
  end

  @spec contains?(:gb_sets.set(number()), number()) :: boolean
  def contains?(domain, value) do
    :gb_sets.is_member(value, domain)
  end

  @spec remove(:gb_sets.set(number()), number()) ::
          :fail
          | :no_change
          | {Common.domain_change(), :gb_sets.set(number())}
  def remove(domain, value) do
    :gb_sets.delete_any(value, domain)
    |> post_remove(domain, :domain_change)
  end

  @spec removeAbove(:gb_sets.set(number()), number()) ::
          :fail
          | :no_change
          | {Common.domain_change(), :gb_sets.set(number())}

  def removeAbove(domain, value) do
    :gb_sets.filter(fn v -> v <= value end, domain)
    |> post_remove(domain, :max_change)
  end

  @spec removeBelow(:gb_sets.set(number()), number()) ::
          :fail | :no_change | {Common.domain_change(), :gb_sets.set(number())}
  def removeBelow(domain, value) do
    :gb_sets.filter(fn v -> v >= value end, domain)
    |> post_remove(domain, :min_change)
  end

  @spec fix(:gb_sets.set(any), number()) :: :fail | {:fixed, :gb_sets.set(number())}
  def fix(domain, value) do
    if contains?(domain, value) do
      {:fixed, :gb_sets.from_list([value])}
    else
      :no_change
    end
  end

  defp post_remove(new_domain, domain, change_kind) do
    case size(new_domain) do
      0 ->
        :fail

      new_size ->
        case size(domain) do
          old_size when old_size == new_size ->
            :no_change

          old_size when old_size > new_size ->
            {(new_size == 1 && :fixed) || change_kind, new_domain}
        end
    end
  end
end