%% @doc An optionally fixed-sized ordered set of complex terms, including %% Erlang records. %% %%
recordset provides 3 unique properties not found with
%% ordsets.
%%
%%
%%
%% One of the most useful features of a set is that each item only exists in
%% the set once. However both sets and ordsets
%% require that the entire term match (=:=) or compare equal
%% (==) respectively. This requirement can be too strict for
%% certain types of data (such as a set of 10 high scores with only a single
%% score per user.) recordset allows you to define a 2-arity
%% function which will be used to compare the identity of two elements in
%% the set.
%%
%%
IdentityFun compares 2 #score{} records
%% and determines that they are the same if and only if they were created by
%% the same player. If two elements are the same, only one of them is
%% allowed to exist in the list.
%%
%%
%% fun(#score{player=A}, #score{player=B}) ->
%% A =:= B
%% end.
%%
%%
%%
%%
%% The most useful feature of ordsets is that they are indeed
%% ordered. However as with identity of elements, the sorting of elements
%% is based on the entirety of the terms in the set. recordset
%% allows you to define another 2-arity function which will be used to sort
%% the elements.
%%
%%
SortFun will cause #score{} records
%% to be sorted by score in ascending order. So that when converted to a
%% list the lowest scores will be at the front of the list.
%%
%%
%% fun(#score{score=A}, #score{score=B}) ->
%% A < B
%% end.
%%
%%
%%
%%
%% sets, ordsets, and by default
%% recordset will also grow in size as items are added to the
%% set. However it may be desirable to store a fixed number of elements
%% (such as 50 or 100 highest scores.) In which case recordset
%% can be given a max_size, and once more than
%% max_size items are added to the set items which sort lowest
%% based on the supplied SortFun will be removed.
%%
%%
recordset.
%% Options:
%% max_sizetrue if the argument is a recordset,
%% false otherwise.
-spec is_recordset(any()) -> boolean().
is_recordset(#recordset{}) ->
true;
is_recordset(_) ->
false.
%% @equiv from_list(List, recordset:new(IdentityFun, SortFun, Options))
-spec from_list([term()], cmp_fun(), cmp_fun(), [option()]) -> recordset().
from_list(List, IdentityFun, SortFun, Options) ->
from_list(List, recordset:new(IdentityFun, SortFun, Options)).
%% @doc Populate the specified RecordSet with the given
%% List of elements.
-spec from_list([term()], recordset()) -> recordset().
from_list(List, RecordSet) ->
lists:foldl(fun(Term, RS) ->
recordset:add(Term, RS)
end,
RecordSet,
List).
%% @doc Return the elements in the recordset as an ordered list
%% of elements.
-spec to_list(recordset()) -> list().
to_list(#recordset{set=Set}) ->
Set.
%% @doc Return the current size of the given recordset as an
%% integer.
-spec size(recordset()) -> integer().
size(#recordset{set=Set}) ->
length(Set).
%% @doc Return the max size as an integer or undefined if this
%% recordset is not of a fixed size.
-spec max_size(recordset()) -> undefined | integer().
max_size(#recordset{max_size=MaxSize}) ->
MaxSize.
%% @doc Add Term to the recordset.
%%
%% If the recordset is fixed-sized and Term is the
%% smallest element when SortFun and max_size has
%% been exceeded, then Term will not be added to the set. And if
%% Term is not the smallest element in the set, the new smallest
%% element will be removed.
%%
%% If the recordset is not fixed-sized then Term
%% will be added to the set as long as IdentityFun does not
%% determine that is the same as an existing element and SortFun
%% does not determine that it is smaller than the existing element with the
%% same identity.
-spec add(term(), recordset()) -> recordset().
add(Term, RecordSet = #recordset{set=[]}) ->
RecordSet#recordset{set=[Term]};
add(Term, RecordSet = #recordset{
max_size=MaxSize,
identity_function=IdentityFun,
sort_function=SortFun,
set=Set}) ->
Set1 = case add_1(Term, IdentityFun, SortFun, Set) of
Set0 when is_integer(MaxSize),
length(Set0) > MaxSize ->
truncate(Set0, length(Set0) - MaxSize);
Set0 ->
Set0
end,
RecordSet#recordset{set=Set1}.
add_1(Term, IdentityFun, SortFun, [H | Set] = FullSet) ->
case SortFun(Term, H) of
true ->
case IdentityFun(Term, H) of
true ->
FullSet;
false ->
[Term | FullSet]
end;
false ->
case IdentityFun(Term, H) of
true ->
add_1(Term, IdentityFun, SortFun, Set);
false ->
[H | add_1(Term, IdentityFun, SortFun, Set)]
end
end;
add_1(Term, _IdentityFun, _SortFun, []) ->
[Term].
truncate(S, 0) ->
S;
truncate([_H | Set], I) ->
truncate(Set, I-1).
%% @doc Remove an element from the recordset.
-spec delete(term(), recordset()) -> recordset().
delete(_Term, RecordSet = #recordset{set=[]}) ->
RecordSet;
delete(Term, RecordSet = #recordset{
identity_function=IdentityFun,
set=Set}) ->
RecordSet#recordset{set=delete_1(Term, IdentityFun, Set)}.
delete_1(Term, IdentityFun, [H | Set]) ->
case IdentityFun(Term, H) of
true ->
Set;
false ->
[H | delete_1(Term, IdentityFun, Set)]
end.
%% @doc Return a statebox:op() which will add the given
%% Term to a recordset.
-spec statebox_add(term()) -> op().
statebox_add(Term) ->
{fun ?MODULE:add/2, [Term]}.
%% @doc Return a statebox:op() which will delete the given
%% Term from a recordset.
-spec statebox_delete(term()) -> op().
statebox_delete(Term) ->
{fun ?MODULE:delete/2, [Term]}.