defmodule ZipZip do @moduledoc File.read!("README.md") @initial_position -1 @enforce_keys [:l, :r, :current, :size] # credo:disable-for-next-line defstruct @enforce_keys ++ [position: @initial_position] @opaque t :: %__MODULE__{ l: [term] | [], current: term, r: [term] | [], size: non_neg_integer, position: integer } @opaque t(val) :: %__MODULE__{ r: [val] | [], l: [val] | [], current: val | nil, size: non_neg_integer, position: integer } defmodule TraversalError do defexception [:message] @impl Exception def exception(value) do msg = "cannot traverse the zipper in this direction. #{inspect(value)}" %TraversalError{message: msg} end end defguard is_zipper(term) when is_struct(term, __MODULE__) defguard is_at_start(zipper) when zipper.l == [] and is_nil(zipper.current) and zipper.position == @initial_position defguard is_at_end(zipper) when zipper.position == zipper.size defguard is_out_of_bounds(zipper) when is_zipper(zipper) and (zipper.position >= zipper.size or zipper.position < @initial_position) @spec new([val]) :: t(val) when val: term def new(list) when is_list(list) do struct!(__MODULE__, l: [], current: nil, r: list, size: length(list), position: @initial_position ) end defdelegate zip(list), to: __MODULE__, as: :new @spec node(t) :: term def node(%__MODULE__{} = zipper), do: zipper.current @spec right(t) :: t | no_return def right(zipper) when is_out_of_bounds(zipper), do: raise(TraversalError, zipper) def right(zipper) when is_at_start(zipper) do zipper |> Map.put(:current, hd(zipper.r)) |> Map.put(:r, tl(zipper.r)) |> Map.update(:position, nil, &(&1 + 1)) end def right(%__MODULE__{r: []} = zipper) do zipper |> Map.put(:l, append_to_list(zipper.l, zipper.current)) |> Map.update(:position, nil, &(&1 + 1)) end def right(%__MODULE__{} = zipper) do zipper |> Map.put(:l, append_to_list(zipper.l, zipper.current)) |> Map.put(:current, hd(zipper.r)) |> Map.put(:r, tl(zipper.r)) |> Map.update(:position, nil, &(&1 + 1)) end defp append_to_list(list, item), do: Enum.reverse([item] ++ list) @spec replace_at(t, integer, term) :: t def replace_at(%__MODULE__{size: size} = zipper, index, _) when index >= size, do: zipper def replace_at(zipper, 0, new_value) when is_at_start(zipper) do Map.update(zipper, :r, nil, fn r -> List.replace_at(r, 0, new_value) end) end def replace_at(%__MODULE__{position: position} = zipper, index, new_value) when index == position do Map.put(zipper, :current, new_value) end def replace_at(%__MODULE__{position: position} = zipper, index, new_value) when index < position do Map.update(zipper, :l, nil, fn l -> List.replace_at(l, index, new_value) end) end def replace_at(%__MODULE__{position: position} = zipper, index, new_value) when index > position do Map.update(zipper, :r, nil, fn r -> r_index = index - position - 1 List.replace_at(r, r_index, new_value) end) end @spec put_current(t, term) :: t def put_current(%__MODULE__{} = zipper, new_value), do: replace_at(zipper, zipper.position, new_value) @spec rightmost(t) :: term def rightmost(%__MODULE__{} = zipper), do: zipper |> right_items() |> List.last() @spec left_items(t) :: list def left_items(%__MODULE__{} = zipper), do: zipper.l @spec right_items(t) :: list def right_items(%__MODULE__{} = zipper), do: zipper.r @spec to_list(t) :: list def to_list(zipper) when is_at_start(zipper), do: zipper.r def to_list(zipper) when is_at_start(zipper) or is_at_end(zipper), do: zipper.l ++ zipper.r def to_list(%__MODULE__{} = zipper), do: zipper.l ++ [zipper.current] ++ zipper.r @spec end?(t) :: boolean def end?(zipper) when is_at_end(zipper), do: true def end?(_), do: false @spec zipper?(term) :: boolean def zipper?(zipper) when is_zipper(zipper), do: true def zipper?(_), do: false end