lib/enum.ex

defmodule Bio.Enum do
  @moduledoc """
  Implements a wrapper around the `Enum` module's public interface.

  The semantics of the `Enum` module don't always match up with what I would think
  is best for certain cases. The best example of this is the `slide/3` function.
  Because of the `Enum` implementation, there is no way to coerce the return
  value back into a struct. So for example, given a `Bio.Sequence.DnaStrand` it
  would return a list of graphemes. This is not what I want users to expect.

  That said, there are other functions that _do_ behave well. Or at the very
  least, their semantics seem meaningfully useful. So in order to preserve the
  maximum utility, I wrap the module.

  The expectation should be as follows:
  `Enum` functions will return bare data.
  `Bio.Enum` functions will return the closest thing to the struct as is
  reasonable.

  There are cases where it doesn't make much sense to return more than is
  required. For example, the `Bio.Enum.at/2` function will return a binary
  grapheme. I have a hard time imagining a case where the user would want a
  struct with a sequence of a single character instead of the character itself.

  Contrast that with the `Enum.at/2` function, which will return a raw char.
  """
  @type acc() :: any()
  @type default() :: any()
  @type element() :: any()
  @type index() :: integer()
  @type t() :: Enumerable.t()

  # def all?(enumerable), do: Enum.all?(enumerable)
  # def all?(enumerable, func), do: Enum.all?(enumerable, func)

  # @spec any?(t()) :: boolean()
  # def any?(enumerable), do: Enum.any?(enumerable)

  # TODO: add a check for `func` in `any?/2` is_binary to allow
  # `any?(seq, "char")` as a valid approach.
  # @spec any?(t(), (element() -> as_boolean(term()))) :: boolean()
  # def any?(enumerable, func), do: Enum.any?(enumerable, func)

  def at(enumerable, index) when is_integer(index) do
    Enum.at(enumerable, index)
    |> then(&[&1])
    |> List.to_string()
  end

  def at(enumerable, index, default) when is_integer(index) do
    Enum.at(enumerable, index, default)
    |> then(&[&1])
    |> List.to_string()
  end

  # def chunk_by(enumerable, func) do
  #   Enum.chunk_by(enumerable, func)
  #   |> Enum.map(&Enum.join/1)
  #   |> Enum.map(&apply(enumerable.__struct__, :new, [&1, [label: enumerable.label]]))
  # end

  def chunk_every(enumerable, count),
    do:
      Enum.chunk_every(enumerable, count)
      |> Enum.map(&Enum.join/1)
      |> Enum.map(&new(&1, enumerable))

  def chunk_every(enumerable, count, step),
    do:
      Enum.chunk_every(enumerable, count, step)
      |> Enum.map(&Enum.join/1)
      |> Enum.map(&new(&1, enumerable))

  def chunk_every(enumerable, count, step, options),
    do:
      Enum.chunk_every(enumerable, count, step, options)
      |> Enum.map(&Enum.join/1)
      |> Enum.map(&new(&1, enumerable))

  def chunk_while(enumerable, acc, chunk_fun, after_fun),
    do:
      Enum.chunk_while(enumerable, acc, chunk_fun, after_fun)
      |> Enum.map(&Enum.join/1)
      |> Enum.map(&new(&1, enumerable))

  # TODO: figure out the semantics for concatenation with non-sequence
  # enumerables
  # def concat(a), do: {a}
  # def concat(a, b), do: {a, b}

  # def count(enumerable), do: Enum.count(enumerable)
  # def count(enumerable, fun), do: Enum.count(enumerable, fun)

  # def count_until(a), do: {a}
  # def count_until(a, b), do: {a, b}

  # def dedup(), do: {}

  # def dedup_by(), do: {}

  # def drop(), do: {}
  #
  # def drop_every(), do: {}
  #
  # def drop_while(), do: {}
  #
  # def each(), do: {}
  #
  # def empty?(), do: {}
  #
  # def fetch!(), do: {}
  #
  # def fetch(), do: {}
  #
  # def filter(), do: {}
  #
  # def find(a), do: {a}
  # def find(a, b), do: {a, b}
  #
  # def find_index(), do: {}
  #
  # def find_value(a), do: {a}
  # def find_value(a, b), do: {a, b}
  #
  # def flat_map(), do: {}
  #
  # def flat_map_reduce(), do: {}
  #
  # def frequencies(), do: {}
  #
  # def frequencies_by(), do: {}
  #
  # def group_by(a), do: {a}
  # def group_by(a, b), do: {a, b}
  #
  # def intersperse(), do: {}
  #
  # def into(a), do: {a}
  # def into(a, b), do: {a, b}
  #
  # def join(a), do: {a}
  # def join(a, b), do: {a, b}

  def map(enumerable, func),
    do:
      Enum.map(enumerable, func)
      |> Enum.join()
      |> new(enumerable)

  # def map_every(), do: {}
  #
  # def map_intersperse(), do: {}
  #
  # def map_join(a), do: {a}
  # def map_join(a, b), do: {a, b}
  #
  # def map_reduce(), do: {}
  #
  # def max(), do: {}
  #
  # def max_by(a), do: {a}
  # def max_by(a, b), do: {a, b}
  #
  # def member?(), do: {}
  #
  # def min(), do: {}
  #
  # def min_by(a), do: {a}
  # def min_by(a, b), do: {a, b}
  #
  # def min_max(a), do: {a}
  # def min_max(a, b), do: {a, b}
  #
  # def min_max_by(a), do: {a}
  # def min_max_by(a, b), do: {a, b}
  #
  # def product(), do: {}
  #
  # def random(), do: {}
  #
  # def reduce(a), do: {a}
  # def reduce(a, b), do: {a, b}
  #
  # def reduce_while(), do: {}
  #
  # def reject(), do: {}

  def reverse(enumerable),
    do:
      Enum.reverse(enumerable)
      |> Enum.join()
      |> new(enumerable)

  # TODO: what type is tail?
  def reverse(enumerable, tail),
    do:
      Enum.reverse(enumerable, tail)
      |> Enum.join()
      |> new(enumerable)

  # def reverse_slice(), do: {}
  #
  # def scan(a), do: {a}
  # def scan(a, b), do: {a, b}
  #
  # def shuffle(), do: {}

  def slice(enumerable, index_range),
    do:
      Enum.slice(enumerable, index_range)
      |> List.to_string()
      |> new(enumerable)

  def slice(enumerable, start_index, amount),
    do:
      Enum.slice(enumerable, start_index, amount)
      |> List.to_string()
      |> new(enumerable)

  # def slide(), do: {}
  #
  # def sort(a), do: {a}
  # def sort(a, b), do: {a, b}
  #
  # def sort_by(a), do: {a}
  # def sort_by(a, b), do: {a, b}
  #
  # def split(), do: {}
  #
  # def split_while(), do: {}
  #
  # def split_with(), do: {}
  #
  # def sum(), do: {}
  #
  # def take(), do: {}
  #
  # def take_every(), do: {}
  #
  # def take_random(), do: {}
  #
  # def take_while(), do: {}
  #
  # def to_list(), do: {}
  #
  # def uniq(), do: {}
  #
  # def uniq_by(), do: {}
  #
  # def unzip(), do: {}

  # def zip(a), do: {a}
  # def zip(a, b), do: {a, b}
  #
  # def zip_reduce(a), do: {a}
  # def zip_reduce(a, b), do: {a, b}
  #
  # def zip_with(a), do: {a}
  # def zip_with(a, b), do: {a, b}
  defp new(seq, enumerable) do
    data =
      enumerable
      |> Map.from_struct()
      |> Map.drop([:sequence, :length])
      |> Map.to_list()

    apply(enumerable.__struct__, :new, [seq, data])
  end
end