-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.