defmodule PieceTable.Differ do
@moduledoc """
A module to calculate the difference between a text and another text or a piece table.
## Usage
```elixir
iex> PieceTable.Differ.diff("test", "text")
{:ok,
%PieceTable{
original: "test",
result: "text",
applied: [
%PieceTable.Change{change: :ins, text: "x", position: 2},
%PieceTable.Change{change: :del, text: "s", position: 2}
],
to_apply: []
}}
```
"""
alias PieceTable.Change
@type original_input :: PieceTable.t() | String.t()
@doc """
Computes the difference between the original input and a modified string using the PieceTable data structure.
## Parameters
- `original` (original_input()): The original input string.
- `modified` (String.t()): The modified string.
## Returns
- `{:ok, %PieceTable{}}`: A tuple containing the modified PieceTable structure.
- `{:error, String.t()}`: An error message indicating wrong arguments.
## Examples
iex> PieceTable.Differ.diff("test", "text")
{:ok,
%PieceTable{
original: "test",
result: "text",
applied: [
%PieceTable.Change{change: :ins, text: "x", position: 2},
%PieceTable.Change{change: :del, text: "s", position: 2}
],
to_apply: []
}}
iex> PieceTable.Differ.diff("test", 42)
{:error, "Wrong arguments"}
"""
@spec diff(original_input(), String.t()) :: {:ok, PieceTable.t()}
def diff(original, modified) when is_binary(original) and is_binary(modified),
do: original |> PieceTable.new!() |> diff(modified)
def diff(%PieceTable{} = table, modified) when is_binary(modified) do
{table, _} =
table.result
|> String.myers_difference(modified)
|> Enum.reduce({table, 0}, &add_edit/2)
{:ok, table}
end
def diff(_, _), do: {:error, "Wrong arguments"}
@doc """
Computes the difference between the original input and a modified string using the PieceTable data structure.
## Parameters
- `original` (original_input()): The original input string.
- `modified` (String.t()): The modified string.
## Returns
- `%PieceTable{}`: A tuple containing the modified PieceTable structure.
## Examples
iex> PieceTable.Differ.diff!("test", "text")
%PieceTable{
original: "test",
result: "text",
applied: [
%PieceTable.Change{change: :ins, text: "x", position: 2},
%PieceTable.Change{change: :del, text: "s", position: 2}
],
to_apply: []
}
"""
@spec diff!(String.t(), String.t()) :: PieceTable.t()
def diff!(original, modified), do: diff(original, modified) |> raise_or_return()
defp add_edit({:eq, text}, {table, pos}),
do: {table, pos + String.length(text)}
defp add_edit({:ins, text}, {table, pos}) do
change = Change.new!(:ins, text, pos)
table = PieceTable.update_piece_table(table, change)
pos = pos + String.length(text)
{table, pos}
end
defp add_edit({:del, text}, {table, pos}) do
change = Change.new!(:del, text, pos)
table = PieceTable.update_piece_table(table, change)
{table, pos}
end
# handle responses, raises if :error atom
defp raise_or_return({:ok, result}), do: result
defp raise_or_return({:error, args}), do: raise(ArgumentError, args)
end