src/partisan_plumtree_util.erl

%% -------------------------------------------------------------------
%%
%% Copyright (c) 2014 Basho Technologies, Inc.  All Rights Reserved.
%%
%% This file is provided to you under the Apache License,
%% Version 2.0 (the "License"); you may not use this file
%% except in compliance with the License.  You may obtain
%% a copy of the License at
%%
%%   http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing,
%% software distributed under the License is distributed on an
%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
%% KIND, either express or implied.  See the License for the
%% specific language governing permissions and limitations
%% under the License.
%%
%% -------------------------------------------------------------------

-module(partisan_plumtree_util).


-include("partisan_logger.hrl").

-export([build_tree/3]).
-export([log/2]).
-export([log/3]).


%% -----------------------------------------------------------------------------
%% @doc Convert a list of elements into an N-ary tree. This conversion
%%      works by treating the list as an array-based tree where, for
%%      example in a binary 2-ary tree, a node at index i has children
%%      2i and 2i+1. The conversion also supports a "cycles" mode where
%%      the array is logically wrapped around to ensure leaf nodes also
%%      have children by giving them backedges to other elements.
%% @end
%% -----------------------------------------------------------------------------
-spec build_tree(N :: integer(), Nodes :: [term()], Opts :: [term()]) ->
    orddict:orddict().

build_tree(N, Nodes, Opts) ->
    Expand =
        case lists:member(cycles, Opts) of
            true ->
                lists:flatten(lists:duplicate(N + 1, Nodes));
            false ->
                Nodes
        end,
    {Tree, _} =
        lists:foldl(fun(Elm, {Result, Worklist}) ->
                            Len = erlang:min(N, length(Worklist)),
                            {Children, Rest} = lists:split(Len, Worklist),
                            NewResult = [{Elm, Children} | Result],
                            {NewResult, Rest}
                    end, {[], tl(Expand)}, Nodes),
    orddict:from_list(Tree).


%% -----------------------------------------------------------------------------
%% @doc
%% @end
%% -----------------------------------------------------------------------------
-spec log(debug | info | error, String :: string(), Args :: list(term())) -> ok.

-ifdef(TEST).

log(Level, String) ->
    log(Level, String, []).


log(debug, String, Args) ->
    ?LOG_DEBUG(String, Args);

log(info, String, Args) ->
    ?LOG_INFO(String, Args);

log(error, String, Args) ->
    ?LOG_ERROR(String, Args).

-else.

log(_Level, _String) -> ok.
log(_Level, _String, _Args) -> ok.

-endif.




%% =============================================================================
%% TESTS
%% =============================================================================



-ifdef(TEST).

-include_lib("eunit/include/eunit.hrl").

arity_test() ->
    %% 1-ary tree
    ?assertEqual([{node1, []}], orddict:to_list(build_tree(1, [node1], []))),
    ?assertEqual([{node1, [node2]},
                  {node2, []}], orddict:to_list(build_tree(1, [node1, node2], []))),
    ?assertEqual([{node1, [node2]},
                  {node2, [node3]},
                  {node3, []}], orddict:to_list(build_tree(1, [node1, node2, node3], []))),
    ?assertEqual([{node1, [node2]},
                  {node2, [node3]},
                  {node3, [node4]},
                  {node4, []}], orddict:to_list(build_tree(1, [node1, node2,
                                                               node3, node4], []))),

    %% 2-ary tree
    ?assertEqual([{node1, []}], orddict:to_list(build_tree(2, [node1], []))),
    ?assertEqual([{node1, [node2]},
                  {node2, []}], orddict:to_list(build_tree(2, [node1, node2], []))),
    ?assertEqual([{node1, [node2, node3]},
                  {node2, []},
                  {node3, []}], orddict:to_list(build_tree(2, [node1, node2, node3], []))),
    ?assertEqual([{node1, [node2, node3]},
                  {node2, [node4]},
                  {node3, []},
                  {node4, []}], orddict:to_list(build_tree(2, [node1, node2,
                                                               node3, node4], []))),
    ?assertEqual([{node1, [node2, node3]},
                  {node2, [node4, node5]},
                  {node3, []},
                  {node4, []},
                  {node5, []}], orddict:to_list(build_tree(2, [node1, node2,
                                                               node3, node4,
                                                               node5], []))),
    ?assertEqual([{node1, [node2, node3]},
                  {node2, [node4, node5]},
                  {node3, [node6]},
                  {node4, []},
                  {node5, []},
                  {node6, []}], orddict:to_list(build_tree(2, [node1, node2,
                                                               node3, node4,
                                                               node5, node6], []))),

    %% 3-ary tree
    ?assertEqual([{node1, []}], orddict:to_list(build_tree(3, [node1], []))),
    ?assertEqual([{node1, [node2]},
                  {node2, []}], orddict:to_list(build_tree(3, [node1, node2], []))),
    ?assertEqual([{node1, [node2, node3]},
                  {node2, []},
                  {node3, []}], orddict:to_list(build_tree(3, [node1, node2, node3], []))),
    ?assertEqual([{node1, [node2, node3, node4]},
                  {node2, []},
                  {node3, []},
                  {node4, []}], orddict:to_list(build_tree(3, [node1, node2,
                                                               node3, node4], []))),
    ?assertEqual([{node1, [node2, node3, node4]},
                  {node2, [node5]},
                  {node3, []},
                  {node4, []},
                  {node5, []}], orddict:to_list(build_tree(3, [node1, node2,
                                                               node3, node4,
                                                               node5], []))),
    ?assertEqual([{node1, [node2, node3, node4]},
                  {node2, [node5, node6]},
                  {node3, []},
                  {node4, []},
                  {node5, []},
                  {node6, []}], orddict:to_list(build_tree(3, [node1, node2,
                                                               node3, node4,
                                                               node5, node6], []))),
    ?assertEqual([{node1, [node2, node3, node4]},
                  {node2, [node5, node6, node7]},
                  {node3, []},
                  {node4, []},
                  {node5, []},
                  {node6, []},
                  {node7, []}], orddict:to_list(build_tree(3, [node1, node2,
                                                               node3, node4,
                                                               node5, node6,
                                                               node7], []))),
    ?assertEqual([{node1, [node2, node3, node4]},
                  {node2, [node5, node6, node7]},
                  {node3, [node8]},
                  {node4, []},
                  {node5, []},
                  {node6, []},
                  {node7, []},
                  {node8, []}], orddict:to_list(build_tree(3, [node1, node2,
                                                               node3, node4,
                                                               node5, node6,
                                                               node7, node8], []))).

