From 3e1776536802739d8819351b15d54ec70568aba7 Mon Sep 17 00:00:00 2001 From: Yaroslav Rogov Date: Tue, 25 May 2021 18:25:18 +0300 Subject: [PATCH] feat: Add bunch of useful functions for seqs (#32) * feat: Add bunch of useful functions for seqs * docs: Add docs to new functions * Update src/genlib_map.erl Co-authored-by: ndiezel0 * test: Split test for list:wrap into eunit and prop * test: Split group_by prop tests by semantics * test: move 0-size test for map:fold_while to unit * test: simplify prop_fold_while generator * test: Move white-box unit tests to test/ Co-authored-by: ndiezel0 --- src/genlib_list.erl | 51 +++++++++++++++++++++++ src/genlib_map.erl | 19 +++++++++ test/genlib_list_tests.erl | 10 +++++ test/genlib_map_tests.erl | 10 +++++ test/prop_genlib_list.erl | 85 ++++++++++++++++++++++++++++++++++++++ test/prop_genlib_map.erl | 26 ++++++++++++ 6 files changed, 201 insertions(+) create mode 100644 test/genlib_list_tests.erl create mode 100644 test/prop_genlib_list.erl create mode 100644 test/prop_genlib_map.erl diff --git a/src/genlib_list.erl b/src/genlib_list.erl index 7309933..220bce3 100644 --- a/src/genlib_list.erl +++ b/src/genlib_list.erl @@ -3,6 +3,10 @@ %% API -export([join/2]). -export([compact/1]). +-export([wrap/1]). +-export([group_by/2]). +-export([foldl_while/3]). +-export([orderless_equal/2]). %% %% API @@ -21,3 +25,50 @@ compact(List) -> end, List ). + +%% @doc Wrap given term into a list. +%% If a list is given, this list is returned. +%% If undefined is passed, empty list is returned + +-spec wrap(undefined | list(T) | T) -> [] | list(T) when T :: term(). +wrap(undefined) -> + []; +wrap(List) when is_list(List) -> + List; +wrap(Term) -> + [Term]. + +%% @doc Group values in a given list by result of KeyFun. +%% The result is a map, where keys are results of KeyFun, and values are list of values of original list +%% that result in such key. +-spec group_by(fun((T) -> K), list(T)) -> #{K := list(T)} when T :: term(), K :: term(). +group_by(KeyFun, List) -> + lists:foldl( + fun(Elt, Acc) -> + Key = KeyFun(Elt), + GroupList = maps:get(Key, Acc, []), + maps:put(Key, [Elt | GroupList], Acc) + end, + #{}, + List + ). + +%% @doc Like lists:foldl, but can be stopped amid the traversal of a list. +%% Function must return {cont, NewAcc} to continue folding the list, or {halt, FinalAcc} to stop immediately. +-spec foldl_while(fun((T, Acc) -> {cont, Acc} | {halt, Acc}), Acc, list(T)) -> Acc when T :: term(), Acc :: term(). +foldl_while(Fun, Acc, List) when is_function(Fun, 2), is_list(List) -> + do_foldl_while(Fun, Acc, List). + +do_foldl_while(_Fun, Acc, []) -> + Acc; +do_foldl_while(Fun, Acc, [Elem | Rest]) -> + case Fun(Elem, Acc) of + {cont, NextAcc} -> + do_foldl_while(Fun, NextAcc, Rest); + {halt, FinalAcc} -> + FinalAcc + end. + +-spec orderless_equal(list(), list()) -> boolean(). +orderless_equal(List1, List2) -> + (List1 -- List2) =:= (List2 -- List1). diff --git a/src/genlib_map.erl b/src/genlib_map.erl index 4a5dfa7..1626945 100644 --- a/src/genlib_map.erl +++ b/src/genlib_map.erl @@ -17,6 +17,7 @@ -export([binarize/1]). -export([binarize/2]). -export([diff/2]). +-export([fold_while/3]). %% @@ -142,3 +143,21 @@ diff(Map, Since) -> Map, Since ). + +%% @doc Like maps:fold, but can be stopped amid the traversal of a map. +%% Function must return {cont, NewAcc} to continue folding the list, or {halt, FinalAcc} to stop immediately. +-spec fold_while(fun((K, V, Acc) -> {cont, Acc} | {halt, Acc}), Acc, #{K => V}) -> Acc when + K :: term(), V :: term(), Acc :: term(). +fold_while(Fun, Acc, Map) when is_function(Fun, 3) and is_map(Map) -> + do_fold_while(Fun, Acc, maps:iterator(Map)). + +do_fold_while(Fun, Acc, Iter) -> + case maps:next(Iter) of + none -> + Acc; + {K, V, NextIter} -> + case Fun(K, V, Acc) of + {halt, FinalAcc} -> FinalAcc; + {cont, NextAcc} -> do_fold_while(Fun, NextAcc, NextIter) + end + end. diff --git a/test/genlib_list_tests.erl b/test/genlib_list_tests.erl new file mode 100644 index 0000000..16695d4 --- /dev/null +++ b/test/genlib_list_tests.erl @@ -0,0 +1,10 @@ +-module(genlib_list_tests). + +-include_lib("eunit/include/eunit.hrl"). + +-spec test() -> _. + +-spec wrap_empty_list_for_undefined_test() -> _. + +wrap_empty_list_for_undefined_test() -> + ?assertEqual(genlib_list:wrap(undefined), []). diff --git a/test/genlib_map_tests.erl b/test/genlib_map_tests.erl index ab779a6..886f191 100644 --- a/test/genlib_map_tests.erl +++ b/test/genlib_map_tests.erl @@ -48,3 +48,13 @@ diff_test_() -> ?_assertEqual(#{this_is => 'undefined'}, genlib_map:diff(#{this_is => 'undefined'}, #{this_is => 'not'})), ?_assertEqual(#{this_is => 'not'}, genlib_map:diff(#{this_is => 'not'}, #{this_is => 'undefined'})) ]. + +-spec fold_while_does_nothing_for_empty_map_test() -> _. +fold_while_does_nothing_for_empty_map_test() -> + genlib_map:fold_while( + fun(_, _, _) -> + throw(blow_up) + end, + true, + #{} + ). diff --git a/test/prop_genlib_list.erl b/test/prop_genlib_list.erl new file mode 100644 index 0000000..d954b38 --- /dev/null +++ b/test/prop_genlib_list.erl @@ -0,0 +1,85 @@ +-module(prop_genlib_list). + +-include_lib("proper/include/proper.hrl"). + +-spec prop_wrap() -> proper:test(). +prop_wrap() -> + ?FORALL( + Term, + term(), + begin + Result = genlib_list:wrap(Term), + %% List must stay the same, anything else (except `undefined`) ­ + (is_list(Term) and (Result == Term)) or is_list(Result) + end + ). + +-spec prop_foldl_while() -> proper:test(). +prop_foldl_while() -> + ?FORALL( + [Int1, Int2], + [pos_integer(), pos_integer()], + begin + SeqLength = max(Int1, Int2), + Limit = min(Int1, Int2), + + Seq = lists:seq(1, SeqLength), + SimpleResult = length(lists:filter(fun(E) -> E =< Limit end, Seq)), + FoldlWhileResult = + genlib_list:foldl_while( + fun(E, Acc) -> + NewAcc = Acc + 1, + case E < Limit of + true -> {cont, NewAcc}; + false -> {halt, NewAcc} + end + end, + 0, + Seq + ), + SimpleResult =:= FoldlWhileResult + end + ). + +-spec prop_orderless_equal() -> proper:test(). +prop_orderless_equal() -> + ?FORALL( + [List1, List2], + [list(), list()], + genlib_list:orderless_equal(List1, List2) == (lists:sort(List1) == lists:sort(List2)) + ). + +-spec prop_group_by_has_same_values() -> proper:test(). +prop_group_by_has_same_values() -> + ?FORALL( + List, + list(), + begin + Result = genlib_list:group_by(fun(X) -> X end, List), + AllValues = lists:flatmap(fun(X) -> X end, maps:values(Result)), + + lists:sort(List) =:= lists:sort(AllValues) + end + ). + +-spec prop_group_by_groups_correctly() -> proper:test(). +prop_group_by_groups_correctly() -> + ?FORALL( + List, + list(), + begin + Result = genlib_list:group_by(fun(X) -> X end, List), + + maps:fold( + fun + (_, _, false) -> + false; + (Key, Values, true) -> + Filtered = lists:filter(fun(K) -> K =:= Key end, List), + genlib_list:orderless_equal(Values, Filtered) + end, + true, + Result + ) + end + ). diff --git a/test/prop_genlib_map.erl b/test/prop_genlib_map.erl new file mode 100644 index 0000000..6929444 --- /dev/null +++ b/test/prop_genlib_map.erl @@ -0,0 +1,26 @@ +-module(prop_genlib_map). + +-include_lib("proper/include/proper.hrl"). + +-spec prop_fold_while() -> proper:test(). +prop_fold_while() -> + ?FORALL( + Map, + ?LET(KVList, non_empty(list({term(), term()})), maps:from_list(KVList)), + begin + RandomId = rand:uniform(map_size(Map)), + {RandomKey, RandomValue} = lists:nth(RandomId, maps:to_list(Map)), + + Result = + genlib_map:fold_while( + fun + (K, V, _Acc) when K =:= RandomKey -> {halt, V}; + (_K, _V, Acc) -> {cont, Acc} + end, + make_ref(), + Map + ), + + Result =:= RandomValue + end + ).