Packages

OT provides libraries for operational transformation, which is a method of achieving consistency in a collaborative software system.

Current section

Files

Jump to
ot_ex lib ot text transformation.ex
Raw

lib/ot/text/transformation.ex

defmodule OT.Text.Transformation do
@moduledoc """
The transformation of two concurrent operations such that they satisfy the
[TP1][tp1] property of operational transformation.
[tp1]: https://en.wikipedia.org/wiki/Operational_transformation#Convergence_properties
"""
alias OT.Text.{Component, Operation, Scanner}
@doc """
Transform an operation against another operation.
Given an operation A that occurred at the same time as operation B against the
same text state, transform the components of operation A such that the state
of the text after applying operation A and then operation B is the same as
after applying operation B and then the transformation of operation A against
operation B:
*S ○ Oa ○ transform(Ob, Oa) = S ○ Ob ○ transform(Oa, Ob)*
This function also takes a third `side` argument that indicates which
operation came later. This is important when deciding whether it is acceptable
to break up insert components from one operation or the other.
"""
@spec transform(Operation.t, Operation.t, OT.Type.side) :: Operation.t
def transform(op_a, op_b, side) do
{op_a, op_b}
|> next
|> do_transform(side)
end
@spec do_transform(Scanner.output, OT.Type.side, Operation.t) :: Operation.t
defp do_transform(next_pair, side, result \\ [])
# Operation A is exhausted
defp do_transform({{nil, _}, _}, _, result) do
result
end
# Operation B is exhausted
defp do_transform({{head_a, tail_a}, {nil, _}}, _, result) do
result
|> Operation.append(head_a)
|> Operation.join(tail_a)
end
# insert / insert / left
defp do_transform({{head_a = %{i: _}, tail_a},
{head_b = %{i: _}, tail_b}}, :left, result) do
{tail_a, [head_b | tail_b]}
|> next
|> do_transform(:left, Operation.append(result, head_a))
end
# insert / insert / left
defp do_transform({{head_a = %{i: _}, tail_a},
{head_b = %{i: _}, tail_b}}, :right, result) do
{[head_a | tail_a], tail_b}
|> next
|> do_transform(:right, Operation.append(result, Component.length(head_b)))
end
# insert / retain
defp do_transform({{head_a = %{i: _}, tail_a},
{head_b, tail_b}}, side, result) when is_integer(head_b) do
{tail_a, [head_b | tail_b]}
|> next
|> do_transform(side, Operation.append(result, head_a))
end
# insert / delete
defp do_transform({{head_a = %{i: _}, tail_a},
{head_b = %{d: _}, tail_b}}, side, result) do
{tail_a, [head_b | tail_b]}
|> next
|> do_transform(side, Operation.append(result, head_a))
end
# retain / insert
defp do_transform({{head_a, tail_a},
{head_b = %{i: _}, tail_b}}, side, result)
when is_integer(head_a) do
{[head_a | tail_a], tail_b}
|> next
|> do_transform(side, Operation.append(result, Component.length(head_b)))
end
# retain / retain
defp do_transform({{head_a, tail_a},
{head_b, tail_b}}, side, result)
when is_integer(head_a) and is_integer(head_b) do
{tail_a, tail_b}
|> next
|> do_transform(side, Operation.append(result, head_a))
end
# retain / delete
defp do_transform({{head_a, tail_a},
{%{d: _}, tail_b}}, side, result)
when is_integer(head_a) do
{tail_a, tail_b}
|> next
|> do_transform(side, result)
end
# delete / insert
defp do_transform({{head_a = %{d: _}, tail_a},
{head_b = %{i: _}, tail_b}}, side, result) do
{[head_a | tail_a], tail_b}
|> next
|> do_transform(side, Operation.append(result, Component.length(head_b)))
end
# delete / retain
defp do_transform({{head_a = %{d: _}, tail_a},
{head_b, tail_b}}, side, result) when is_integer(head_b) do
{tail_a, tail_b}
|> next
|> do_transform(side, Operation.append(result, head_a))
end
# delete / delete
defp do_transform({{%{d: _}, tail_a},
{%{d: _}, tail_b}}, side, result) do
{tail_a, tail_b}
|> next
|> do_transform(side, result)
end
@spec next(Scanner.input) :: Scanner.output
defp next(scanner_input), do: Scanner.next(scanner_input, :insert)
end