%% @doc An optionally fixed-sized ordered set of complex terms, including %% Erlang records. %% %%

Rationale

%% %% A recordset provides 3 unique properties not found with %% ordsets. %% %%
    %%
  1. User defined identity of elements.

    %% %% 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. %% %%

    Example

    %% %% This 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.
    %%   
    %% %%

  2. %% %%
  3. User defined sorting of elements.

    %% %% 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. %% %%

    Example

    %% %% This 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.
    %%   
    %% %%

  4. %%
  5. Optionally fixed size.

    %% %% 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. %% %%

  6. %%
%% -module(recordset). -author('David Reid '). -copyright('2011 Mochi Media, Inc.'). -export_type([recordset/0]). -record(recordset, { max_size :: undefined | pos_integer(), identity_function :: cmp_fun(), sort_function :: cmp_fun(), set = [] :: list() }). -opaque recordset() :: #recordset{}. -type cmp_fun() :: {fun((term(), term()) -> boolean())}. -type option() :: {atom(), term()}. -type op() :: statebox:op(). -export([new/2, new/3]). -export([from_list/2, from_list/4, to_list/1]). -export([is_recordset/1, size/1, max_size/1]). -export([add/2, delete/2]). -export([statebox_add/1, statebox_delete/1]). %% @equiv new(IdentityFun, SortFun, []) -spec new(cmp_fun(), cmp_fun()) -> recordset(). new(IdentityFun, SortFun) -> new(IdentityFun, SortFun, []). %% @doc Create an empty recordset. %% Options: %%
%%
max_size
%%
Specifies the maximum number of elements that will exist in the set. %%
%%
-spec new(cmp_fun(), cmp_fun(), [option()]) -> recordset(). new(IdentityFun, SortFun, Options) -> #recordset{max_size=proplists:get_value(max_size, Options), identity_function=IdentityFun, sort_function=SortFun}. %% @doc Return true 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]}.