Packages

Property based testing library for exploring different executions in an imperative style.

Current section

Files

Jump to
dorer src dorer_list_utils.erl
Raw

src/dorer_list_utils.erl

-module(dorer_list_utils).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-endif.
-export([group_by_first/1, group_by/2, group_by/4, reduce/2, topsort/2, is_prefix/2, with_index/1, iterate/2, pick_most_similar/2, confuse_dialyzer/1, loop/2, limit/2]).
%% groups a list of key-value pairs by key
-spec group_by_first([{K, V}]) -> #{K => [V]}.
group_by_first(List) ->
group_by(
fun({K, _}) -> K end,
fun({_, V}) -> [V] end,
fun({_, V}, Xs) -> [V | Xs] end,
List
).
% groups
-spec group_by(fun((E) -> K), [E]) -> #{K => [E]}.
group_by(F, List) ->
group_by(F, fun(X) -> [X] end, fun(X, Xs) -> [X | Xs] end, List).
-spec group_by(fun((E) -> K), fun((E) -> V), fun((E, V) -> V), [E]) -> #{K => V}.
group_by(F, Init, Merge, List) ->
lists:foldr(fun(X, M) ->
K = F(X),
maps:update_with(K, fun(L) -> Merge(X, L) end, Init(X), M)
end, maps:new(), List).
-spec reduce(fun((E, E) -> E), [E]) -> E.
reduce(_, []) -> throw('cannot reduce empty list');
reduce(_, [X]) -> X;
reduce(M, [X, Y | Xs]) -> reduce(M, [M(X, Y) | Xs]).
topsort(_Cmp, []) -> [];
topsort(Cmp, Xs) ->
% Min are all elements X from Xs, such that for all elements Y from Xs: not Y < X
{Min, NotMin} = lists:partition(
fun(X) ->
lists:all(fun(Y) -> not Cmp(Y, X) end, Xs)
end,
Xs
),
Min ++ topsort(Cmp, NotMin).
-spec is_prefix([T], [T]) -> boolean().
is_prefix([], _) -> true;
is_prefix([X | Xs], [X | Ys]) ->
is_prefix(Xs, Ys);
is_prefix(_, _) -> false.
-spec with_index([T]) -> [{non_neg_integer(), T}].
with_index(List) ->
lists:zip(lists:seq(0, length(List) - 1), List).
-spec iterate(T, fun((T) -> break | {continue, T})) -> [T].
iterate(Start, Fun) ->
case Fun(Start) of
break -> [];
{continue, X} -> [X | iterate(X, Fun)]
end.
-spec loop(T, fun((T) -> {return, X} | {continue, T})) -> X.
loop(Start, Fun) ->
case Fun(Start) of
{return, X} -> X;
{continue, X} -> loop(X, Fun)
end.
-spec limit(integer(), [T]) -> [T].
limit(N, [X | Xs]) when N > 0 ->
[X | limit(N - 1, Xs)];
limit(_, _) -> [].
pick_most_similar(_Elem, []) -> throw('no choices available');
pick_most_similar(Elem, List) ->
case lists:member(Elem, List) of
true -> Elem;
false ->
WithSimilarity = [{similarity(Elem, X), X} || X <- List],
{_, Res} = lists:max(WithSimilarity),
Res
end.
similarity(X, X) -> 1;
similarity(X, Y) when is_atom(X) andalso is_atom(Y) ->
0.1;
similarity(X, Y) when is_list(X) andalso is_list(Y) ->
case {X, Y} of
{[], _} -> 0.1;
{_, []} -> 0.1;
{[A | As], [A | Bs]} ->
L = max(length(As), length(Bs)),
1 / (1 + L) + similarity(As, Bs) * L / (1 + L);
{[A | As], Xs} ->
L = max(1 + length(As), length(Xs)),
Sim = pick_most_similar(A, Xs),
similarity(A, Sim) / L + similarity(As, Xs -- [Sim]) * (L - 1) / L
end;
similarity(X, Y) when is_tuple(X) andalso is_tuple(Y) ->
0.5 + similarity(tuple_to_list(X), tuple_to_list(Y)) * 0.5;
similarity(X, Y) when is_map(X) andalso is_map(Y) ->
0.5 + similarity(maps:to_list(X), maps:to_list(Y)) * 0.5;
similarity(_, _) -> 0.
% the identitiy function to confuse dialyzer in case of false positives
-spec confuse_dialyzer(T) -> T.
confuse_dialyzer(X) -> X.
-ifdef(TEST).
group_by_first_test() ->
M = group_by_first([{a, 1}, {b, 2}, {a, 3}, {a, 4}]),
?assertEqual(#{a => [1, 3, 4], b => [2]}, M).
with_index_test() ->
?assertEqual([{0, a}, {1, b}, {2, c}], with_index([a, b, c])).
-endif.