lib/combinators.ex

defmodule Formulae.Combinators do
  @moduledoc """
  Functions to calculate all the combinations, permutations and
  repeated permutations of the list given.

  ## Examples

      iex> Formulae.Combinators.combinations(~w|a b c d|a, 2)
      [[:a, :b], [:a, :c], [:a, :d], [:b, :c], [:b, :d], [:c, :d]]
      iex> Formulae.Combinators.permutations(~w|a b c d|a, 2)
      [[:a, :b], [:a, :c], [:a, :d], [:b, :a], [:b, :c], [:b, :d],
       [:c, :a], [:c, :b], [:c, :d], [:d, :a], [:d, :b], [:d, :c]]
      iex> Formulae.Combinators.repeated_permutations(~w|a b c d|a, 2)
      [[:a, :a], [:a, :b], [:a, :c], [:a, :d], [:b, :a], [:b, :b],
       [:b, :c], [:b, :d], [:c, :a], [:c, :b], [:c, :c], [:c, :d],
       [:d, :a], [:d, :b], [:d, :c], [:d, :d]]

  **NB** this functions should not be used for relatively big `n` because
  they perform greedy evaluation. See to `Formulae.Combinators.Stream`
  for lazy analogues returning streams.

  ## Dynamic numbers

  To use combinations and permutations with dynamic number `n`, use
  `Formulae.combinations/2` and `Formulae.permutations/2`, generated
  in the compile time. By default, there are `42` clauses for combinations
  and `12` for `permutations` are generated. They might be changed in `config`,
  keys `:formulae, :max_combinations` and `:formulae, :max_permutations`. To
  suppress generation, use `:formulae, :generate_combinators, false`.

      iex> with n <- 2, do: Formulae.combinations(~w|a b c d|a, n)
      [[:a, :b], [:a, :c], [:a, :d], [:b, :c], [:b, :d], [:c, :d]]

  """

  import Formulae.Combinators.H

  @doc "Calculates all combinations of the list, given as the first parameter"
  @spec combinations(list :: list(), count :: non_neg_integer()) :: [list()]

  defmacro combinations(_, 0), do: []
  defmacro combinations(l, 1), do: quote(do: Enum.map(unquote(l), &[&1]))

  defmacro combinations(l, n) do
    guards = mapper(n, 2, &{:>, [context: Elixir, import: Kernel], [idx(&1), idx(&1 - 1)]})

    {:for, [],
     Enum.reverse([
       [do: mapper(1, n, &var/1)]
       | guards ++ mapper(n, 1, &for_clause(&1, l))
     ])}
  end

  @doc "Calculates all permutations of the list, given as the first parameter"
  @spec permutations(list :: list(), count :: non_neg_integer()) :: [list()]

  defmacro permutations(_, 0), do: []
  defmacro permutations(l, 1), do: quote(do: Enum.map(unquote(l), &[&1]))

  defmacro permutations(l, n) do
    guards =
      n
      |> mapper(1, &idx/1)
      |> combinations(2)
      |> Enum.map(&{:!=, [context: Elixir, import: Kernel], &1})

    {:for, [],
     Enum.reverse([
       [do: mapper(1, n, &var/1)]
       | guards ++ mapper(n, 1, &for_clause(&1, l))
     ])}
  end

  @doc "Calculates all repeated permutations of the list, given as the first parameter"
  @spec repeated_permutations(list :: list(), count :: non_neg_integer()) :: [list()]

  defmacro repeated_permutations(_, 0), do: []
  defmacro repeated_permutations(l, 1), do: quote(do: Enum.map(unquote(l), &[&1]))

  defmacro repeated_permutations(l, n) do
    clause = fn i -> {:<-, [], [var(i), l]} end
    {:for, [], Enum.reverse([[do: mapper(1, n, &var/1)] | mapper(n, 1, clause)])}
  end

  # def combinations([], _), do: []
  # def combinations(list, n) when length(list) < n, do: []
  # def combinations(list, n) when length(list) == n, do: [list]
  # def combinations(list, 1), do: Enum.map(list, &[&1])
  # def combinations(list, n), do: m_combinations(list, n)

  # def permutations([], _), do: []
  # def permutations(list, n) when length(list) < n, do: []
  # def permutations(list, 1), do: Enum.map(list, &[&1])
  # def permutations(list, n), do: m_permutations(list, n)

  # def repeated_permutations([], _), do: []
  # def repeated_permutations(list, 1), do: Enum.map(list, &[&1])
  # def repeated_permutations(list, n), do: m_repeated_permutations(list, n)
end