lib/mptree_node.ex

defmodule MPTree.Node do
  @moduledoc """
  A Tree is made of many nodes. Here is one.

  A valid `MPTree` node is a map with a `:__mptree_node__` key holding a value of `t:meta/0`.
  Here is the absolute minimal node you can build:

  ```
  my_node = %{__mptree_node__: init()}
  ```

  Here's a node implemented as a struct:

  ```
  defmodule MyNode do
    defstruct [:name, __mptree_node__: init()]

    @type t :: %__MODULE__{
      name: String.t(),
      __mptree_node__: MPTree.Node.meta()
    }

    new(name), do: %__MODULE__{name: name}
  end

  iex> MyNode.new("hello")
  %MyNode{name: "hello", __mptree_node__: _}
  ```

  If you're a fan of the excellent little `TypedStruct` library,
  you can just use `MPTree.Node` as a plugin.
  The below module compiles to the exact same result as above,
  but is cleaner, more expressive,
  and hides more `MPTree` implementation details.
  ```
  defmodule MyNode do
    use TypedStruct

    typedstruct do
      plugin MPTree.Node
      field :name, String.t()
    end

    new(name), do: %__MODULE__{name: name}
  end

  iex> MyNode.new("hello")
  %MyNode{name: "hello", __mptree_node__: _}
  ```

  But it would be easier if you just used the
  """

  use TypedStruct.Plugin
  alias MPTree.Node
  alias MPTree.NodeMeta

  @typedoc """
  Instantiated by `MPTree.Node.init/0` and maintained by `MPTree`
  """
  @opaque meta :: NodeMeta.t()

  @type t :: %{required(:__mptree_node__) => meta(), optional(any()) => any()}

  #####################################
  # PLUGIN

  @impl TypedStruct.Plugin
  defmacro init(_) do
    quote do
      field :__mptree_node__, MPTree.NodeMeta.t(), default: MPTree.Node.init()
    end
  end

  #####################################
  # REDUCERS

  # Increase `lft` and/or `rgt` if they're greater than or equal to the `threshold`
  def __incr_lft_rgt__(
        %{__mptree_node__: %NodeMeta{lft: lft, rgt: rgt} = meta} = node,
        increment_by,
        threshold \\ 0
      ) do
    meta =
      cond do
        lft >= threshold -> %NodeMeta{meta | lft: lft + increment_by, rgt: rgt + increment_by}
        rgt >= threshold -> %NodeMeta{meta | rgt: rgt + increment_by}
        true -> nil
      end

    if meta, do: %{node | __mptree_node__: meta}, else: node
  end

  def __incr_id__(%{__mptree_node__: %NodeMeta{} = meta} = node, increment_by) do
    meta = Map.update!(meta, :id, &(&1 + increment_by))
    %{node | __mptree_node__: meta}
  end

  #####################################
  # CONVERTERS

  def __ancestor_and_descendent__?(
        %{__mptree_node__: %NodeMeta{} = ancestor},
        %{__mptree_node__: %NodeMeta{} = descendent}
      ) do
    ancestor.lft < descendent.lft && descendent.rgt < ancestor.rgt
  end

  def __siblings__?(
        %{__mptree_node__: %NodeMeta{} = brother},
        %{__mptree_node__: %NodeMeta{} = sister}
      ) do
    brother.rgt + 1 == sister.lft || sister.rgt + 1 == brother.lft
  end

  def __parent_and_last_child__?(
        %{__mptree_node__: %NodeMeta{} = parent},
        %{__mptree_node__: %NodeMeta{} = child}
      ) do
    child.rgt + 1 == parent.rgt
  end

  @doc false
  def __leaf__?(%{__mptree_node__: %NodeMeta{lft: lft, rgt: rgt}}) do
    lft + 1 == rgt
  end

  def __id__(%{__mptree_node__: %NodeMeta{id: val}}), do: val
  def __lft__(%{__mptree_node__: %NodeMeta{lft: val}}), do: val
  def __rgt__(%{__mptree_node__: %NodeMeta{rgt: val}}), do: val
  def __rgt_less_than__(%{__mptree_node__: %NodeMeta{rgt: rgt}}, threshold), do: rgt < threshold

  @spec __lft_rgt__(t()) :: {NodeMeta.lft(), NodeMeta.rgt()}
  def __lft_rgt__(%{__mptree_node__: %NodeMeta{lft: lft, rgt: rgt}}), do: {lft, rgt}

  @spec __count_self_and_descendents__(t()) :: pos_integer
  def __count_self_and_descendents__(node) do
    {lft, rgt} = Node.__lft_rgt__(node)
    div(1 + rgt - lft, 2)
  end

  @spec __count_descendents__(t()) :: non_neg_integer
  def __count_descendents__(node) do
    {lft, rgt} = Node.__lft_rgt__(node)
    div(rgt - lft - 1, 2)
  end

  #####################################
  # HELPERS

  @doc """
  When creating your nodes, and before inserting into a tree,
  ensure it has a `:__mptree_node__` key with the value from `init/0` assigned to it.
  """
  def init, do: NodeMeta.new()
end