Current section
Files
Jump to
Current section
Files
lib/diff.ex
defmodule Diff do
alias Diff.Matrix
alias Diff.Diffable
@moduledoc """
Functions for performing diffs.
"""
defmodule Insert do
defstruct [:element, :index, :length]
end
defmodule Delete do
defstruct [:element, :index, :length]
end
defmodule Modified do
defstruct [:element, :old_element, :index, :length]
end
defmodule Unchanged do
defstruct [:element, :index, :length]
end
defmodule Ignored do
defstruct [:element, :index, :length]
end
@doc """
Applies the patches from a previous diff to the given string.
Will return the patched version as a list unless a from_list_fn/1 is supplied.
This function will takes the patched list as input and outputs the result.
"""
def patch(original, patches, from_list_fn \\ fn(list) -> list end) do
original = Diffable.to_list(original)
Enum.reduce(patches, original, fn(patch, changed) ->
do_patch(changed, patch)
end)
|> from_list_fn.()
end
defp do_patch(original, %Diff.Insert{element: element, index: index}) do
{ left, right } = Enum.split(original, index)
left ++ element ++ right
end
defp do_patch(original, %Diff.Delete{ element: _, index: index, length: length }) do
{ left, deleted } = Enum.split(original, index)
{ _, right } = Enum.split(deleted, length)
left ++ right
end
defp do_patch(original, %Diff.Modified{element: element, old_element: _, index: index, length: length}) do
{ left, deleted } = Enum.split(original, index)
{ _, right } = Enum.split(deleted, length)
left ++ element ++ right
end
defp do_patch(original, %Diff.Unchanged{}) do
original
end
defp do_patch(original, %Diff.Ignored{element: element, index: index}) do
{ left, right } = Enum.split(original, index)
left ++ element ++ right
end
@doc"""
Creates a list of changes from the orginal binary to the changed one.
Takes the following options:
* `:keep_unchanged` - Keeps unchanged binary parts in the returned patches
* `ignore` - Takes a regex and ignores matches
"""
def diff(original, changed, options \\ []) do
original = Diffable.to_list(original)
changed = Diffable.to_list(changed)
original_length = length(original)
changed_length = length(changed)
longest_common_subsequence(original, changed, original_length, changed_length)
|> build_diff(original, changed, original_length, changed_length, [], options)
|> build_changes(options)
end
defp longest_common_subsequence(x, y, x_length, y_length) do
matrix = Matrix.new(x_length + 1, y_length + 1)
matrix = Enum.reduce(1..x_length, matrix, fn(i, matrix) ->
Enum.reduce(1..y_length, matrix, fn(j, matrix) ->
if Enum.fetch!(x, i-1) == Enum.fetch!(y, j-1) do
value = Matrix.get(matrix, i-1, j-1)
Matrix.put(matrix, i, j, value + 1)
else
original_value = Matrix.get(matrix, i, j-1)
changed_value = Matrix.get(matrix, i - 1, j)
Matrix.put(matrix, i, j, max(original_value, changed_value))
end
end)
end)
matrix
end
defp build_diff(matrix, x, y, i, j, edits, options) do
cond do
i > 0 and j > 0 and Enum.fetch!(x, i-1) == Enum.fetch!(y, j-1) ->
if Dict.get(options, :keep_unchanged, false) do
edits = edits ++ [{:unchanged, Enum.fetch!(x, i-1), i-1}]
end
build_diff(matrix, x, y, i-1, j-1, edits, options)
j > 0 and (i == 0 or Matrix.get(matrix, i, j-1) >= Matrix.get(matrix,i-1, j)) ->
build_diff(matrix, x, y, i, j-1, edits ++ [{:insert, Enum.fetch!(y, j-1), j-1}], options)
i > 0 and (j == 0 or Matrix.get(matrix, i, j-1) < Matrix.get(matrix, i-1, j)) ->
build_diff(matrix, x, y, i-1, j, edits ++ [{:delete, Enum.fetch!(x, i-1), i-1}], options)
true ->
edits |> Enum.reverse
end
end
defp build_changes(edits, options) do
Enum.reduce(edits, [], fn({type, char, index}, changes) ->
if changes == [] do
changes ++ [change(type, char, index)]
else
change = List.last(changes)
regex = Dict.get(options, :ignore)
cond do
regex && Regex.match?(regex, char) ->
changes ++ [change(:ignored, char, index)]
is_type(change, type) && index == (change.index + change.length) ->
change = %{change | element: change.element ++ [char], length: change.length + 1 }
if regex && Regex.match?(regex, Enum.join(change.element)) do
change = %Ignored{ element: change.element, index: change.index, length: change.length }
end
List.replace_at(changes, length(changes)-1, change)
true ->
changes ++ [change(type, char, index)]
end
end
end)
|> Enum.reduce([], fn(x, changes) ->
if changes == [] do
[x]
else
last_change = List.last(changes)
if is_type(last_change, :delete) and is_type(x, :insert) and last_change.index == x.index and last_change.length == x.length do
last_change = %Modified{ element: x.element, old_element: last_change.element, index: x.index, length: x.length }
List.replace_at(changes, length(changes) - 1, last_change)
else
changes ++ [x]
end
end
end)
end
defp change(:insert, char, index) do
%Insert{ element: [char], index: index, length: 1 }
end
defp change(:delete, char, index) do
%Delete{ element: [char], index: index, length: 1 }
end
defp change(:unchanged, char, index) do
%Unchanged{ element: [char], index: index, length: 1 }
end
defp change(:ignored, char, index) do
%Ignored{ element: [char], index: index, length: 1 }
end
defp is_type(%Insert{}, :insert) do
true
end
defp is_type(%Delete{}, :delete) do
true
end
defp is_type(%Unchanged{}, :unchanged) do
true
end
defp is_type(%Ignored{}, :ignored) do
true
end
defp is_type(_, _) do
false
end
end