cycles_test() ->
    %% 1-ary tree
    ?assertEqual([{node1, [node1]}], orddict:to_list(build_tree(1, [node1], [cycles]))),
    ?assertEqual([{node1, [node2]},
                  {node2, [node1]}], orddict:to_list(build_tree(1, [node1, node2], [cycles]))),
    ?assertEqual([{node1, [node2]},
                  {node2, [node3]},
                  {node3, [node1]}], orddict:to_list(build_tree(1, [node1, node2, node3], [cycles]))),
    ?assertEqual([{node1, [node2]},
                  {node2, [node3]},
                  {node3, [node4]},
                  {node4, [node1]}], orddict:to_list(build_tree(1, [node1, node2,
                                                                   node3, node4], [cycles]))),

    %% 2-ary tree
    ?assertEqual([{node1, [node1, node1]}], orddict:to_list(build_tree(2, [node1], [cycles]))),
    ?assertEqual([{node1, [node2, node1]},
                  {node2, [node2, node1]}], orddict:to_list(build_tree(2, [node1, node2], [cycles]))),
    ?assertEqual([{node1, [node2, node3]},
                  {node2, [node1, node2]},
                  {node3, [node3, node1]}], orddict:to_list(build_tree(2, [node1, node2, node3], [cycles]))),
    ?assertEqual([{node1, [node2, node3]},
                  {node2, [node4, node1]},
                  {node3, [node2, node3]},
                  {node4, [node4, node1]}], orddict:to_list(build_tree(2, [node1, node2,
                                                                           node3, node4], [cycles]))),
    ?assertEqual([{node1, [node2, node3]},
                  {node2, [node4, node5]},
                  {node3, [node1, node2]},
                  {node4, [node3, node4]},
                  {node5, [node5, node1]}], orddict:to_list(build_tree(2, [node1, node2,
                                                                           node3, node4,
                                                                           node5], [cycles]))),
    ?assertEqual([{node1, [node2, node3]},
                  {node2, [node4, node5]},
                  {node3, [node6, node1]},
                  {node4, [node2, node3]},
                  {node5, [node4, node5]},
                  {node6, [node6, node1]}], orddict:to_list(build_tree(2, [node1, node2,
                                                               node3, node4,
                                                               node5, node6], [cycles]))),

    %% 3-ary tree
    ?assertEqual([{node1, [node1, node1, node1]}], orddict:to_list(build_tree(3, [node1], [cycles]))),
    ?assertEqual([{node1, [node2, node1, node2]},
                  {node2, [node1, node2, node1]}], orddict:to_list(build_tree(3, [node1, node2], [cycles]))),
    ?assertEqual([{node1, [node2, node3, node1]},
                  {node2, [node2, node3, node1]},
                  {node3, [node2, node3, node1]}], orddict:to_list(build_tree(3, [node1, node2, node3], [cycles]))),
    ?assertEqual([{node1, [node2, node3, node4]},
                  {node2, [node1, node2, node3]},
                  {node3, [node4, node1, node2]},
                  {node4, [node3, node4, node1]}], orddict:to_list(build_tree(3, [node1, node2,
                                                                                  node3, node4], [cycles]))),
    ?assertEqual([{node1, [node2, node3, node4]},
                  {node2, [node5, node1, node2]},
                  {node3, [node3, node4, node5]},
                  {node4, [node1, node2, node3]},
                  {node5, [node4, node5, node1]}], orddict:to_list(build_tree(3, [node1, node2,
                                                                                  node3, node4,
                                                                                  node5], [cycles]))),
    ?assertEqual([{node1, [node2, node3, node4]},
                  {node2, [node5, node6, node1]},
                  {node3, [node2, node3, node4]},
                  {node4, [node5, node6, node1]},
                  {node5, [node2, node3, node4]},
                  {node6, [node5, node6, node1]}], orddict:to_list(build_tree(3, [node1, node2,
                                                                                  node3, node4,
                                                                                  node5, node6], [cycles]))).
-endif.