-module(textmetrics@search).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/textmetrics/search.gleam").
-export([did_you_mean/3, closest/3, rank_jaro_winkler/3]).
-export_type([ranked/0]).
-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(
" Convenience helpers for spell-correction-style search built on\n"
" [`distance`](textmetrics/distance.html) and\n"
" [`similarity`](textmetrics/similarity.html).\n"
"\n"
" Both functions are deterministic: ties are broken by the\n"
" candidates' original input order.\n"
).
-type ranked() :: {ranked, binary(), float()}.
-file("src/textmetrics/search.gleam", 29).
?DOC(
" Return candidates within `max_distance` Levenshtein graphemes of\n"
" `query`, sorted ascending by distance. Empty list when nothing\n"
" matches or when `candidates` is empty.\n"
"\n"
" Ties on distance are broken by the candidate's position in\n"
" `candidates`.\n"
).
-spec did_you_mean(binary(), list(binary()), integer()) -> list(binary()).
did_you_mean(Query, Candidates, Max_distance) ->
_pipe = Candidates,
_pipe@1 = gleam@list:index_map(
_pipe,
fun(C, I) -> {C, textmetrics@distance:levenshtein(Query, C), I} end
),
_pipe@2 = gleam@list:filter(
_pipe@1,
fun(T) ->
{_, D, _} = T,
D =< Max_distance
end
),
_pipe@3 = gleam@list:sort(
_pipe@2,
fun(A, B) ->
{_, Da, Ia} = A,
{_, Db, Ib} = B,
case gleam@int:compare(Da, Db) of
eq ->
gleam@int:compare(Ia, Ib);
O ->
O
end
end
),
gleam@list:map(
_pipe@3,
fun(T@1) ->
{C@1, _, _} = T@1,
C@1
end
).
-file("src/textmetrics/search.gleam", 62).
?DOC(
" Single closest candidate within `max_distance` Levenshtein\n"
" graphemes of `query`. Returns `Error(Nil)` when no candidate is\n"
" close enough or when `candidates` is empty.\n"
"\n"
" This is the convenience form of [`did_you_mean`](#did_you_mean) for\n"
" the dominant CLI use case (\"Unknown command. Did you mean `X`?\").\n"
" Ties on distance are broken by the candidate's position in\n"
" `candidates` — the first qualifying candidate wins.\n"
).
-spec closest(binary(), list(binary()), integer()) -> {ok, binary()} |
{error, nil}.
closest(Query, Candidates, Max_distance) ->
case did_you_mean(Query, Candidates, Max_distance) of
[First | _] ->
{ok, First};
[] ->
{error, nil}
end.
-file("src/textmetrics/search.gleam", 79).
?DOC(
" Rank `candidates` by Jaro-Winkler similarity (Winkler-1990\n"
" defaults) descending, returning up to `top_n` [`Ranked`](#Ranked)\n"
" records.\n"
"\n"
" Ties on similarity are broken by the candidate's position in\n"
" `candidates`. When `top_n <= 0` returns an empty list.\n"
).
-spec rank_jaro_winkler(binary(), list(binary()), integer()) -> list(ranked()).
rank_jaro_winkler(Query, Candidates, Top_n) ->
_pipe = Candidates,
_pipe@1 = gleam@list:index_map(
_pipe,
fun(C, I) -> {C, textmetrics@similarity:jaro_winkler(Query, C), I} end
),
_pipe@2 = gleam@list:sort(
_pipe@1,
fun(A, B) ->
{_, Sa, Ia} = A,
{_, Sb, Ib} = B,
case gleam@float:compare(Sb, Sa) of
eq ->
gleam@int:compare(Ia, Ib);
O ->
O
end
end
),
_pipe@3 = gleam@list:take(_pipe@2, Top_n),
gleam@list:map(
_pipe@3,
fun(T) ->
{C@1, S, _} = T,
{ranked, C@1, S}
end
).