src/textmetrics@edit.erl

-module(textmetrics@edit).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/textmetrics/edit.gleam").
-export([recover_old/1, recover_new/1, cost/1, runs/1]).
-export_type([edit/1, run/1]).

-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.

?MODULEDOC(
    " Edit-script algebraic data type and helpers shared by the diff\n"
    " module.\n"
    "\n"
    " An [`EditScript`](#EditScript) is a linear sequence of\n"
    " [`Edit`](#Edit) values describing how to transform one list into\n"
    " another. Replaying the `Equal` and `Delete` steps recovers the\n"
    " original (`old`) list, while replaying `Equal` and `Insert` steps\n"
    " recovers the new list.\n"
    "\n"
    " This module operates on plain lists; it does not perform Unicode\n"
    " normalization. Callers comparing strings should split inputs into\n"
    " graphemes via `gleam/string.to_graphemes` before computing a diff.\n"
).

-type edit(DKT) :: {equal, DKT} | {insert, DKT} | {delete, DKT}.

-type run(DKU) :: {equal_run, list(DKU)} |
    {insert_run, list(DKU)} |
    {delete_run, list(DKU)}.

-file("src/textmetrics/edit.gleam", 54).
-spec do_recover_old(list(edit(DLB)), list(DLB)) -> list(DLB).
do_recover_old(Script, Acc) ->
    case Script of
        [] ->
            lists:reverse(Acc);

        [{equal, Item} | Rest] ->
            do_recover_old(Rest, [Item | Acc]);

        [{delete, Item@1} | Rest@1] ->
            do_recover_old(Rest@1, [Item@1 | Acc]);

        [{insert, _} | Rest@2] ->
            do_recover_old(Rest@2, Acc)
    end.

-file("src/textmetrics/edit.gleam", 50).
?DOC(
    " Replay the `Equal` and `Delete` steps of `script` to recover the\n"
    " `old` list.\n"
    "\n"
    " `recover_old(diff(old, new)) == old` for every input pair, by\n"
    " construction.\n"
).
-spec recover_old(list(edit(DKY))) -> list(DKY).
recover_old(Script) ->
    do_recover_old(Script, []).

-file("src/textmetrics/edit.gleam", 72).
-spec do_recover_new(list(edit(DLI)), list(DLI)) -> list(DLI).
do_recover_new(Script, Acc) ->
    case Script of
        [] ->
            lists:reverse(Acc);

        [{equal, Item} | Rest] ->
            do_recover_new(Rest, [Item | Acc]);

        [{insert, Item@1} | Rest@1] ->
            do_recover_new(Rest@1, [Item@1 | Acc]);

        [{delete, _} | Rest@2] ->
            do_recover_new(Rest@2, Acc)
    end.

-file("src/textmetrics/edit.gleam", 68).
?DOC(
    " Replay the `Equal` and `Insert` steps of `script` to recover the\n"
    " `new` list.\n"
    "\n"
    " `recover_new(diff(old, new)) == new` for every input pair, by\n"
    " construction.\n"
).
-spec recover_new(list(edit(DLF))) -> list(DLF).
recover_new(Script) ->
    do_recover_new(Script, []).

-file("src/textmetrics/edit.gleam", 90).
-spec do_cost(list(edit(any())), integer()) -> integer().
do_cost(Script, Acc) ->
    case Script of
        [] ->
            Acc;

        [{equal, _} | Rest] ->
            do_cost(Rest, Acc);

        [{insert, _} | Rest@1] ->
            do_cost(Rest@1, Acc + 1);

        [{delete, _} | Rest@2] ->
            do_cost(Rest@2, Acc + 1)
    end.

-file("src/textmetrics/edit.gleam", 86).
?DOC(
    " Total number of `Insert` and `Delete` operations in `script`.\n"
    "\n"
    " For an optimal Myers diff this equals\n"
    " `distance.levenshtein_list(old, new)` with substitution counted as\n"
    " one insert plus one delete.\n"
).
-spec cost(list(edit(any()))) -> integer().
cost(Script) ->
    do_cost(Script, 0).

-file("src/textmetrics/edit.gleam", 122).
-spec take_while_equal(list(edit(DMA)), list(DMA)) -> {list(DMA),
    list(edit(DMA))}.
take_while_equal(Script, Acc) ->
    case Script of
        [{equal, Item} | Rest] ->
            take_while_equal(Rest, [Item | Acc]);

        _ ->
            {lists:reverse(Acc), Script}
    end.

-file("src/textmetrics/edit.gleam", 132).
-spec take_while_insert(list(edit(DMF)), list(DMF)) -> {list(DMF),
    list(edit(DMF))}.
take_while_insert(Script, Acc) ->
    case Script of
        [{insert, Item} | Rest] ->
            take_while_insert(Rest, [Item | Acc]);

        _ ->
            {lists:reverse(Acc), Script}
    end.

-file("src/textmetrics/edit.gleam", 142).
-spec take_while_delete(list(edit(DMK)), list(DMK)) -> {list(DMK),
    list(edit(DMK))}.
take_while_delete(Script, Acc) ->
    case Script of
        [{delete, Item} | Rest] ->
            take_while_delete(Rest, [Item | Acc]);

        _ ->
            {lists:reverse(Acc), Script}
    end.

-file("src/textmetrics/edit.gleam", 104).
-spec do_runs(list(edit(DLU)), list(run(DLU))) -> list(run(DLU)).
do_runs(Script, Acc) ->
    case Script of
        [] ->
            lists:reverse(Acc);

        [{equal, Item} | Rest] ->
            {Items, Remaining} = take_while_equal(Rest, [Item]),
            do_runs(Remaining, [{equal_run, Items} | Acc]);

        [{insert, Item@1} | Rest@1] ->
            {Items@1, Remaining@1} = take_while_insert(Rest@1, [Item@1]),
            do_runs(Remaining@1, [{insert_run, Items@1} | Acc]);

        [{delete, Item@2} | Rest@2] ->
            {Items@2, Remaining@2} = take_while_delete(Rest@2, [Item@2]),
            do_runs(Remaining@2, [{delete_run, Items@2} | Acc])
    end.

-file("src/textmetrics/edit.gleam", 100).
?DOC(" Group consecutive edits that share a constructor into runs.\n").
-spec runs(list(edit(DLQ))) -> list(run(DLQ)).
runs(Script) ->
    do_runs(Script, []).