Skip to main content

lib/algo.ex

defmodule Algo do
  @moduledoc """
  A collection of utility functions for working with lists, maps, keyword lists, and strings.
  This module is a convenience wrapper around the various `Algo` submodules, providing a single
  namespace for importing functions.
  """

  @doc group: "Map and keyword list functions"
  defdelegate evolve(map, evolution_map), to: Algo.Assoc
  @doc group: "Map and keyword list functions"
  defdelegate invert(map), to: Algo.Assoc
  @doc group: "Map and keyword list functions"
  defdelegate get_paths(map), to: Algo.Assoc
  @doc group: "Map and keyword list functions"
  defdelegate has_path?(map, path), to: Algo.Assoc
  @doc group: "Map and keyword list functions"
  defdelegate get_path(map, path, default \\ nil), to: Algo.Assoc
  @doc group: "Map and keyword list functions"
  defdelegate fetch_path(map, path), to: Algo.Assoc
  @doc group: "Map and keyword list functions"
  defdelegate fetch_path!(map, path), to: Algo.Assoc
  @doc group: "Map and keyword list functions"
  defdelegate put_path(map, path, value), to: Algo.Assoc
  @doc group: "Map and keyword list functions"
  defdelegate put_new_path(map, path, value), to: Algo.Assoc
  @doc group: "Map and keyword list functions"
  defdelegate replace_path(map, path, value), to: Algo.Assoc
  @doc group: "Map and keyword list functions"
  defdelegate replace_path!(map, path, value), to: Algo.Assoc
  @doc group: "Map and keyword list functions"
  defdelegate update_path(map, path, initial, fun), to: Algo.Assoc
  @doc group: "Map and keyword list functions"
  defdelegate update_path!(map, path, fun), to: Algo.Assoc
  @doc group: "Map and keyword list functions"
  defdelegate delete_path(map, path), to: Algo.Assoc
  @doc group: "Map and keyword list functions"
  defdelegate get_values_at_paths(map, paths), to: Algo.Assoc
  @doc group: "Map and keyword list functions"
  defdelegate path_satisfies?(map, path, fun), to: Algo.Assoc
  @doc group: "Map and keyword list functions"
  defdelegate merge_right(left_map, right_map), to: Algo.Assoc
  @doc group: "Map and keyword list functions"
  defdelegate merge_left(left_map, right_map), to: Algo.Assoc
  @doc group: "Map and keyword list functions"
  defdelegate deep_merge_left(left_map, right_map), to: Algo.Assoc
  @doc group: "Map and keyword list functions"
  defdelegate deep_merge_right(left_map, right_map), to: Algo.Assoc
  @doc group: "Map and keyword list functions"
  defdelegate deep_merge_with(left_map, right_map, fun), to: Algo.Assoc
  @doc group: "Map and keyword list functions"
  defdelegate get_depth(map), to: Algo.Assoc
  @doc group: "Map and keyword list functions"
  defdelegate each_depth_first(map, fun), to: Algo.Assoc
  @doc group: "Map and keyword list functions"
  defdelegate each_breadth_first(map, fun), to: Algo.Assoc
  @doc group: "Map and keyword list functions"
  defdelegate project(list_of_maps, keys), to: Algo.Assoc
  @doc group: "Map and keyword list functions"
  defdelegate where_all?(map, condition_map), to: Algo.Assoc
  @doc group: "Map and keyword list functions"
  defdelegate where_any?(map, condition_map), to: Algo.Assoc
  @doc group: "Map and keyword list functions"
  defdelegate where_eq?(map, condition_map), to: Algo.Assoc
  @doc group: "Map and keyword list functions"
  defdelegate prop(name), to: Algo.Assoc
  @doc group: "Map and keyword list functions"
  defdelegate props(names), to: Algo.Assoc
  @doc group: "Map and keyword list functions"
  defdelegate rename_keys(map, renames), to: Algo.Assoc
  @doc group: "Map and keyword list functions"
  defdelegate unnest_keys(map), to: Algo.Assoc
  @doc group: "Map and keyword list functions"
  defdelegate stringify_keys(map), to: Algo.Assoc
  @doc group: "Map and keyword list functions"
  defdelegate atomise_keys_with(map, key_fun), to: Algo.Assoc
  @doc group: "Map and keyword list functions"
  defdelegate atomize_keys_with(map, key_fun), to: Algo.Assoc
  @doc group: "Map and keyword list functions"
  defdelegate equivalent?(map, kw_list), to: Algo.Assoc
  @doc group: "Map and keyword list functions"
  defdelegate as_map(kw_list), to: Algo.Assoc
  @doc group: "Map and keyword list functions"
  defdelegate as_keyword_list(map), to: Algo.Assoc
  @doc group: "List functions"
  defdelegate interweave(list1, list2), to: Algo.List
  @doc group: "List functions"
  defdelegate get_cartesian_product(list1, list2, output_as), to: Algo.List
  @doc group: "List functions"
  defdelegate get_permutations(list), to: Algo.List
  @doc group: "List functions"
  defdelegate get_subsequences(list), to: Algo.List
  @doc group: "List functions"
  defdelegate get_unique_pairs(list, output_as), to: Algo.List
  @doc group: "List functions"
  defdelegate map_accum(list, acc, fun), to: Algo.List
  @doc group: "List functions"
  defdelegate map_accum_right(list, acc, fun), to: Algo.List
  @doc group: "List functions"
  defdelegate chunk_while_adjacent(list, fun), to: Algo.List
  @doc group: "List functions"
  defdelegate split_whenever(list, fun), to: Algo.List
  @doc group: "List functions"
  defdelegate transpose(rows), to: Algo.List
  @doc group: "List functions"
  defdelegate move_at(list, from_index, to_index), to: Algo.List
  @doc group: "List functions"
  defdelegate unique_with(enumerable, fun), to: Algo.List
  @doc group: "List functions"
  defdelegate index_by(enumerable, fun), to: Algo.List
  @doc group: "List functions"
  defdelegate reduce_by(enumerable, key_fun, initial_acc, reduce_fun), to: Algo.List
  @doc group: "List functions"
  defdelegate unwind(enumerable, field), to: Algo.List
  @doc group: "List functions"
  defdelegate partition_map(enumerable, fun), to: Algo.List
  @doc group: "List functions"
  defdelegate difference_by(left, right, key_fun), to: Algo.List
  @doc group: "List functions"
  defdelegate difference_with(left, right, fun), to: Algo.List
  @doc group: "List functions"
  defdelegate intersect_by(left, right, key_fun), to: Algo.List
  @doc group: "List functions"
  defdelegate intersect_with(left, right, fun), to: Algo.List
  @doc group: "List functions"
  defdelegate get_symmetric_difference_by(left, right, key_fun), to: Algo.List
  @doc group: "List functions"
  defdelegate get_symmetric_difference_with(left, right, fun), to: Algo.List
  @doc group: "List functions"
  defdelegate inner_join(left, right, fun), to: Algo.List

  @doc group: "String functions"
  defdelegate humanise(str), to: Algo.String

  @doc group: "String functions"
  defdelegate humanize(str), to: Algo.String, as: :humanise

  @doc """
  Returns a comparator that checks equality after applying a function.

  ## Examples

      iex> same_length? = Algo.eq_by?(&String.length/1)
      iex> same_length?.("a", "b")
      true

      iex> same_length? = Algo.eq_by?(&String.length/1)
      iex> same_length?.("a", "bb")
      false

      iex> Algo.unique_with(["a", "bb", "c"], Algo.eq_by?(&String.length/1))
      ["a", "bb"]
  """
  @doc group: "Higher-order functions"
  @spec eq_by?((any -> any)) :: (any, any -> boolean)
  def eq_by?(fun) when is_function(fun, 1) do
    fn left, right -> fun.(left) == fun.(right) end
  end

  @doc """
  Returns whether all elements are unique after applying a function.

  ## Examples

      iex> Algo.all_different_by?(["a", "bb", "ccc"], &String.length/1)
      true

      iex> Algo.all_different_by?(["a", "b"], &String.length/1)
      false
  """
  @doc group: "Enumerable functions"
  @spec all_different_by?(Enumerable.t(), (any -> any)) :: boolean
  def all_different_by?(enumerable, fun) do
    Enum.count(Enum.uniq_by(enumerable, fun)) == Enum.count(enumerable)
  end

  @doc """
  Returns whether every element in an enumerable is unique.

  ## Examples

      iex> Algo.all_different?([1, 2, 3])
      true

      iex> Algo.all_different?([1, 2, 1])
      false
  """
  @doc group: "Enumerable functions"
  @spec all_different?(Enumerable.t()) :: boolean
  def all_different?(enumerable) do
    all_different_by?(enumerable, & &1)
  end

  defmodule Alt do
    @moduledoc """
    Provides alternative non-verb names for functions in Algo.
    These better reflect the purity of functions and immutability of data structures.
    """

    @doc """
    Alias for `Algo.evolve/2`.
    """
    defdelegate evolved(map, evolution_map), to: Algo, as: :evolve

    @doc """
    Alias for `Algo.invert/1`.
    """
    defdelegate inverted(map), to: Algo, as: :invert

    @doc """
    Alias for `Algo.get_paths/1`.
    """
    defdelegate paths(map), to: Algo, as: :get_paths

    @doc """
    Alias for `Algo.has_path?/2`.
    """
    defdelegate has_path?(map, path), to: Algo, as: :has_path?

    @doc """
    Alias for `Algo.get_path/3`.
    """
    defdelegate maybe_value_at_path(map, path, default \\ nil), to: Algo, as: :get_path

    @doc """
    Alias for `Algo.fetch_path/2`.
    """
    defdelegate value_at_path(map, path), to: Algo, as: :fetch_path

    @doc """
    Alias for `Algo.fetch_path!/2`.
    """
    defdelegate value_at_path!(map, path), to: Algo, as: :fetch_path!

    @doc """
    Alias for `Algo.put_path/3`.
    """
    defdelegate with_path(map, path, value), to: Algo, as: :put_path

    @doc """
    Alias for `Algo.put_new_path/3`.
    """
    defdelegate with_new_path(map, path, value), to: Algo, as: :put_new_path

    @doc """
    Alias for `Algo.replace_path/3`.
    """
    defdelegate with_replaced_path(map, path, value), to: Algo, as: :replace_path

    @doc """
    Alias for `Algo.replace_path!/3`.
    """
    defdelegate with_replaced_path!(map, path, value), to: Algo, as: :replace_path!

    @doc """
    Alias for `Algo.update_path/4`.
    """
    defdelegate with_updated_path(map, path, initial, fun), to: Algo, as: :update_path

    @doc """
    Alias for `Algo.update_path!/3`.
    """
    defdelegate with_updated_path!(map, path, fun), to: Algo, as: :update_path!

    @doc """
    Alias for `Algo.delete_path/2`.
    """
    defdelegate without_path(map, path), to: Algo, as: :delete_path

    @doc """
    Alias for `Algo.get_values_at_paths/2`.
    """
    defdelegate values_at_paths(map, paths), to: Algo, as: :get_values_at_paths

    @doc """
    Alias for `Algo.path_satisfies?/3`.
    """
    defdelegate path_satisfies?(map, path, fun), to: Algo, as: :path_satisfies?

    @doc """
    Alias for `Algo.merge_right/2`.
    """
    defdelegate right_merged(left_map, right_map), to: Algo, as: :merge_right

    @doc """
    Alias for `Algo.merge_left/2`.
    """
    defdelegate left_merged(left_map, right_map), to: Algo, as: :merge_left

    @doc """
    Alias for `Algo.deep_merge_left/2`.
    """
    defdelegate deep_left_merged(left_map, right_map), to: Algo, as: :deep_merge_left

    @doc """
    Alias for `Algo.deep_merge_right/2`.
    """
    defdelegate deep_right_merged(left_map, right_map), to: Algo, as: :deep_merge_right

    @doc """
    Alias for `Algo.deep_merge_with/3`.
    """
    defdelegate deep_merged_with(left_map, right_map, fun), to: Algo, as: :deep_merge_with

    @doc """
    Alias for `Algo.get_depth/1`.
    """
    defdelegate depth(map), to: Algo, as: :get_depth

    @doc """
    Alias for `Algo.each_depth_first/2`.
    """
    defdelegate each_depth_first(map, fun), to: Algo, as: :each_depth_first

    @doc """
    Alias for `Algo.each_breadth_first/2`.
    """
    defdelegate each_breadth_first(map, fun), to: Algo, as: :each_breadth_first

    @doc """
    Alias for `Algo.project/2`.
    """
    defdelegate projection(list_of_maps, keys), to: Algo, as: :project

    @doc """
    Alias for `Algo.where_all?/2`.
    """
    defdelegate where_all?(map, condition_map), to: Algo, as: :where_all?

    @doc """
    Alias for `Algo.where_any?/2`.
    """
    defdelegate where_any?(map, condition_map), to: Algo, as: :where_any?

    @doc """
    Alias for `Algo.where_eq?/2`.
    """
    defdelegate where_eq?(map, condition_map), to: Algo, as: :where_eq?

    @doc """
    Alias for `Algo.interweave/2`.
    """
    defdelegate interwoven(list1, list2), to: Algo, as: :interweave

    @doc """
    Alias for `Algo.get_cartesian_product/3`.
    """
    defdelegate cartesian_product(list1, list2, output_as), to: Algo, as: :get_cartesian_product

    @doc """
    Alias for `Algo.get_permutations/1`.
    """
    defdelegate permutations(list), to: Algo, as: :get_permutations

    @doc """
    Alias for `Algo.get_subsequences/1`.
    """
    defdelegate subsequences(list), to: Algo, as: :get_subsequences

    @doc """
    Alias for `Algo.get_unique_pairs/2`.
    """
    defdelegate unique_pairs(list, output_as), to: Algo, as: :get_unique_pairs

    @doc """
    Alias for `Algo.map_accum/3`.
    """
    defdelegate accumulated_map(list, acc, fun), to: Algo, as: :map_accum

    @doc """
    Alias for `Algo.map_accum_right/3`.
    """
    defdelegate right_accumulated_map(list, acc, fun), to: Algo, as: :map_accum_right

    @doc """
    Alias for `Algo.chunk_while_adjacent/2`.
    """
    defdelegate adjacent_chunks(list, fun), to: Algo, as: :chunk_while_adjacent

    @doc """
    Alias for `Algo.split_whenever/2`.
    """
    defdelegate split_whenever(list, fun), to: Algo, as: :split_whenever

    @doc """
    Alias for `Algo.transpose/1`.
    """
    defdelegate transposed(rows), to: Algo, as: :transpose

    @doc """
    Alias for `Algo.move_at/3`.
    """
    defdelegate with_moved_elem(list, from_index, to_index), to: Algo, as: :move_at

    @doc """
    Alias for `Algo.unique_with/2`.
    """
    defdelegate unique_with(enumerable, fun), to: Algo, as: :unique_with

    @doc """
    Alias for `Algo.index_by/2`.
    """
    defdelegate indexed_by(enumerable, fun), to: Algo, as: :index_by

    @doc """
    Alias for `Algo.reduce_by/4`.
    """
    defdelegate reduced_by(enumerable, key_fun, initial_acc, reduce_fun), to: Algo, as: :reduce_by

    @doc """
    Alias for `Algo.unwind/2`.
    """
    defdelegate unwound(enumerable, field), to: Algo, as: :unwind

    @doc """
    Alias for `Algo.partition_map/2`.
    """
    defdelegate partitioned_map(enumerable, fun), to: Algo, as: :partition_map

    @doc """
    Alias for `Algo.difference_by/3`.
    """
    defdelegate difference_by(left, right, key_fun), to: Algo, as: :difference_by

    @doc """
    Alias for `Algo.difference_with/3`.
    """
    defdelegate difference_with(left, right, fun), to: Algo, as: :difference_with

    @doc """
    Alias for `Algo.intersect_by/3`.
    """
    defdelegate intersection_by(left, right, key_fun), to: Algo, as: :intersect_by

    @doc """
    Alias for `Algo.intersect_with/3`.
    """
    defdelegate intersection_with(left, right, fun), to: Algo, as: :intersect_with

    @doc """
    Alias for `Algo.get_symmetric_difference_by/3`.
    """
    defdelegate symmetric_difference_by(left, right, key_fun), to: Algo, as: :get_symmetric_difference_by

    @doc """
    Alias for `Algo.get_symmetric_difference_with/3`.
    """
    defdelegate symmetric_difference_with(left, right, fun), to: Algo, as: :get_symmetric_difference_with

    @doc """
    Alias for `Algo.inner_join/3`.
    """
    defdelegate inner_join(left, right, fun), to: Algo, as: :inner_join

    @doc """
    Alias for `Algo.all_different_by?/2`.
    """
    defdelegate all_different_by?(enumerable, fun), to: Algo, as: :all_different_by?

    @doc """
    Alias for `Algo.eq_by?/1`.
    """
    defdelegate eq_by?(fun), to: Algo, as: :eq_by?

    @doc """
    Alias for `Algo.all_different?/1`.
    """
    defdelegate all_different?(enumerable), to: Algo, as: :all_different?

    @doc """
    Alias for `Algo.prop/1`.
    """
    defdelegate prop(name), to: Algo, as: :prop

    @doc """
    Alias for `Algo.props/1`.
    """
    defdelegate props(names), to: Algo, as: :props

    @doc """
    Alias for `Algo.rename_keys/2`.
    """
    defdelegate with_renamed_keys(map, renames), to: Algo, as: :rename_keys

    @doc """
    Alias for `Algo.unnest_keys/1`.
    """
    defdelegate with_unnested_keys(map), to: Algo, as: :unnest_keys

    @doc """
    Alias for `Algo.stringify_keys/1`.
    """
    defdelegate with_stringified_keys(map), to: Algo, as: :stringify_keys

    @doc """
    Alias for `Algo.atomise_keys_with/2`.
    """
    defdelegate with_atomised_keys(map, key_fun), to: Algo, as: :atomise_keys_with

    @doc """
    Alias for `Algo.atomize_keys_with/2`.
    """
    defdelegate with_atomized_keys(map, key_fun), to: Algo, as: :atomize_keys_with

    @doc """
    Alias for `Algo.equivalent?/2`.
    """
    defdelegate equivalent?(map, kw_list), to: Algo, as: :equivalent?

    @doc """
    Alias for `Algo.as_map/1`.
    """
    defdelegate as_map(kw_list), to: Algo, as: :as_map

    @doc """
    Alias for `Algo.as_keyword_list/1`.
    """
    defdelegate as_keyword_list(map), to: Algo, as: :as_keyword_list

    @doc """
    Alias for `Algo.humanise/1`.
    """
    defdelegate humanised(str), to: Algo, as: :humanise

    @doc """
    Alias for `Algo.humanize/1`.
    """
    defdelegate humanized(str), to: Algo, as: :humanize
  end
end