Current section

Files

Jump to
caffeine_lang src caffeine_lang@string_distance.erl
Raw

src/caffeine_lang@string_distance.erl

-module(caffeine_lang@string_distance).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/caffeine_lang/string_distance.gleam").
-export([levenshtein/2, closest_match/2]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
-file("src/caffeine_lang/string_distance.gleam", 44).
-spec build_row_loop(
list(integer()),
list(binary()),
binary(),
list(integer()),
integer()
) -> {list(integer()), integer()}.
build_row_loop(Prev_row, B_remaining, A_char, Acc, Prev_val) ->
case {B_remaining, Prev_row} of
{[], _} ->
{Acc, Prev_val};
{[B_char | B_rest], [Diag | Prev_rest]} ->
Above = case Prev_rest of
[V | _] ->
V;
[] ->
0
end,
Cost = case A_char =:= B_char of
true ->
0;
false ->
1
end,
Val = gleam@int:min(
Prev_val + 1,
gleam@int:min(Above + 1, Diag + Cost)
),
build_row_loop(Prev_rest, B_rest, A_char, [Val | Acc], Val);
{_, []} ->
{Acc, Prev_val}
end.
-file("src/caffeine_lang/string_distance.gleam", 33).
?DOC(" Builds one row of the Levenshtein matrix.\n").
-spec build_row(list(integer()), list(binary()), binary(), integer()) -> list(integer()).
build_row(Prev_row, B_graphemes, A_char, Initial_val) ->
{Row, _} = build_row_loop(
Prev_row,
B_graphemes,
A_char,
[Initial_val],
Initial_val
),
lists:reverse(Row).
-file("src/caffeine_lang/string_distance.gleam", 9).
?DOC(" Computes the Levenshtein edit distance between two strings.\n").
-spec levenshtein(binary(), binary()) -> integer().
levenshtein(A, B) ->
A_graphemes = gleam@string:to_graphemes(A),
B_graphemes = gleam@string:to_graphemes(B),
B_len = erlang:length(B_graphemes),
Initial_row = gleam@int:range(B_len, -1, [], fun(Acc, I) -> [I | Acc] end),
Result_row = gleam@list:index_fold(
A_graphemes,
Initial_row,
fun(Prev_row, A_char, I@1) ->
build_row(Prev_row, B_graphemes, A_char, I@1 + 1)
end
),
case gleam@list:last(Result_row) of
{ok, D} ->
D;
{error, nil} ->
0
end.
-file("src/caffeine_lang/string_distance.gleam", 71).
?DOC(
" Returns the closest match from a list of candidates, if within threshold.\n"
" Threshold: distance <= max(2, ceil(length(target) * 0.4)).\n"
).
-spec closest_match(binary(), list(binary())) -> gleam@option:option(binary()).
closest_match(Target, Candidates) ->
Target_len = string:length(Target),
Threshold = gleam@int:max(
2,
erlang:trunc((erlang:float(Target_len) * 0.4) + 0.99)
),
Result = gleam@list:fold(
Candidates,
none,
fun(Best, Candidate) ->
Dist = levenshtein(Target, Candidate),
case Dist > Threshold of
true ->
Best;
false ->
case Best of
none ->
{some, {Candidate, Dist}};
{some, {_, Best_dist}} ->
case Dist < Best_dist of
true ->
{some, {Candidate, Dist}};
false ->
Best
end
end
end
end
),
case Result of
{some, {Name, _}} ->
{some, Name};
none ->
none
end.