Current section
Files
Jump to
Current section
Files
lib/cll.ex
defmodule CLL do
@moduledoc """
This module can be used to represent a data structure with similar behavior
as circular Doubly-Linked-List.
"But wait, aren't all Lists in Erlang Linked Lists?" Well yes, but they
are immutable, which makes things like removing elements while iterating
through the list very slow. Also, getting consistent CLL-like behaviour
from normal Lists is not easy when dealing with problems such as polygon
math around the beginning and end of the list.
Internally, it uses a Zipper data structure
(https://en.wikipedia.org/wiki/Zipper_(data_structure))
to keep the items before and
after the current item in a way that optimizes for moving forward and
backward in the list. Because the next and previous item are always the
first items in the surrounding lists, those operations are substantially
faster than tracking a cursor in a standar List an fetching its neighbors.
A list can be created by passing a List to the `init/2` function along with
an boolean defining if the resulting Doubly-Linked-List is circular or not.
Once created, you can traverse through the list one or more steps at a time.
## Examples
iex> [1, 2, 3, 4, 5]
...> |> CLL.init()
...> |> CLL.value()
1
iex> [1, 2, 3, 4, 5]
...> |> CLL.init()
...> |> CLL.next()
...> |> CLL.value()
2
iex> [1, 2, 3, 4, 5]
...> |> CLL.init()
...> |> CLL.prev()
...> |> CLL.prev(3)
...> |> CLL.next(2)
...> |> CLL.value()
4
You can also modify the list by inserting, replacing, or removing the current
element. Finally, if desired, you can convert the CLL back into a List.
## Examples
iex> CLL.init([1, 2, 3, 4, 5])
...> |> CLL.next(2)
...> |> CLL.remove()
...> |> CLL.to_list()
[1, 2, 4, 5]
iex> CLL.init([1, 2, 3, 4, 5])
...> |> CLL.prev(2)
...> |> CLL.replace(:foo)
...> |> CLL.to_list()
[1, 2, 3, :foo, 5]
iex> CLL.init([1, 2, 3, 4, 5])
...> |> CLL.next(3)
...> |> CLL.insert(3.5)
...> |> CLL.insert(3.75)
...> |> CLL.to_list()
[1, 2, 3, 3.5, 3.75, 4, 5]
To help with use cases where iterating through the list once is useful, CLL
keeps track of the "start" of the list so that you can determine when a list
has been fully traversed. A list can also be reset to the initial start
position at any time.
## Examples
iex> CLL.init([1, 2, 3, 4, 5])
...> |> CLL.next(3)
...> |> CLL.prev(2)
...> |> CLL.next()
...> |> CLL.offset()
2
iex> CLL.init([1, 2, 3, 4, 5])
...> |> CLL.next(5)
...> |> CLL.done?()
true
iex> CLL.init([1, 2, 3, 4, 5])
...> |> CLL.next(4)
...> |> CLL.reset()
...> |> CLL.value()
1
"""
@type cll :: {list, list}
@type value :: any
@spec init(list) :: cll
def init(list) when is_list(list), do: {[], list}
@spec next(cll) :: cll
def next({[], []}), do: {[], []}
def next({visited, []}) do
[a | remain] = Enum.reverse(visited)
{[a], remain}
end
def next({visited, [a | remain]}), do: {[a] ++ visited, remain}
@spec next(cll, number) :: cll
def next(state, 0), do: state
def next(state, 1), do: next(state)
def next(state, offset) when offset < 0, do: prev(state, -offset)
def next(state, offset), do: next(next(state), offset - 1)
@spec prev(cll) :: cll
def prev({[], []}), do: {[], []}
def prev({[], remain}), do: {Enum.reverse(remain), []} |> prev()
def prev({[a | visited], remain}), do: {visited, [a] ++ remain}
@spec prev(cll, number) :: cll
def prev(state, 0), do: state
def prev(state, 1), do: prev(state)
def prev(state, offset) when offset < 0, do: next(state, -offset)
def prev(state, offset), do: prev(prev(state), offset - 1)
@spec value(cll) :: any
@spec value(cll, number) :: any
def value(state, offset \\ 0)
def value({[], []}, _), do: nil
def value({_, remain} = state, offset) when offset >= length(remain),
do: value(state, offset - len(state))
def value({visited, _} = state, offset) when offset < -length(visited),
do: value(state, offset + len(state))
def value({visited, _}, offset) when offset < 0, do: Enum.at(visited, -offset - 1)
def value({_, remain}, offset), do: Enum.at(remain, offset)
@spec len(cll) :: non_neg_integer
def len({visited, remain}), do: length(visited) + length(remain)
@spec empty?(cll) :: boolean
def empty?({[], []}), do: true
def empty?(_), do: false
@spec offset(cll) :: non_neg_integer
def offset({visited, _}), do: length(visited)
@spec done?(cll) :: boolean
def done?({_, []}), do: true
def done?(_), do: false
@spec reset(cll) :: cll
def reset({_, _} = state) do
state |> to_list() |> init()
end
@spec remove(cll) :: cll
def remove({[], []}), do: {[], []}
def remove({visited, []}), do: {Enum.drop(visited, -1), []}
def remove({visited, [_ | remain]}), do: {visited, remain}
@spec insert(cll, any) :: cll
def insert({visited, remain}, value), do: {[value | visited], remain}
@spec replace(cll, any) :: cll
def replace({[], []}, _), do: {[], []}
def replace({visited, []}, value), do: {Enum.drop(visited, -1) ++ [value], []}
def replace({visited, [_ | remain]}, value), do: {visited, [value | remain]}
@spec to_list(cll) :: list
def to_list({visited, remain}), do: visited |> Enum.reverse() |> Enum.concat(remain)
end