defmodule Dmp.Cursor do
@moduledoc """
A container for Elixir lists, that can be used to iterate forward
and backward, with a focused "current" item in the list, and "prev" and "next"
lists of items that come before and after the current item.
In Elm this has been described as a "Zipper".
"""
defstruct prev: [], current: nil, next: []
alias __MODULE__
@type t() :: %Cursor{
prev: list(),
current: term(),
next: list()
}
@typedoc """
A value used to set the Cursor position.
* `-1` means before the first item in the container.
* `0` means at the first item.
* `:last` means at the last item.
* `:tail` means after the last item.
"""
@type position_value() :: -1 | non_neg_integer() | :last | :tail
@type init_option() :: {:position, position_value()}
@typedoc """
Use `position: 0` for example, to set the initial
position of the Cursor to the first item.
"""
@type init_options() :: list(init_option())
@doc """
Create a Cursor containing no items.
## Examples
iex> Cursor.new()
%Cursor{current: nil, next: [], prev: []}
"""
@spec new() :: t()
def new(), do: %Cursor{prev: [], current: nil, next: []}
@doc """
Create a Cursor from a list of items.
* `opts` - The keyword `:position` can specify a position value,
such as `-1`, `0`, `1`, `:last`, or `tail` in order to set the
position of the created Cursor.
## Examples
iex> Cursor.from_list([1, 2, 3])
%Cursor{current: nil, next: [1, 2, 3], prev: []}
iex> Cursor.from_list([1, 2, 3]) |> Cursor.move_forward()
%Cursor{current: 1, next: [2, 3], prev: []}
iex> Cursor.from_list([1, 2, 3], position: 1)
%Cursor{current: 2, next: [3], prev: [1]}
"""
@spec from_list(list(), init_options()) :: t()
def from_list(items, opts \\ []) when is_list(items) do
%Cursor{prev: [], current: nil, next: items} |> with_init_options(opts)
end
@doc """
Create a Cursor from two lists.
* `opts` - the keyword `:position` can specify a position value,
such as `-1`, `0`, `1`, `:last`, or `tail` in order to set the
position of the created Cursor.
## Examples
iex> Cursor.from_split([1, 2, 3], [4, 5, 6])
%Cursor{current: 4, next: [5, 6], prev: [3, 2, 1]}
"""
@spec from_split(list(), list(), init_options()) :: t()
def from_split(prev, next, opts \\ [])
def from_split([], next, opts) when is_list(next) do
%Cursor{prev: [], current: nil, next: next} |> with_init_options(opts)
end
def from_split(prev, [], opts) when is_list(prev) do
%Cursor{prev: Enum.reverse(prev), current: nil, next: []} |> with_init_options(opts)
end
def from_split(prev, [h | t], opts) when is_list(prev) do
%Cursor{prev: Enum.reverse(prev), current: h, next: t} |> with_init_options(opts)
end
@spec with_init_options(t(), init_options()) :: t()
defp with_init_options(%Cursor{} = c, opts) do
case Keyword.get(opts, :position, nil) do
pos when is_integer(pos) -> move_to(c, pos)
_ -> c
end
end
@doc """
Extract the list from a Cursor.
## Examples
iex> Cursor.from_list([1, 2, 3]) |> Cursor.to_list()
[1, 2, 3]
"""
@spec to_list(t()) :: list()
def to_list(%Cursor{prev: prev, current: nil, next: next}) do
Enum.reverse(prev) ++ next
end
def to_list(%Cursor{prev: prev, current: current, next: next}) do
Enum.reverse(prev) ++ [current | next]
end
@doc """
Returns `true` if there are no items in the Cursor.
## Examples
iex> Cursor.from_list([]) |> Cursor.empty?()
true
iex> Cursor.from_list([1, 2, 3]) |> Cursor.empty?()
false
"""
@spec empty?(t()) :: boolean()
def empty?(%Cursor{prev: prev, current: nil, next: next}),
do: Enum.empty?(prev) && Enum.empty?(next)
def empty?(%Cursor{}), do: false
@doc """
Returns `true` if the Cursor is positioned before the first item.
## Examples
iex> Cursor.from_list([1, 2, 3]) |> Cursor.before_first?()
true
iex> Cursor.from_list([1, 2, 3]) |> Cursor.move_forward() |> Cursor.before_first?()
false
"""
@spec before_first?(t()) :: boolean()
def before_first?(%Cursor{prev: [], current: nil}), do: true
def before_first?(%Cursor{}), do: false
@doc """
Returns `false` if the Cursor is positioned at or before the first item.
## Examples
iex> Cursor.from_list([1, 2]) |> Cursor.has_previous?()
false
iex> Cursor.from_list([1, 2]) |> Cursor.move_forward() |> Cursor.has_previous?()
false
iex> Cursor.from_list([1, 2]) |> Cursor.move_forward(2) |> Cursor.has_previous?()
true
iex> Cursor.from_list([1, 2]) |> Cursor.move_forward(3) |> Cursor.has_previous?()
true
"""
@spec has_previous?(t()) :: boolean()
def has_previous?(%Cursor{prev: []}), do: false
def has_previous?(%Cursor{}), do: true
@doc """
Returns `true` if the Cursor is positioned after the last item.
## Examples
iex> Cursor.from_list([1, 2]) |> Cursor.after_last?()
false
iex> Cursor.from_list([1, 2]) |> Cursor.move_forward(2) |> Cursor.after_last?()
false
iex> Cursor.from_list([1, 2]) |> Cursor.move_forward(3) |> Cursor.after_last?()
true
"""
@spec after_last?(t()) :: boolean()
def after_last?(%Cursor{current: nil, next: []}), do: true
def after_last?(%Cursor{}), do: false
@doc """
Returns `false` if the Cursor is positioned at or after the last item.
## Examples
iex> Cursor.from_list([1, 2]) |> Cursor.has_next?()
true
iex> Cursor.from_list([1, 2]) |> Cursor.move_forward(2) |> Cursor.has_next?()
false
iex> Cursor.from_list([1, 2]) |> Cursor.move_forward(3) |> Cursor.has_next?()
false
"""
@spec has_next?(t()) :: boolean()
def has_next?(%Cursor{next: []}), do: false
def has_next?(%Cursor{}), do: true
@doc """
Returns the total number of items in the Cursor.
## Examples
iex> Cursor.from_list([1, 2, 3]) |> Cursor.count()
3
"""
@spec count(t()) :: non_neg_integer()
def count(%Cursor{prev: prev, current: nil, next: next}),
do: Enum.count(prev) + Enum.count(next)
def count(%Cursor{prev: prev, next: next}), do: Enum.count(prev) + Enum.count(next) + 1
@doc """
Returns the current position of the Cursor.
A return value of `-1` means the Cursor is positioned before the first item.
A return value of `Cursor.count(c)` means the Cursor is positioned after the last item.
## Examples
iex> Cursor.from_list([1, 2]) |> Cursor.position()
-1
iex> Cursor.from_list([1, 2]) |> Cursor.move_forward() |> Cursor.position()
0
iex> Cursor.from_list([1, 2]) |> Cursor.move_forward(2) |> Cursor.position()
1
iex> Cursor.from_list([1, 2]) |> Cursor.move_forward(3) |> Cursor.position()
2
iex> Cursor.from_list([1, 2]) |> Cursor.move_forward(4) |> Cursor.position()
2
"""
@spec position(t()) :: integer()
def position(%Cursor{prev: [], current: nil}), do: -1
def position(%Cursor{prev: prev}), do: Enum.count(prev)
@doc """
Changes the current position of the Cursor.
* `pos` - The desired position.
* `-1` means the Cursor is positioned before the first item.
* `0` means the Cursor is positioned at the first item (if it is not empty).
* `:last` means the Cursor is positioned at the last item (if it is not empty).
* `:tail` means the Cursor is positioned after the last item.
## Examples
iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.move_to(-1)
%Cursor{current: nil, next: [1, 2, 3, 4, 5], prev: []}
iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.move_to(0)
%Cursor{current: 1, next: [2, 3, 4, 5], prev: []}
iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.move_to(1)
%Cursor{current: 2, next: [3, 4, 5], prev: [1]}
iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.move_to(:last)
%Cursor{current: 5, next: [], prev: [4, 3, 2, 1]}
iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.move_to(:tail)
%Cursor{current: nil, next: [], prev: [5, 4, 3, 2, 1]}
"""
@spec move_to(t(), position_value()) :: t()
def move_to(%Cursor{prev: prev, current: current, next: next}, :tail) do
current_and_next =
if is_nil(current) do
prev
else
[current | next]
end
%Cursor{prev: Enum.reverse(current_and_next) ++ prev, current: nil, next: []}
end
def move_to(%Cursor{} = c, :last), do: c |> move_to(:tail) |> move_back(1)
def move_to(%Cursor{prev: prev, current: current, next: next}, -1) do
current_and_next =
if is_nil(current) do
next
else
[current | next]
end
%Cursor{prev: [], current: nil, next: Enum.reverse(prev) ++ current_and_next}
end
def move_to(%Cursor{} = c, pos) when is_integer(pos) and pos >= 0 do
c |> move_to(-1) |> move_forward(pos + 1)
end
@doc """
Resets the position of the Cursor to before the first item.
An alias for `Cursor.move_to(c, -1)`.
## Examples
iex> Cursor.new() |> Cursor.reset()
%Cursor{current: nil, next: [], prev: []}
iex> Cursor.from_list([1]) |> Cursor.move_forward() |> Cursor.reset()
%Cursor{current: nil, next: [1], prev: []}
iex> Cursor.from_list([1, 2]) |> Cursor.move_forward() |> Cursor.reset()
%Cursor{current: nil, next: [1, 2], prev: []}
iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.reset()
%Cursor{current: nil, next: [1, 2, 3, 4, 5], prev: []}
"""
def reset(%Cursor{} = c), do: move_to(c, -1)
@doc """
Moves the position of the Cursor to the first item.
An alias of `Cursor.move_to(c, 0)`.
## Examples
iex> Cursor.new() |> Cursor.move_first()
%Cursor{current: nil, next: [], prev: []}
iex> Cursor.from_list([1]) |> Cursor.move_first()
%Cursor{current: 1, next: [], prev: []}
iex> Cursor.from_list([1, 2]) |> Cursor.move_first()
%Cursor{current: 1, next: [2], prev: []}
iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.move_first()
%Cursor{current: 1, next: [2, 3, 4, 5], prev: []}
"""
@spec move_first(t()) :: t()
def move_first(%Cursor{} = c), do: move_to(c, 0)
@doc """
Moves the position of the Cursor forward a number of steps.
## Examples
iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.move_forward()
%Cursor{current: 4, next: [5], prev: [3, 2, 1]}
iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.move_forward(2)
%Cursor{current: 5, next: [], prev: [4, 3, 2, 1]}
iex> Cursor.from_list([1, 2, 3, 4, 5]) |> Cursor.move_forward(3)
%Cursor{current: 3, next: [4, 5], prev: [2, 1]}
iex> Cursor.from_list([1, 2, 3, 4, 5]) |> Cursor.move_forward(5)
%Cursor{current: 5, next: [], prev: [4, 3, 2, 1]}
iex> Cursor.from_list([1, 2, 3, 4, 5]) |> Cursor.move_forward(6)
%Cursor{current: nil, next: [], prev: [5, 4, 3, 2, 1]}
iex> %Cursor{current: {:delete, "abc"}, next: [{:equal, "xxx"}, {:insert, "def"}], prev: []} |> Cursor.move_forward(1)
%Cursor{current: {:equal, "xxx"}, next: [{:insert, "def"}], prev: [{:delete, "abc"}]}
iex> %Cursor{current: {:delete, "abc"}, next: [{:equal, "xxx"}, {:insert, "def"}], prev: []} |> Cursor.move_forward(2)
%Cursor{current: {:insert, "def"}, next: [], prev: [{:equal, "xxx"}, {:delete, "abc"}]}
iex> %Cursor{current: {:delete, "abc"}, next: [{:equal, "xxx"}, {:insert, "def"}], prev: []} |> Cursor.move_forward(3)
%Cursor{current: nil, next: [], prev: [{:insert, "def"}, {:equal, "xxx"}, {:delete, "abc"}]}
"""
@spec move_forward(t(), integer()) :: t()
def move_forward(cursor, count \\ 1)
def move_forward(%Cursor{} = c, count) when is_integer(count) and count <= 0, do: c
def move_forward(%Cursor{prev: prev, current: current, next: next}, count)
when is_integer(count) do
current_and_prev =
if is_nil(current) do
prev
else
[current | prev]
end
if count > Enum.count(next) do
%Cursor{prev: Enum.reverse(next) ++ current_and_prev, current: nil, next: []}
else
{moved, next_next} = Enum.split(next, count)
{next_cur, to_prev} =
if Enum.empty?(moved) do
{nil, []}
else
[next_cur | to_prev] = Enum.reverse(moved)
{next_cur, to_prev}
end
%Cursor{prev: to_prev ++ current_and_prev, current: next_cur, next: next_next}
end
end
@doc """
Moves the position of the Cursor forward through the "next" list
until the given item is found. Returns `nil` and if the item cannot be found.
## Examples
iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.find_forward(5)
%Cursor{current: 5, next: [], prev: [4, 3, 2, 1]}
iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.find_forward(1)
nil
iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.find_forward(3)
nil
"""
@spec find_forward(t(), term()) :: nil | t()
def find_forward(%Cursor{next: next} = c, item) do
case Enum.find_index(next, fn i -> i == item end) do
nil -> nil
i -> move_forward(c, i + 1)
end
end
@doc """
Moves the position of the Cursor back a number of steps.
## Examples
iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.move_back(1)
%Cursor{current: 2, next: [3, 4, 5], prev: [1]}
iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.move_back(2)
%Cursor{current: 1, next: [2, 3, 4, 5], prev: []}
iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.move_back(3)
%Cursor{current: nil, next: [1, 2, 3, 4, 5], prev: []}
"""
@spec move_back(t(), integer()) :: t()
def move_back(cursor, count \\ 1)
def move_back(%Cursor{} = c, count) when is_integer(count) and count <= 0, do: c
def move_back(%Cursor{prev: prev, current: current, next: next}, count)
when is_integer(count) do
current_and_next =
if is_nil(current) do
next
else
[current | next]
end
if count > Enum.count(prev) do
%Cursor{prev: [], current: nil, next: Enum.reverse(prev) ++ current_and_next}
else
{moved, next_prev} = Enum.split(prev, count)
{next_cur, to_next} =
if Enum.empty?(moved) do
{nil, []}
else
[next_cur | to_next] = Enum.reverse(moved)
{next_cur, to_next}
end
%Cursor{prev: next_prev, current: next_cur, next: to_next ++ current_and_next}
end
end
@doc """
Moves the position of the Cursor back through the "prev" list
until the given item is found. Returns `nil` and if the item cannot be found.
## Examples
iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.find_back(1)
%Cursor{current: 1, next: [2, 3, 4, 5], prev: []}
iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.find_back(5)
nil
iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.find_back(3)
nil
"""
@spec find_back(t(), term()) :: nil | t()
def find_back(%Cursor{prev: prev} = c, item) do
case Enum.find_index(prev, fn i -> i == item end) do
nil -> nil
i -> move_back(c, i + 1)
end
end
@doc """
Moves the position of the Cursor back through the "prev" list to the given item.
Raises if the item cannot be found.
"""
@spec find_back!(t(), term()) :: t()
def find_back!(%Cursor{} = c, item) do
case find_back(c, item) do
nil -> raise "item #{inspect(item)} not found in Cursor"
found -> found
end
end
@doc """
Return a 3-tuple of the previous, current, and next items relative to
the Cursor's current position.
Any elements of the tuple may be `nil`.
## Examples
iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.get()
{2, 3, 4}
iex> Cursor.from_list([]) |> Cursor.get()
{nil, nil, nil}
iex> Cursor.from_list([1, 2]) |> Cursor.get()
{nil, nil, 1}
iex> Cursor.from_list([1, 2]) |> Cursor.move_forward() |> Cursor.get()
{nil, 1, 2}
iex> Cursor.from_list([1, 2]) |> Cursor.move_forward(2) |> Cursor.get()
{1, 2, nil}
iex> Cursor.from_list([1, 2]) |> Cursor.move_forward(3) |> Cursor.get()
{2, nil, nil}
"""
@spec get(t()) :: {term(), term(), term()}
def get(%Cursor{prev: prev, current: current, next: next}) do
{List.first(prev), current, List.first(next)}
end
@doc """
Remove items at the Cursor's current position, leaving the previous items alone.
## Examples
iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.delete(1)
%Cursor{current: 4, next: [5], prev: [2, 1]}
"""
@spec delete(t(), integer()) :: t()
def delete(cursor, count \\ 1)
def delete(%Cursor{} = c, count) when is_integer(count) and count <= 0, do: c
def delete(%Cursor{current: current, next: next} = c, count) when is_integer(count) do
next_next =
if is_nil(current) do
Enum.drop(next, count)
else
Enum.drop(next, count - 1)
end
case next_next do
[] -> %Cursor{c | current: nil, next: []}
[next_cur | rest] -> %Cursor{c | current: next_cur, next: rest}
end
end
@doc """
Remove items before the Cursor's current position, leaving the current and next items alone.
## Examples
iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.delete_before(1)
%Cursor{current: 3, next: [4, 5], prev: [1]}
"""
@spec delete_before(t(), integer()) :: t()
def delete_before(cursor, count \\ 1)
def delete_before(%Cursor{} = c, count) when is_integer(count) and count <= 0, do: c
def delete_before(%Cursor{prev: prev} = c, count) when is_integer(count) do
%Cursor{c | prev: Enum.drop(prev, count)}
end
@doc """
Insert items at the Cursor's current position, leaving the previous items alone.
After the insertion, the Cursor points to the first inserted item.
## Examples
iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.insert([10, 11])
%Cursor{current: 10, next: [11, 3, 4, 5], prev: [2, 1]}
"""
@spec insert(t(), list()) :: t()
def insert(%Cursor{} = c, []), do: c
def insert(%Cursor{current: nil, next: next} = c, [next_cur | rest]) do
%Cursor{c | current: next_cur, next: rest ++ next}
end
def insert(%Cursor{current: current, next: next} = c, [next_cur | rest]) do
%Cursor{c | current: next_cur, next: rest ++ [current | next]}
end
@doc """
Insert items before the Cursor's current position, leaving the current and next items alone.
## Examples
iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.insert_before([10, 11])
%Cursor{current: 3, next: [4, 5], prev: [11, 10, 2, 1]}
"""
@spec insert_before(t(), list()) :: t()
def insert_before(%Cursor{} = c, []), do: c
def insert_before(%Cursor{prev: prev} = c, items) when is_list(items) do
%Cursor{c | prev: Enum.reverse(items) ++ prev}
end
@doc """
Insert items at the Cursor's head position, leaving the current position pointer alone.
## Examples
iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.insert_at_head([10, 11])
%Cursor{current: 3, next: [4, 5], prev: [2, 1, 11, 10]}
"""
@spec insert_at_head(t(), list()) :: t()
def insert_at_head(%Cursor{} = c, []), do: c
def insert_at_head(%Cursor{prev: prev} = c, items) when is_list(items) do
%Cursor{c | prev: prev ++ Enum.reverse(items)}
end
end