Packages

A port of the Hunt-Szymanski diff algorithm to Elixir, with a focus on performance and memory efficiency.

Current section

Files

Jump to
hunt_szymanski_diff lib h_s_diff patch.ex
Raw

lib/h_s_diff/patch.ex

defmodule HSDiff.Patch do
@moduledoc """
Applies a diff ([eq:, del:, ins:]) to an old text, producing the new text.
If the old text is a string, we return a **string** (with lines joined by "\n").
If the old text is a list of lines, we return a **list of lines**.
"""
@doc """
Patches the `old_text` with the given `diff`.
- If `old_text` is a string, returns a **string**.
- If `old_text` is a list of lines, returns a list of lines.
"""
def patch(old_text, diff) when is_binary(old_text) do
old_lines = String.split(old_text, "\n")
new_lines = patch_lines(old_lines, diff, 0, [])
# If old_text was a string, re-join so we return a string
Enum.reverse(new_lines) |> Enum.join("\n")
end
def patch(old_lines, diff) when is_list(old_lines) do
# Return a list of lines
patch_lines(old_lines, diff, 0, []) |> Enum.reverse()
end
# ---------------------------------------------------------------------------
# Internal pointer-based patch logic
# `patch_lines/4` always returns a list of lines (in reverse order).
# We finalize it in patch/2 above, reversing it and re-joining if needed.
# ---------------------------------------------------------------------------
defp patch_lines([], _diff, _i, new_lines_acc), do: new_lines_acc
# In a typical scenario we keep applying each diff block. But we do
# pointer-based processing inside a reduce or recursion. Let’s do
# a simple approach: we define a private function do_patch_diff/3
# that processes a single block, then recursively proceed.
defp patch_lines(_old_lines, [], _i, new_lines_acc) do
# no more diff blocks => we do nothing with leftover old_lines,
# because that leftover is presumably deleted or unreachable.
new_lines_acc
end
defp patch_lines(old_lines, [block | rest], i, new_lines_acc) do
{new_acc, new_i} = apply_block(block, old_lines, i, new_lines_acc)
patch_lines(old_lines, rest, new_i, new_acc)
end
# Each block can be optimized or non-optimized:
defp apply_block({:eq, block_data}, old_lines, i, new_lines_acc) do
eq_count = eq_length(block_data)
eq_segment = Enum.slice(old_lines, i, eq_count)
# Prepend eq_segment in reverse
new_acc = Enum.reverse(eq_segment, new_lines_acc)
{new_acc, i + eq_count}
end
defp apply_block({:del, block_data}, _old_lines, i, new_lines_acc) do
del_count = del_length(block_data)
# We skip these lines from the old text => do not add to new_lines_acc
{new_lines_acc, i + del_count}
end
defp apply_block({:ins, block_data}, _old_lines, i, new_lines_acc) do
ins_segment = ins_lines(block_data)
# Prepend ins_segment in reverse
new_acc = Enum.reverse(ins_segment, new_lines_acc)
# We do not advance i because insertion has no counterpart in old text
{new_acc, i}
end
# If there's any other block type, just skip it or handle it as no-op
defp apply_block(_block, _old_lines, i, new_lines_acc) do
{new_lines_acc, i}
end
# ---------------------------------------------------------------------------
# Helpers for eq/del/ins data
# ---------------------------------------------------------------------------
# eq can be an integer (# lines to copy) or a list of lines.
defp eq_length(block_data) when is_integer(block_data), do: block_data
defp eq_length(block_data) when is_list(block_data), do: length(block_data)
# del can be an integer (# lines to skip) or a list of lines.
defp del_length(block_data) when is_integer(block_data), do: block_data
defp del_length(block_data) when is_list(block_data), do: length(block_data)
# ins is typically a list of lines. If it’s a single string, wrap in list.
defp ins_lines(block_data) when is_list(block_data), do: block_data
defp ins_lines(block_data) when is_binary(block_data), do: [block_data]
end