Current section
Files
Jump to
Current section
Files
lib/ot/text/component.ex
defmodule OT.Text.Component do
@moduledoc """
An individual unit of work to be performed on a piece of text.
A component represents a retain or modification of the text:
- `5`: Retain 5 characters of the text
- `%{i:"Hello"}`: Insert the string "Hello"
- `%{d:"World"}`: Delete the string "World"
"""
alias OT.Text
alias Text.Operation
@typedoc """
A delete component, in which a string of zero or more characters are deleted
from the text
"""
@type delete :: %{d: Text.datum}
@typedoc """
An insert component, in which a string of zero or more characters are inserted
into the text
"""
@type insert :: %{i: Text.datum}
@typedoc """
A retain component, in which a number of characters in the text are skipped
over
"""
@type retain :: non_neg_integer
@typedoc """
An atom declaring the type of a component
"""
@type type :: :delete | :insert | :retain
@typedoc """
The result of comparing two components
"""
@type comparison :: :eq | :gt | :lt
@typedoc """
A single unit of "work" performed on a piece of text
"""
@type t :: delete | insert | retain
@doc """
Invert a component.
## Examples
iex> OT.Text.Component.invert(%{i: "Foo"})
%{d: "Foo"}
iex> OT.Text.Component.invert(%{d: "Foo"})
%{i: "Foo"}
iex> OT.Text.Component.invert(4)
4
"""
@spec invert(t) :: t
def invert(comp) when is_integer(comp), do: comp
def invert(%{d: del}), do: %{i: del}
def invert(%{i: ins}), do: %{d: ins}
@doc """
Determine the length of a component.
## Examples
iex> OT.Text.Component.length(4)
4
iex> OT.Text.Component.length(%{i: "Foo"})
3
"""
@spec length(t) :: non_neg_integer
def length(comp) when is_integer(comp), do: comp
def length(%{d: del}), do: String.length(del)
def length(%{i: ins}), do: String.length(ins)
@doc """
Determine the type of a component.
## Examples
iex> OT.Text.Component.type(4)
:retain
iex> OT.Text.Component.type(%{i: "Foo"})
:insert
iex> OT.Text.Component.type(%{d: "Foo"})
:delete
"""
@spec type(t) :: type
def type(comp) when is_integer(comp), do: :retain
def type(%{d: _}), do: :delete
def type(%{i: _}), do: :insert
@doc """
Compare the length of two components.
Will return `:gt` if first is greater than second, `:lt` if first is less
than second, or `:eq` if they span equal lengths.
## Example
iex> OT.Text.Component.compare(%{i: "Foo"}, 1)
:gt
"""
@spec compare(t, t) :: comparison
def compare(comp_a, comp_b) do
length_a = __MODULE__.length(comp_a)
length_b = __MODULE__.length(comp_b)
cond do
length_a > length_b -> :gt
length_a < length_b -> :lt
true -> :eq
end
end
@doc """
Join two components into an operation, combining them into a single component
if they are of the same type.
## Example
iex> OT.Text.Component.join(%{i: "Foo"}, %{i: "Bar"})
[%{i: "FooBar"}]
"""
@spec join(t, t) :: Operation.t
def join(retain_a, retain_b)
when is_integer(retain_a) and is_integer(retain_b),
do: [retain_a + retain_b]
def join(%{i: ins_a}, %{i: ins_b}),
do: [%{i: ins_a <> ins_b}]
def join(%{d: del_a}, %{d: del_b}),
do: [%{d: del_a <> del_b}]
def join(comp_a, comp_b),
do: [comp_a, comp_b]
@doc """
Determine whether a comopnent is a no-op.
## Examples
iex> OT.Text.Component.no_op?(0)
true
iex> OT.Text.Component.no_op?(%{i: ""})
true
"""
@spec no_op?(t) :: boolean
def no_op?(0), do: true
def no_op?(%{d: ""}), do: true
def no_op?(%{i: ""}), do: true
def no_op?(_), do: false
@doc """
Split a component at a given index.
Returns a tuple containing a new component before the index, and a new
component after the index.
## Examples
iex> OT.Text.Component.split(4, 3)
{3, 1}
iex> OT.Text.Component.split(%{i: "Foo"}, 2)
{%{i: "Fo"}, %{i: "o"}}
"""
@spec split(t, non_neg_integer) :: {t, t}
def split(comp, index) when is_integer(comp) do
{index, comp - index}
end
def split(%{d: del}, index) do
{%{d: String.slice(del, 0, index)},
%{d: String.slice(del, index..-1)}}
end
def split(%{i: ins}, index) do
{%{i: String.slice(ins, 0, index)},
%{i: String.slice(ins, index..-1)}}
end
@doc false
@spec random(Text.datum) :: t
def random(text), do: do_random(random_type(), text)
@spec do_random(type, Text.datum) :: t
defp do_random(:delete, text),
do: %{d: text}
defp do_random(:insert, _text),
do: %{i: Text.init_random(:rand.uniform(16))}
defp do_random(:retain, text),
do: String.length(text)
@spec random_type :: type
defp random_type, do: Enum.random([:delete, :insert, :retain])
end