Permalink
Browse files

Add List.myers_difference/3 (#8021)

This removes the duplication with ExUnit.Diff.

This is done by allowing custom diff scripts when traversing the list.
  • Loading branch information...
josevalim committed Jul 31, 2018
1 parent c9a3c6c commit 2d7bd055ed0f8ee9fdb130bbd969033b705b4943
Showing with 93 additions and 159 deletions.
  1. +70 −20 lib/elixir/lib/list.ex
  2. +23 −139 lib/ex_unit/lib/ex_unit/diff.ex
@@ -863,6 +863,8 @@ defmodule List do
corresponding key is `:del`), or left alone (if the corresponding key is
`:eq`) in `list1` in order to be closer to `list2`.
See `myers_difference/3` if you want to handle nesting in the diff scripts.
## Examples
iex> List.myers_difference([1, 4, 2, 3], [1, 2, 3, 4])
@@ -872,19 +874,49 @@ defmodule List do
@doc since: "1.4.0"
@spec myers_difference(list, list) :: [{:eq | :ins | :del, list}]
def myers_difference(list1, list2) when is_list(list1) and is_list(list2) do
myers_difference_with_diff_script(list1, list2, nil)
end
@doc """
Returns a keyword list that represents an *edit script* with nested diffs.
This is an extension of `myers_difference/2` where a `diff_script` function
can be given in case it is desired to compute nested differences. The function
may return a list with the inner edit script or `nil` in case there is no
such script. The returned inner edit will be under the `:diff` key.
## Examples
iex> List.myers_difference(["a", "db", "c"], ["a", "bc"], &String.myers_difference/2)
[eq: ["a"], diff: [del: "d", eq: "b", ins: "c"], del: ["c"]]
"""
@doc since: "1.8.0"
@spec myers_difference(list, list, (term, term -> script | nil)) :: script
when script: [{:eq | :ins | :del | :diff, list}]
def myers_difference(list1, list2, diff_script)
when is_list(list1) and is_list(list2) and is_function(diff_script) do
myers_difference_with_diff_script(list1, list2, diff_script)
end
defp myers_difference_with_diff_script(list1, list2, diff_script) do
path = {0, list1, list2, []}
find_script(0, length(list1) + length(list2), [path])
find_script(0, length(list1) + length(list2), [path], diff_script)
end
defp find_script(envelope, max, paths) do
case each_diagonal(-envelope, envelope, paths, []) do
defp find_script(envelope, max, paths, diff_script) do
case each_diagonal(-envelope, envelope, paths, [], diff_script) do
{:done, edits} -> compact_reverse(edits, [])
{:next, paths} -> find_script(envelope + 1, max, paths)
{:next, paths} -> find_script(envelope + 1, max, paths, diff_script)
end
end
defp compact_reverse([], acc), do: acc
defp compact_reverse([{:diff, _} = fragment | rest], acc) do
compact_reverse(rest, [fragment | acc])
end
defp compact_reverse([{kind, elem} | rest], [{kind, result} | acc]) do
compact_reverse(rest, [{kind, [elem | result]} | acc])
end
@@ -897,50 +929,68 @@ defmodule List do
compact_reverse(rest, [{kind, [elem]} | acc])
end
defp each_diagonal(diag, limit, _paths, next_paths) when diag > limit do
defp each_diagonal(diag, limit, _paths, next_paths, _diff_script) when diag > limit do
{:next, :lists.reverse(next_paths)}
end
defp each_diagonal(diag, limit, paths, next_paths) do
{path, rest} = proceed_path(diag, limit, paths)
defp each_diagonal(diag, limit, paths, next_paths, diff_script) do
{path, rest} = proceed_path(diag, limit, paths, diff_script)
case follow_snake(path) do
{:cont, path} -> each_diagonal(diag + 2, limit, rest, [path | next_paths])
{:cont, path} -> each_diagonal(diag + 2, limit, rest, [path | next_paths], diff_script)
{:done, edits} -> {:done, edits}
end
end
defp proceed_path(0, 0, [path]), do: {path, []}
defp proceed_path(0, 0, [path], _diff_script), do: {path, []}
defp proceed_path(diag, limit, [path | _] = paths) when diag == -limit do
{move_down(path), paths}
defp proceed_path(diag, limit, [path | _] = paths, diff_script) when diag == -limit do
{move_down(path, diff_script), paths}
end
defp proceed_path(diag, limit, [path]) when diag == limit do
{move_right(path), []}
defp proceed_path(diag, limit, [path], diff_script) when diag == limit do
{move_right(path, diff_script), []}
end
defp proceed_path(_diag, _limit, [path1, path2 | rest]) do
defp proceed_path(_diag, _limit, [path1, path2 | rest], diff_script) do
if elem(path1, 0) > elem(path2, 0) do
{move_right(path1), [path2 | rest]}
{move_right(path1, diff_script), [path2 | rest]}
else
{move_down(path2), [path2 | rest]}
{move_down(path2, diff_script), [path2 | rest]}
end
end
defp move_right({y, list1, [elem | rest], edits}) do
defp move_right({y, [elem1 | rest1] = list1, [elem2 | rest2], edits}, diff_script)
when diff_script != nil do
if diff = diff_script.(elem1, elem2) do
{y + 1, rest1, rest2, [{:diff, diff} | edits]}
else
{y, list1, rest2, [{:ins, elem2} | edits]}
end
end
defp move_right({y, list1, [elem | rest], edits}, _diff_script) do
{y, list1, rest, [{:ins, elem} | edits]}
end
defp move_right({y, list1, [], edits}) do
defp move_right({y, list1, [], edits}, _diff_script) do
{y, list1, [], edits}
end
defp move_down({y, [elem | rest], list2, edits}) do
defp move_right({y, [elem1 | rest1], [elem2 | rest2] = list2, edits}, diff_script)
when diff_script != nil do
if diff = diff_script.(elem1, elem2) do
{y + 1, rest1, rest2, [{:diff, diff} | edits]}
else
{y + 1, rest1, list2, [{:del, elem1} | edits]}
end
end
defp move_down({y, [elem | rest], list2, edits}, _diff_script) do
{y + 1, rest, list2, [{:del, elem} | edits]}
end
defp move_down({y, [], list2, edits}) do
defp move_down({y, [], list2, edits}, _diff_script) do
{y + 1, [], list2, edits}
end
@@ -53,7 +53,7 @@ defmodule ExUnit.Diff do
script_string(List.to_string(left), List.to_string(right), ?')
else
keywords? = Inspect.List.keyword?(left) and Inspect.List.keyword?(right)
script = script_list(left, right, keywords?)
script = script_maybe_improper_list(left, right, keywords?)
[{:eq, "["}, script, {:eq, "]"}]
end
end
@@ -67,15 +67,7 @@ defmodule ExUnit.Diff do
# Tuples
def script(left, right) when is_tuple(left) and is_tuple(right) do
script =
script_list(
Tuple.to_list(left),
tuple_size(left),
Tuple.to_list(right),
tuple_size(right),
false
)
script = script_list(Tuple.to_list(left), Tuple.to_list(right), false)
[{:eq, "{"}, script, {:eq, "}"}]
end
@@ -95,23 +87,15 @@ defmodule ExUnit.Diff do
String.myers_difference(string1, string2)
end
defp length_and_slice_proper_part([item | rest], length, result) do
length_and_slice_proper_part(rest, length + 1, [item | result])
end
defp slice_proper_part([item | rest], result), do: slice_proper_part(rest, [item | result])
defp slice_proper_part([], result), do: {Enum.reverse(result), []}
defp slice_proper_part(item, result), do: {Enum.reverse(result), [item]}
defp length_and_slice_proper_part([], length, result) do
{length, Enum.reverse(result), []}
end
defp script_maybe_improper_list(list1, list2, keywords?) do
{list1, improper_rest1} = slice_proper_part(list1, [])
{list2, improper_rest2} = slice_proper_part(list2, [])
defp length_and_slice_proper_part(item, length, result) do
{length, Enum.reverse(result), [item]}
end
defp script_list(list1, list2, keywords?) do
{length1, list1, improper_rest1} = length_and_slice_proper_part(list1, 0, [])
{length2, list2, improper_rest2} = length_and_slice_proper_part(list2, 0, [])
script = script_list(list1, length1, list2, length2, keywords?)
script = script_list(list1, list2, keywords?)
case {improper_rest1, improper_rest2} do
{[item1], [item2]} ->
@@ -128,19 +112,23 @@ defmodule ExUnit.Diff do
end
end
defp script_list(list1, length1, list2, length2, keywords?) do
case script_subset_list(list1, list2) do
{:ok, script} ->
format_each_fragment(script, [], keywords?)
:error ->
initial_path = {0, 0, list1, list2, []}
defp script_list(list1, list2, keywords?) do
script =
case script_subset_list(list1, list2) do
{:ok, script} -> script
:error when keywords? -> List.myers_difference(list1, list2, &script_keyword/2)
:error -> List.myers_difference(list1, list2, &script/2)
end
find_script(0, length1 + length2, [initial_path], keywords?)
|> format_each_fragment([], keywords?)
end
format_each_fragment(script, [], keywords?)
end
defp script_keyword({key, val1}, {key, val2}),
do: [{:eq, format_key(key, true)}, script_inner(val1, val2)]
defp script_keyword(_pair1, _pair2),
do: nil
defp script_subset_list(list1, list2) do
case find_subset_list(list1, list2, []) do
{subset, rest1, rest2} ->
@@ -232,110 +220,6 @@ defmodule ExUnit.Diff do
{kind, Enum.map_join(elems, ", ", formatter)}
end
defp find_script(envelope, max, paths, keywords?) do
case each_diagonal(-envelope, envelope, paths, [], keywords?) do
{:done, edits} ->
compact_reverse(edits, [])
{:next, paths} ->
find_script(envelope + 1, max, paths, keywords?)
end
end
defp compact_reverse([], acc), do: acc
defp compact_reverse([{:diff, _} = fragment | rest], acc),
do: compact_reverse(rest, [fragment | acc])
defp compact_reverse([{kind, char} | rest], [{kind, chars} | acc]),
do: compact_reverse(rest, [{kind, [char | chars]} | acc])
defp compact_reverse([{kind, char} | rest], acc),
do: compact_reverse(rest, [{kind, [char]} | acc])
defp each_diagonal(diag, limit, _paths, next_paths, _keywords?) when diag > limit do
{:next, Enum.reverse(next_paths)}
end
defp each_diagonal(diag, limit, paths, next_paths, keywords?) do
{path, rest} = proceed_path(diag, limit, paths, keywords?)
with {:cont, path} <- follow_snake(path) do
each_diagonal(diag + 2, limit, rest, [path | next_paths], keywords?)
end
end
defp proceed_path(0, 0, [path], _keywords?), do: {path, []}
defp proceed_path(diag, limit, [path | _] = paths, keywords?) when diag == -limit do
{move_down(path, keywords?), paths}
end
defp proceed_path(diag, limit, [path], keywords?) when diag == limit do
{move_right(path, keywords?), []}
end
defp proceed_path(_diag, _limit, [path1, path2 | rest], keywords?) do
if elem(path1, 1) > elem(path2, 1) do
{move_right(path1, keywords?), [path2 | rest]}
else
{move_down(path2, keywords?), [path2 | rest]}
end
end
defp script_keyword_inner({key, val1}, {key, val2}, true),
do: [{:eq, format_key(key, true)}, script_inner(val1, val2)]
defp script_keyword_inner(_pair1, _pair2, true),
do: nil
defp script_keyword_inner(elem1, elem2, false),
do: script(elem1, elem2)
defp move_right({x, x, [elem1 | rest1] = list1, [elem2 | rest2], edits}, keywords?) do
if result = script_keyword_inner(elem1, elem2, keywords?) do
{x + 1, x + 1, rest1, rest2, [{:diff, result} | edits]}
else
{x + 1, x, list1, rest2, [{:ins, elem2} | edits]}
end
end
defp move_right({x, y, list1, [elem | rest], edits}, _keywords?) do
{x + 1, y, list1, rest, [{:ins, elem} | edits]}
end
defp move_right({x, y, list1, [], edits}, _keywords?) do
{x + 1, y, list1, [], edits}
end
defp move_down({x, x, [elem1 | rest1], [elem2 | rest2] = list2, edits}, keywords?) do
if result = script_keyword_inner(elem1, elem2, keywords?) do
{x + 1, x + 1, rest1, rest2, [{:diff, result} | edits]}
else
{x, x + 1, rest1, list2, [{:del, elem1} | edits]}
end
end
defp move_down({x, y, [elem | rest], list2, edits}, _keywords?) do
{x, y + 1, rest, list2, [{:del, elem} | edits]}
end
defp move_down({x, y, [], list2, edits}, _keywords?) do
{x, y + 1, [], list2, edits}
end
defp follow_snake({x, y, [elem | rest1], [elem | rest2], edits}) do
follow_snake({x + 1, y + 1, rest1, rest2, [{:eq, elem} | edits]})
end
defp follow_snake({_x, _y, [], [], edits}) do
{:done, edits}
end
defp follow_snake(path) do
{:cont, path}
end
defp script_map(left, right, name) do
{surplus, altered, missing, same} = map_difference(left, right)

0 comments on commit 2d7bd05

Please sign in to comment.