lib/r_map/ruby.ex

defmodule RMap.Ruby do
  @moduledoc """
  Summarized all of Ruby's Hash functions.
  Functions corresponding to the following patterns are not implemented
   - When a function with the same name already exists in Elixir.
   - When a method name includes `!`.
   - <, <=, ==, >, >=, [], []=, default_*
  """
  @spec __using__(any) :: list
  defmacro __using__(_opts) do
    RUtils.define_all_functions!(__MODULE__)
  end

  import RMap.Support

  # https://ruby-doc.org/core-3.1.0/Hash.html
  # [:any?, :assoc, :clear, :compact, :compact!, :compare_by_identity, :compare_by_identity?, :deconstruct_keys, :delete, :delete_if, :dig, :each, :each_key, :each_pair, :each_value, :empty?, :eql?, :except, :fetch, :fetch_values, :filter, :filter!, :flatten, :has_key?, :has_value?, :hash, :include?, :initialize_copy, :inspect, :invert, :keep_if, :key, :key?, :keys, :length, :member?, :merge, :merge!, :rassoc, :rehash, :reject, :reject!, :replace, :select, :select!, :shift, :size, :slice, :store, :to_a, :to_h, :to_hash, :to_proc, :to_s, :transform_keys, :transform_keys!, :transform_values, :transform_values!, :update, :value?, :values, :values_at]
  # |> RUtils.required_functions([Map, REnum])
  # ✔ assoc
  # ✔ clear
  # × compare_by_identity
  # × compare_by_identity?
  # × deconstruct_keys
  # ✔ delete_if
  # ✔ dig
  # ✔ each_key
  # ✔ each_pair
  # ✔ each_value
  # ✔ eql?
  # ✔ except
  # ✔ fetch_values
  # ✔ flatten
  # ✔ has_value?
  # hash TODO: Low priority
  # × initialize_copy
  # ✔ inspect
  # ✔ invert
  # ✔ keep_if
  # ✔ key
  # ✔ key?
  # ✔ length
  # ✔ rassoc
  # × rehash
  # ✔ shift
  # ✔ store
  # ✔ to_hash
  # × to_proc
  # ✔ to_s
  # ✔ transform_keys
  # ✔ transform_values
  # ✔ value?
  # ✔ values_at

  @doc """
  Returns a list whose entries are those for which the function returns a truthy value.
  ## Examples
      iex> RMap.filter(%{a: 1, b: 2, c: 3}, fn {_, v} -> v > 1 end)
      %{b: 2, c: 3}
  """
  @spec filter(map(), function()) :: map()
  def filter(map, func) do
    Enum.filter(map, func)
    |> Map.new()
  end

  @doc """
  Returns a list whose entries are all those from self for which the function returns false or nil.
  ## Examples
      iex> RMap.reject(%{a: 1, b: 2, c: 3}, fn {_, v} -> v > 1 end)
      %{a: 1}
  """
  @spec reject(map(), function()) :: map()
  def reject(map, func) do
    Enum.reject(map, func)
    |> Map.new()
  end

  @doc """
  Returns %{}.
  ## Examples
      iex> RMap.clear(%{a: 1, b: 2, c: 3})
      %{}
  """
  @spec clear(map()) :: %{}
  def clear(_) do
    %{}
  end

  @doc """
  Calls the function with each value; returns :ok.
  ## Examples
      iex> RMap.each_value(%{a: 1, b: 2, c: 3}, &IO.inspect(&1))
      # 1
      # 2
      # 3
      :ok
  """
  @spec each_value(map(), function()) :: :ok
  def each_value(map, func) do
    Enum.each(map, fn {_, value} ->
      func.(value)
    end)
  end

  @doc """
  Calls the function with each key; returns :ok.
  ## Examples
      iex> RMap.each_key(%{a: 1, b: 2, c: 3}, &IO.inspect(&1))
      # :a
      # :b
      # :c
      :ok
  """
  @spec each_key(map(), function()) :: :ok
  def each_key(map, func) do
    Enum.each(map, fn {key, _} ->
      func.(key)
    end)
  end

  @doc """
  Returns true if value is a value in list, otherwise false.
  ## Examples
      iex> RMap.value?(%{a: 1, b: 2, c: 3}, 3)
      true

      iex> RMap.value?(%{a: 1, b: 2, c: 3}, 4)
      false
  """
  @spec value?(map(), any) :: boolean()
  def value?(map, value) do
    Enum.any?(map, fn {_, v} ->
      v == value
    end)
  end

  @doc """
  Returns a list containing values for the given keys.
  ## Examples
      iex> RMap.values_at(%{a: 1, b: 2, c: 3}, [:a, :b, :d])
      [1, 2, nil]
  """
  @spec values_at(map(), list()) :: list()
  def values_at(map, keys) do
    Enum.map(keys, &Map.get(map, &1))
  end

  @doc """
  Returns given map.
  ## Examples
      iex> RMap.to_hash(%{a: 1, b: 2, c: 3})
      %{a: 1, b: 2, c: 3}
  """
  @spec to_hash(map()) :: map()
  def to_hash(map) do
    map
  end

  @doc """
  Returns the object in nested map that is specified by a given key and additional arguments.
  ## Examples
      iex> RMap.dig(%{a: %{b: %{c: 1}}}, [:a, :b, :c])
      1

      iex> RMap.dig(%{a: %{b: %{c: 1}}}, [:a, :c, :b])
      nil
  """
  def dig(nil, _), do: nil
  def dig(result, []), do: result
  @spec dig(map(), list()) :: any()
  def dig(map, keys) do
    [key | tail_keys] = keys
    result = Map.get(map, key)
    dig(result, tail_keys)
  end

  @doc """
  Returns a 2-element tuple containing a given key and its value.
  ## Examples
      iex> RMap.assoc(%{a: 1, b: 2, c: 3}, :a)
      {:a, 1}

      iex> RMap.assoc(%{a: 1, b: 2, c: 3}, :d)
      nil

      iex> RMap.assoc(%{a: %{b: %{c: 1}}}, :a)
      {:a, %{b: %{c: 1}}}
  """
  @spec assoc(map(), any()) :: any()
  def assoc(map, key) do
    if(value = Map.get(map, key)) do
      {key, value}
    else
      nil
    end
  end

  @doc """
  Returns a 2-element tuple consisting of the key and value of the first-found entry having a given value.
  ## Examples
      iex> RMap.rassoc(%{a: 1, b: 2, c: 3}, 1)
      {:a, 1}

      iex> RMap.rassoc(%{a: 1, b: 2, c: 3}, 4)
      nil

      iex> RMap.rassoc(%{a: %{b: %{c: 1}}}, %{b: %{c: 1}})
      {:a, %{b: %{c: 1}}}
  """
  @spec rassoc(map(), any()) :: any()
  def rassoc(map, value) do
    Enum.find_value(map, fn {k, v} ->
      if v == value, do: {k, v}
    end)
  end

  @doc """
  Returns a map with modified keys.
  ## Examples
      iex> RMap.transform_keys(%{a: 1, b: 2, c: 3}, &to_string(&1))
      %{"a" => 1, "b" => 2, "c" => 3}

      iex> RMap.transform_keys(%{a: %{b: %{c: 1}}}, &to_string(&1))
      %{"a" => %{b: %{c: 1}}}
  """
  @spec transform_keys(map(), function()) :: map()
  def transform_keys(map, func) do
    Enum.map(map, fn {key, value} ->
      {func.(key), value}
    end)
    |> Map.new()
  end

  @doc """
  Returns a map with modified values.
  ## Examples
      iex> RMap.transform_values(%{a: 1, b: 2, c: 3}, &inspect(&1))
      %{a: "1", b: "2", c: "3"}

      iex> RMap.transform_values(%{a: %{b: %{c: 1}}}, &inspect(&1))
      %{a: "%{b: %{c: 1}}"}
  """
  @spec transform_values(map(), function()) :: map()
  def transform_values(map, func) do
    Enum.map(map, fn {key, value} ->
      {key, func.(value)}
    end)
    |> Map.new()
  end

  @doc """
  Returns a map excluding entries for the given keys.
  ## Examples
      iex> RMap.except(%{a: 1, b: 2, c: 3}, [:a, :b])
      %{c: 3}
  """
  @spec except(map(), list()) :: map()
  def except(map, keys) do
    delete_if(map, fn {key, _} ->
      key in keys
    end)
  end

  @doc """
  Returns a list containing the values associated with the given keys.
  ## Examples
      iex> RMap.fetch_values(%{ "cat" => "feline", "dog" => "canine", "cow" => "bovine" }, ["cow", "cat"])
      ["bovine", "feline"]

      iex> RMap.fetch_values(%{ "cat" => "feline", "dog" => "canine", "cow" => "bovine" }, ["cow", "bird"])
      ** (MapKeyError) key not found: bird
  """
  @spec fetch_values(map(), list()) :: list()
  def fetch_values(map, keys) do
    Enum.map(keys, fn key ->
      if(value = map |> Map.get(key)) do
        value
      else
        raise MapKeyError, "key not found: #{key}"
      end
    end)
  end

  @doc """
  When a function is given, calls the function with each missing key, treating the block's return value as the value for that key.
  ## Examples
      iex> RMap.fetch_values(%{ "cat" => "feline", "dog" => "canine", "cow" => "bovine" }, ["cow", "bird"], &(String.upcase(&1)))
      ["bovine", "BIRD"]
  """
  @spec fetch_values(map(), list(), function()) :: list()
  def fetch_values(map, keys, func) do
    Enum.map(keys, fn key ->
      if(value = map |> Map.get(key)) do
        value
      else
        func.(key)
      end
    end)
  end

  @doc """
  Returns a flatten list.
  ## Examples
      iex> RMap.flatten(%{1=> "one", 2 => [2,"two"], 3 => "three"})
      [1, "one", 2, 2, "two", 3, "three"]

      iex> RMap.flatten(%{1 => "one", 2 => %{a: 1, b: %{c: 3}}})
      [1, "one", 2, :a, 1, :b, :c, 3]
  """
  @spec flatten(map()) :: list()
  def flatten(map) do
    deep_to_list(map) |> List.flatten()
  end

  @doc """
  Returns a map object with the each key-value pair inverted.
  ## Examples
      iex> RMap.invert(%{"a" => 0, "b" => 100, "c" => 200, "d" => 300, "e" => 300})
      %{0 => "a", 100 => "b", 200 => "c", 300 => "e"}

      iex> RMap.invert(%{a: 1, b: 1, c: %{d: 2}})
      %{1 => :b, %{d: 2} => :c}
  """
  @spec invert(map()) :: map()
  def invert(map) do
    map
    |> Enum.map(fn {k, v} ->
      {v, k}
    end)
    |> Map.new()
  end

  @doc """
  Removes the first map entry; returns a 2-element tuple.
  First element is {key, value}.
  Second element is a map without first pair.
  ## Examples
      iex> RMap.shift(%{a: 1, b: 2, c: 3})
      {{:a, 1}, %{b: 2, c: 3}}

      iex> RMap.shift(%{})
      {nil, %{}}
  """
  @spec shift(map()) :: {tuple() | nil, map()}
  def shift(map) do
    {result, list} = map |> Enum.split(1)
    {List.last(result), Map.new(list)}
  end

  defdelegate delete_if(map, func), to: __MODULE__, as: :reject
  defdelegate keep_if(map, func), to: __MODULE__, as: :filter
  defdelegate select(map, func), to: __MODULE__, as: :filter
  defdelegate length(map), to: Enum, as: :count
  defdelegate size(map), to: Enum, as: :count
  defdelegate to_s(map), to: Kernel, as: :inspect
  defdelegate inspect(map), to: Kernel, as: :inspect
  defdelegate each_pair(map, func), to: Enum, as: :each
  defdelegate key(map, key, default \\ nil), to: Map, as: :get
  defdelegate key?(map, key), to: Map, as: :has_key?
  defdelegate has_value?(map, value), to: __MODULE__, as: :value?
  defdelegate store(map, key, value), to: Map, as: :put
  defdelegate eql?(map1, map2), to: Map, as: :equal?
end

defmodule MapKeyError do
  defexception [:message]
end