Current section
Files
Jump to
Current section
Files
src/starfuzz@rank.erl
-module(starfuzz@rank).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/starfuzz/rank.gleam").
-export([combine/1, builder/0, add_signal/4, with_minimum_score/2, with_limit/2, calculate/2]).
-export_type([rank_error/0, component_score/0, ranking/1, signal/1, rank_builder/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(
" A module providing ranking and composite scoring capabilities.\n"
" Multiple scoring signals can be combined with weights into a single score,\n"
" with automatic weight normalization and stable tie-breaking descending sorting.\n"
).
-type rank_error() :: zero_weight_sum | {negative_weight, binary()}.
-type component_score() :: {component_score, binary(), float(), float()}.
-type ranking(DRT) :: {ranking, DRT, float(), list(component_score())}.
-type signal(DRU) :: {signal, binary(), float(), fun((DRU) -> float())}.
-type rank_builder(DRV) :: {rank_builder,
list(signal(DRV)),
float(),
gleam@option:option(integer())}.
-file("src/starfuzz/rank.gleam", 37).
?DOC(
" Combines a list of component scores using a weighted average.\n"
" Auto-normalizes weights: `sum(score * weight) / sum(weight)`.\n"
).
-spec combine(list(component_score())) -> {ok, float()} | {error, rank_error()}.
combine(Components) ->
Check = gleam@list:fold(
Components,
{ok, {+0.0, +0.0}},
fun(Acc, Comp) -> case Acc of
{error, E} ->
{error, E};
{ok, {Sum_weighted, Sum_weight}} ->
case erlang:element(4, Comp) < +0.0 of
true ->
{error, {negative_weight, erlang:element(2, Comp)}};
false ->
{ok,
{Sum_weighted + (erlang:element(3, Comp) * erlang:element(
4,
Comp
)),
Sum_weight + erlang:element(4, Comp)}}
end
end end
),
case Check of
{error, E@1} ->
{error, E@1};
{ok, {Sum_weighted@1, Sum_weight@1}} ->
case Sum_weight@1 =:= +0.0 of
true ->
{error, zero_weight_sum};
false ->
{ok, case Sum_weight@1 of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> Sum_weighted@1 / Gleam@denominator
end}
end
end.
-file("src/starfuzz/rank.gleam", 83).
?DOC(" Creates a new default RankBuilder with no signals, min score of 0.0, and no limit.\n").
-spec builder() -> rank_builder(any()).
builder() ->
{rank_builder, [], +0.0, none}.
-file("src/starfuzz/rank.gleam", 92).
?DOC(" Adds a named scoring signal with its weight and scorer callback.\n").
-spec add_signal(rank_builder(DSC), binary(), float(), fun((DSC) -> float())) -> rank_builder(DSC).
add_signal(Builder, Name, Weight, Scorer) ->
{rank_builder,
[{signal, Name, Weight, Scorer} | erlang:element(2, Builder)],
erlang:element(3, Builder),
erlang:element(4, Builder)}.
-file("src/starfuzz/rank.gleam", 97).
?DOC(" Configures the minimum combined score threshold. Rankings below this are discarded.\n").
-spec with_minimum_score(rank_builder(DSG), float()) -> rank_builder(DSG).
with_minimum_score(Builder, Min_score) ->
{rank_builder,
erlang:element(2, Builder),
Min_score,
erlang:element(4, Builder)}.
-file("src/starfuzz/rank.gleam", 102).
?DOC(" Configures a limit on the number of returned rankings.\n").
-spec with_limit(rank_builder(DSJ), integer()) -> rank_builder(DSJ).
with_limit(Builder, Limit) ->
{rank_builder,
erlang:element(2, Builder),
erlang:element(3, Builder),
{some, Limit}}.
-file("src/starfuzz/rank.gleam", 108).
?DOC(
" Calculates combined rankings for a list of candidates.\n"
" Returns ranked items sorted descending by combined score, with index-based stable sorting.\n"
).
-spec calculate(rank_builder(DSM), list(DSM)) -> {ok, list(ranking(DSM))} |
{error, rank_error()}.
calculate(Builder, Candidates) ->
Result = gleam@list:fold(
Candidates,
{ok, []},
fun(Acc_state, Candidate) -> case Acc_state of
{error, E} ->
{error, E};
{ok, Acc} ->
Components_res = gleam@list:fold(
erlang:element(2, Builder),
{ok, []},
fun(Comp_acc_state, Signal) -> case Comp_acc_state of
{error, E@1} ->
{error, E@1};
{ok, Comp_acc} ->
case erlang:element(3, Signal) < +0.0 of
true ->
{error,
{negative_weight,
erlang:element(2, Signal)}};
false ->
Score = (erlang:element(4, Signal))(
Candidate
),
{ok,
[{component_score,
erlang:element(
2,
Signal
),
Score,
erlang:element(
3,
Signal
)} |
Comp_acc]}
end
end end
),
case Components_res of
{error, E@2} ->
{error, E@2};
{ok, Components} ->
case combine(Components) of
{error, E@3} ->
{error, E@3};
{ok, Score@1} ->
case Score@1 >= erlang:element(3, Builder) of
true ->
{ok,
[{ranking,
Candidate,
Score@1,
Components} |
Acc]};
false ->
{ok, Acc}
end
end
end
end end
),
case Result of
{error, E@4} ->
{error, E@4};
{ok, Rankings} ->
Indexed = gleam@list:index_map(
lists:reverse(Rankings),
fun(R, I) -> {R, I} end
),
Sorted = gleam@list:sort(
Indexed,
fun(A, B) ->
case erlang:element(3, erlang:element(1, A)) > erlang:element(
3,
erlang:element(1, B)
) of
true ->
lt;
false ->
case erlang:element(3, erlang:element(1, A)) < erlang:element(
3,
erlang:element(1, B)
) of
true ->
gt;
false ->
case erlang:element(2, A) < erlang:element(
2,
B
) of
true ->
lt;
false ->
gt
end
end
end
end
),
Ranked = gleam@list:map(Sorted, fun(X) -> erlang:element(1, X) end),
case erlang:element(4, Builder) of
{some, L} ->
{ok, gleam@list:take(Ranked, L)};
none ->
{ok, Ranked}
end
end.