Current section

Files

Jump to
ra src ra_lol.erl
Raw

src/ra_lol.erl

-module(ra_lol).
%% sorted list of list
-export([
new/0,
append/2,
search/2,
takewhile/2,
from_list/1,
from_list/2,
to_list/1,
len/1
]).
-define(MAX_ROW_LEN, 64).
-type row() :: [term()].
-type gt_fun() :: fun((Item, Item) -> boolean()).
-record(?MODULE, {len = 0 :: non_neg_integer(),
append_row_len = 0 :: non_neg_integer(),
gt_fun :: gt_fun(),
rows = [] :: [row()]}).
-opaque state() :: #?MODULE{}.
%% a search continuation
-opaque cont() :: [row()].
-export_type([state/0,
cont/0]).
-spec new() -> state().
new() ->
#?MODULE{gt_fun = fun erlang:'>'/2}.
-spec new(gt_fun()) -> state().
new(GtFun) ->
#?MODULE{gt_fun = GtFun}.
%% @doc append an item that is greater than the last appended item
-spec append(Item, state()) ->
state() | out_of_order
when Item :: term().
append(Item, #?MODULE{rows = []} = State) ->
State#?MODULE{rows = [[Item]],
len = 1,
append_row_len = 0};
append(Item,
#?MODULE{len = Len,
gt_fun = GtFun,
append_row_len = RowLen,
rows = [[LastItem | _] = Row | Rows]} = State) ->
case GtFun(Item, LastItem) of
true ->
case RowLen of
?MAX_ROW_LEN ->
%% time for a new row
State#?MODULE{rows = [[Item], Row | Rows],
len = Len + 1,
append_row_len = 1};
_ ->
State#?MODULE{rows = [[Item | Row] | Rows],
len = Len + 1,
append_row_len = RowLen + 1}
end;
false ->
out_of_order
end.
-spec search(fun((term()) -> higher | lower | equal),
state() | cont()) ->
{term(), cont()} | undefined.
search(SearchFun, #?MODULE{rows = Rows}) ->
search(SearchFun, Rows);
search(SearchFun, Rows) when is_list(Rows) ->
case find_row(SearchFun, Rows) of
[] ->
undefined;
[SearchRow | RemRows] ->
case search_row(SearchFun, SearchRow) of
undefined ->
undefined;
{Item, Rem} ->
{Item, [Rem | RemRows]}
end
end.
-spec takewhile(fun((Item) -> boolean()), state()) ->
{[Item], state()}
when Item :: term().
takewhile(Fun, #?MODULE{gt_fun = GtFun} = State) ->
%% not the most efficient but rarely used
{Taken, Left} = lists:splitwith(Fun, to_list(State)),
{Taken, from_list(GtFun, lists:reverse(Left))}.
%% @doc initialise from a list sorted in ascending order
-spec from_list(list()) -> state().
from_list(List) ->
from_list(fun erlang:'>'/2, List).
-spec from_list(gt_fun(), list()) -> state().
from_list(GtFun, List)
when is_list(List) ->
lists:foldl(fun append/2, new(GtFun), List).
-spec to_list(state()) -> list().
to_list(#?MODULE{rows = Rows}) ->
lists:append(Rows).
-spec len(state()) -> non_neg_integer().
len(#?MODULE{len = Len}) ->
Len.
%% Internals
search_row(_SearchFun, []) ->
undefined;
search_row(SearchFun, [Item | Rem]) ->
case SearchFun(Item) of
equal ->
{Item, Rem};
lower ->
search_row(SearchFun, Rem);
higher ->
undefined
end.
find_row(SearchFun, [_Row, Row | Rem] = Rows) ->
%% if last item of the second rows is higher than searching for
%% then return all rows
case SearchFun(hd(Row)) of
higher ->
Rows;
_ ->
%% else keep searching
find_row(SearchFun, [Row | Rem])
end;
find_row(_SearchFun, Rows) ->
Rows.
%%% ===================
%%% Internal unit tests
%%% ===================
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
basic_test() ->
Items = lists:seq(1, 100),
L0 = ra_lol:from_list(Items),
?assertEqual(100, ra_lol:len(L0)),
?assertEqual(Items, lists:reverse(ra_lol:to_list(L0))),
?assertMatch(out_of_order, ra_lol:append(1, L0)),
L1 = ra_lol:append(101, L0),
?assertEqual(101, ra_lol:len(L1)),
SearchFun = fun (T) ->
fun (Item) ->
if T == Item -> equal;
T > Item -> higher;
true -> lower
end
end
end,
[begin
{T, _} = ra_lol:search(SearchFun(T), L1)
end || T <- Items ++ [101]],
%% test searching with a continuation
_ = lists:foldl(fun (T, Acc) ->
{T, Cont} = ra_lol:search(SearchFun(T), Acc),
Cont
end, L1, lists:reverse(Items ++ [101])),
TakeFun = fun(Item) -> Item > 50 end,
{Taken, L2} = takewhile(TakeFun, L1),
?assertEqual(50, ra_lol:len(L2)),
?assertEqual(51, length(Taken)),
?assertMatch(out_of_order, ra_lol:append(50, L2)),
L3 = ra_lol:append(51, L2),
?assertEqual(51, ra_lol:len(L3)),
ok.
-endif.