% Copyright (C) 2007-2019 Olivier Boudeville
%
% This file is part of the Ceylan-Myriad library.
%
% This library is free software: you can redistribute it and/or modify
% it under the terms of the GNU Lesser General Public License or
% the GNU General Public License, as they are published by the Free Software
% Foundation, either version 3 of these Licenses, or (at your option)
% any later version.
% You can also redistribute it and/or modify it under the terms of the
% Mozilla Public License, version 1.1 or later.
%
% This library is distributed in the hope that it will be useful,
% but WITHOUT ANY WARRANTY; without even the implied warranty of
% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
% GNU Lesser General Public License and the GNU General Public License
% for more details.
%
% You should have received a copy of the GNU Lesser General Public
% License, of the GNU General Public License and of the Mozilla Public License
% along with this library.
% If not, see and
% .
%
% Author: Olivier Boudeville [olivier (dot) boudeville (at) esperide (dot) com]
% Creation date: Sunday, December 24, 2017.
% Management of various identifiers.
%
% See id_utils_test.erl for the corresponding test.
%
-module(id_utils).
% For the table macro, knowing the current module is bootstrapped:
-include("meta_utils.hrl").
% UUID section.
% A string UUID (ex: "ed64ffd4-74ee-43dc-adba-be37ed8735aa"):
-type uuid() :: string().
-export_type([ uuid/0 ]).
-export([ generate_uuid/0 ]).
% Sortable identifier section.
% No legit sortable identifier can be smaller than that one:
-define( lower_bound_id, [ 0 ] ).
% No legit sortable identifier can be higher than that one (in Erlang term
% order, lists are dominated (only) by bitstrings):
%
-define( upper_bound_id, <<"Max">> ).
% Corresponds to smart (sortable, insertion-friendly) identifiers, typically
% represented (externally) as {1}, {2}, {2,1}, {4}, etc.
%
% Sometimes identifiers that can be sorted and that allow introducing any number
% of new identifiers *between* any two (successive or not) ones are useful; as a
% result, insertion is most probably the most frequent creation operation to be
% performed on sortable identifiers.
%
% We use non-empty lists of non-negative integers ("digits") for that, whose
% last integer element must be strictly positive (so that lower values can
% always be introduced).
%
% The Erlang default ordering for this datatype corresponds to this need.
%
% For example, if having defined two identifiers defined internally as [7,2] and
% [7,3], we can introduce two identifiers between them, typically [7,2,1] and
% [7,2,2], since the Erlang term ordering tells us that [7,2] < [7,2,1] <
% [7,2,2] < [7,3].
%
% As a result, no need to define specific comparison operators, '=:=', '<' and
% '>', and thus 'lists:sort/1', 'lists:keysort/2' are already adequate for that.
%
% Example: lists:sort( [ [7,3], [7,2,1], [7,2,2], [7,2] ] ) =
% [ [7,2], [7,2,1], [7,2,2], [7,3] ].
%
% The maximum lower bound is conventionally chosen to be [0] - which is not a
% valid sortable identifier (as it terminates with zero).
%
% Designed to be efficiently compared/ordered, at the cost of more expensive
% insertions.
%
% An additional goal is to generate identifiers that remain as short and
% human-readable as possible.
%
% Not accepted by the compiler:
% -type sortable_id() ::
% [ non_neg_integer() ] | ?lower_bound_id | ?upper_bound_id.
%
% To include bounds as well:
-type sortable_id() :: [ integer() ] | bitstring().
% Any type of element to which an identifier could be associated:
-type identifiable_element() :: any().
% A table associating, to an element, its sortable identifier.
-type identifier_table() :: ?table:?table( identifiable_element(),
sortable_id() ).
-export_type([ sortable_id/0, identifiable_element/0, identifier_table/0 ]).
-export([ get_initial_sortable_id/0, get_next_sortable_id/1,
get_sortable_id_between/2,
get_sortable_id_lower_bound/0, get_sortable_id_upper_bound/0,
check_sortable_id/1, get_successor_sortable_id/1,
get_higher_same_depth_sortable_id/1,
get_higher_next_depth_sortable_id/1,
assign_sorted_identifiers/2,
sortable_id_to_string/1, sortable_ids_to_string/1,
identifier_table_to_string/1 ]).
% UUID section.
% Returns a string containing a new universally unique identifier (UUID), based
% on a random source and/or the system clock plus the system's ethernet hardware
% address, if present.
%
-spec generate_uuid() -> uuid().
generate_uuid() ->
case executable_utils:lookup_executable( "uuidgen" ) of
false ->
trace_utils:warning( "No 'uuidgen' tool found on system, "
"defaulting to our failsafe implementation." ),
uuidgen_internal();
Exec ->
% Random-based, rather than time-based (otherwise we end up
% collecting a rather constant suffix):
%
case system_utils:run_executable( Exec ++ " -r" ) of
{ _ExitCode=0, Res } ->
Res;
{ ExitCode, ErrorOutput } ->
throw( { uuid_generation_failed, ExitCode, ErrorOutput } )
end
end.
% Quick and dirty replacement:
uuidgen_internal() ->
% Using /dev/random instead would incur waiting of a few seconds that were
% deemed too long for this use:
%
case system_utils:run_executable(
"/bin/dd if=/dev/urandom bs=1 count=32 2>/dev/null" ) of
{ _ReturnCode=0, Output } ->
% We translate these bytes into hexadecimal values:
V = [ string:to_lower( hd(
io_lib:format( "~.16B", [ B rem 16 ] ) ) ) || B <- Output ],
lists:flatten( io_lib:format(
"~s~s~s~s~s~s~s~s-~s~s~s~s-~s~s~s~s-~s~s~s~s-~s"
"~s~s~s~s~s~s~s~s~s~s~s", V ) );
{ ErrorCode, ErrorOutput } ->
throw( { uuidgen_internal_failed, ErrorCode, ErrorOutput } )
end.
% Sortable identifier section.
% Returns a relevant, well-chosen initial low sortable identifier, yet not the
% lowest possible so that we can still introduce, if needed, an arbitrary number
% of identifiers lower than this one.
%
-spec get_initial_sortable_id() -> sortable_id().
get_initial_sortable_id() ->
[ 1 ].
% Returns a relevant sortable identifier that is superior to the specified one.
-spec get_next_sortable_id( sortable_id() ) -> sortable_id().
get_next_sortable_id( Id ) ->
get_higher_same_depth_sortable_id( Id ).
% Returns a sortable identifier that can be inserted between the two specified
% ones, which are presumably ordered.
%
% Note: most probably the most useful function in order to create new sortable
% identifiers.
%
% Ex: get_sortable_id_between( [1,7,1], [1,7,2] ) may return [1,7,1,1].
%
-spec get_sortable_id_between( sortable_id(), sortable_id() ) -> sortable_id().
get_sortable_id_between( Id, Id ) ->
throw( { empty_id_range, Id } );
get_sortable_id_between( ?upper_bound_id, _Id ) ->
throw( cannot_exceed_upper_bound );
get_sortable_id_between( _Id, ?lower_bound_id ) ->
throw( cannot_go_below_lower_bound );
get_sortable_id_between( ?lower_bound_id, Id ) ->
% As lower bound, [0], can be managed through the main rule:
get_sortable_id_between( ?lower_bound_id, Id, _Acc=[] );
get_sortable_id_between( Id, ?upper_bound_id ) ->
get_higher_same_depth_sortable_id( Id );
get_sortable_id_between( Id, Id ) ->
throw( { equal_sortable_identifiers, Id } );
get_sortable_id_between( LowerId, HigherId ) when LowerId > HigherId ->
throw( { wrongly_ordered_sortable_identifiers, LowerId, HigherId } );
% Here LowerId < HigherId, and must be non-empty lists:
get_sortable_id_between( LowerId, HigherId ) ->
get_sortable_id_between( LowerId, HigherId, _Acc=[] ).
% Principle: we iterate through the listed digits (from left to right) until
% they differ.
%
% (helper)
%
% Here still in a common prefix:
get_sortable_id_between( _Lower=[ H | Tl ], _Higher=[ H | Th ], Acc ) ->
get_sortable_id_between( Tl, Th, [ H | Acc ] );
% In clauses below they differ for the first time:
% Here they are wrongly ordered:
get_sortable_id_between( Lower=[ Hl | _Tl ], Higher=[ Hh | _Th ], Acc )
when Hl > Hh ->
CommonPrefix = lists:reverse( Acc ),
LowerId = CommonPrefix ++ Lower,
HigherId = CommonPrefix ++ Higher,
throw( { wrongly_ordered_sortable_identifiers, LowerId, HigherId } );
% Here by design Hl < Hh, and in the next clause we are at the end of Lower:
get_sortable_id_between( _Lower=[ Hl ], _Higher, Acc ) ->
% By design higher than LowerId, lower than HigherId, and not the immediate
% next element to LowerId:
% (ex: if Lower = [1,4,5,8,2] and Higher = [1,4,5,8,3,...], returning:
% [1,4,5,8,2,1]
lists:reverse( [ 1, Hl | Acc ] );
% Here Hl < Hh, and we are not at the end of Lower:
get_sortable_id_between( _Lower=[ Hl, Hlnext | _Tl ], _Higher, Acc ) ->
% Ex: if Lower = [1,4,5,8,2,X,...] and Higher = [1,4,5,8,3,...], returning:
% [1,4,5,8,2,X+1] (which is at least as short as both)
lists:reverse( [ Hlnext+1, Hl | Acc ] );
% Here we are not at the end of Lower, and Higher finishes with 0, abnormal
get_sortable_id_between( _LowerId=[], _HigherId=[ 0 ], Acc ) ->
throw( { zero_ending_sortable_id, lists:reverse( [ 0 | Acc ] ) } );
% Here we are not at the end of Lower, and Higher has still digits:
get_sortable_id_between( _LowerId=[], _HigherId=[ 0 | T ], Acc ) ->
% We cannot introduce an identifier, waiting for the first non-zero digits
% (guaranteed to exist by design):
%
get_sortable_id_between( [], T, [ 0, Acc ] );
% H is non-zero below:
% H is 1 here, we introduce thus a 0, yet append a 1 to be non-zero terminated:
get_sortable_id_between( _LowerId=[], _HigherId=[ 1 | _T ], Acc ) ->
% Thus by design still higher than LowerId, and lower than HigherId, and at
% worse just one-digit longer than it:
%
lists:reverse( [ 1, 0 | Acc ] );
% H > 1 here, no need to go deeper:
get_sortable_id_between( _LowerId=[], _HigherId=[ H | _T ], Acc ) ->
lists:reverse( [ H-1 | Acc ] );
get_sortable_id_between( _LowerId=[], _HigherId=[], Acc ) ->
Id = lists:reverse( Acc ),
throw( { equal_sortable_identifiers, Id } ).
% Returns the maximum lower bound of sortable identifiers - knowing that this
% value does not pertain to sortable identifiers.
%
-spec get_sortable_id_lower_bound() -> sortable_id().
get_sortable_id_lower_bound() ->
?lower_bound_id.
% Returns the minimum upper bound of sortable identifiers - knowing that this
% value does not pertain to sortable identifiers.
%
-spec get_sortable_id_upper_bound() -> sortable_id().
get_sortable_id_upper_bound() ->
?upper_bound_id.
% Checks that the specified sortable identifier is legit.
-spec check_sortable_id( sortable_id() ) -> basic_utils:void().
check_sortable_id( _Id=[] ) ->
throw( { invalid_sortable_identifier, empty_list } );
check_sortable_id( Id ) when is_list( Id ) ->
check_only_non_neg_integers( Id, Id );
check_sortable_id( Id ) ->
throw( { non_list_sortable_identifier, Id } ).
% (helper)
check_only_non_neg_integers( [], _Id ) ->
ok;
check_only_non_neg_integers( [ H | T ], Id )
when is_integer( H ) andalso H >= 0 ->
check_only_non_neg_integers( T, Id );
check_only_non_neg_integers( _, Id ) ->
throw( { invalid_sortable_identifier, Id } ).
% Returns the immediate successor sortable identifier of the specified one,
% i.e. the one that is immediately superior to it.
%
% Note: generally *not* to be used, as by design no sortable identifier can be
% inserted between these two - which defeats the purpose of this datatype.
%
% Hence: mostly defined for reference purpose.
%
-spec get_successor_sortable_id( sortable_id() ) -> sortable_id().
get_successor_sortable_id( SortId ) ->
Reversed = lists:reverse( SortId ),
lists:reverse( [ 0 | Reversed ] ).
% Returns a sortable identifier higher than (i.e. coming after) the specified
% one, yet not finer (i.e. at the same depth).
%
% For example, if [1,4,2] is specified, then [1,4,3] is returned (rather than,
% say, [1,4,2,1]).
%
-spec get_higher_same_depth_sortable_id( sortable_id() ) -> sortable_id().
get_higher_same_depth_sortable_id( SortId ) ->
% Ex: we have SortId = [1,4,2]; we want to return [1,4,3] (rather
% than, say, [1,4,2,1]):
% Ex: Last is 2, RevOthers is [4,1]
[ Last | RevOthers ] = lists:reverse( SortId ),
lists:reverse( [ Last+1 | RevOthers ] ).
% Returns a sortable identifier higher than (i.e. coming after) the specified
% one, at a next depth.
%
% For example, if [1,4,2] is specified, then [1,4,2,1] is returned (rather than,
% say, [1,4,3]).
%
-spec get_higher_next_depth_sortable_id( sortable_id() ) -> sortable_id().
get_higher_next_depth_sortable_id( SortId ) ->
% Ex: we have SortId = [1,4,2]; we want to return [1,4,2,1] (rather
% than, say, [1,4,3]):
Reversed = lists:reverse( SortId ),
% We add 1 as finer coordinate, not 0, so that we can always perform an
% insertion *before* a sortable identifier that we produce that way.
%
% Ex: otherwise, no identifier could be inserted between [0] and [0,0]; thus
% the "basic next identifier" of [0] is [0,1] instead, and we can still
% insert for example [0,0,1] between them.
%
lists:reverse( [ 1 | Reversed ] ).
% Assigns sorted identifiers to the specified elements not being already
% identified (in the specified table, supposedly having its initial elements
% appropriately sorted), so that the order of these elements is respected by
% their identifiers in the returned table.
%
% Ex: if ElementsToIdentify=[ 'a', 'b', 'c', 'd' ] and, in IdentifierTable, 'a'
% is associated to La and 'd' to Ld, supposing La < Ld, whereas 'b' and 'c' are
% not already associated, then the returned table will also associate some Lc to
% 'c' and some Ld to 'd' so that La < Lb < Lc < Ld.
%
% Throws an exception if no correct mapping could be devised.
%
% Note: this is certainly not a trivial algorithm, as sortable identifiers
% should be generated only between two already existing ones (not "just after" a
% given one, for example).
%
% Important note: this function finally was not used and the current test (in
% id_utils_test.erl) shows it still has at least one bug. Beware!
%
-spec assign_sorted_identifiers( [ identifiable_element() ],
identifier_table() ) -> identifier_table().
assign_sorted_identifiers( _ElementsToIdentify=[], IdentifierTable ) ->
IdentifierTable;
% Using the first element in order to establish a correct lower bound:
assign_sorted_identifiers( _ElementsToIdentify=[ E | T ], IdentifierTable ) ->
% Establishing first a relevant (lowest) identifier for the first element:
{ FirstId, NewTable } = case ?table:lookup_entry( E, IdentifierTable ) of
key_not_found ->
% Not identified yet, we have to assign it a newly forged
% identifier, yet we have to ensure that it is by design lower than
% all others (if any) in the table; we just have to know the lowest
% of them then:
%
% (a precomputed stack should be used to search for lowest IDs only
% once)
%
NewId = case find_lowest_identifier_in( T, IdentifierTable ) of
undefined ->
% None found, hence the default lower bound will do:
ResId = get_initial_sortable_id(),
trace_utils:debug_fmt( "- managing element ~p, "
"not having already an identifier, with no next "
"identifier found, hence identified as ~s",
[ E, sortable_id_to_string( ResId ) ] ),
ResId;
LowestId ->
% Then the new identifier shall be even lower:
ResId = get_sortable_id_between(
get_sortable_id_lower_bound(), LowestId ),
trace_utils:debug_fmt( "- managing element ~p, "
"not having already an identifier, with next "
"identifier found as ~s, hence identified as ~s",
[ E, sortable_id_to_string( LowestId ),
sortable_id_to_string( ResId ) ] ),
ResId
end,
NewIdTable = ?table:add_new_entry( E, NewId, IdentifierTable ),
{ NewId, NewIdTable };
{ value, FoundId } ->
trace_utils:debug_fmt( "- managing element ~p, already having "
"an identifier, ~s",
[ E, sortable_id_to_string( FoundId ) ] ),
{ FoundId, IdentifierTable }
end,
assign_ranged_identifiers( T, _ToIdentifyRev=[], FirstId, NewTable ).
% Returns the lowest identifier associated to the specified elements (whose
% order does not matter).
%
% (helper)
%
-spec find_lowest_identifier_in( [ identifiable_element() ],
identifier_table() ) -> basic_utils:maybe( sortable_id() ).
find_lowest_identifier_in( Elements, IdentifierTable ) ->
LowestId = find_lowest_identifier_in( Elements, IdentifierTable,
_LowestId=undefined ),
trace_utils:debug_fmt( "- lowest identifier found in ~s is: ~s",
[ identifier_table_to_string( IdentifierTable ),
sortable_id_to_string( LowestId ) ] ),
LowestId.
find_lowest_identifier_in( _Elements=[], _IdentifierTable, LowestId ) ->
% Possibly 'undefined':
LowestId;
find_lowest_identifier_in( _Elements=[ E | T ], IdentifierTable, LowestId ) ->
case ?table:lookup_entry( E, IdentifierTable ) of
key_not_found ->
find_lowest_identifier_in( T, IdentifierTable, LowestId );
{ value, Id } ->
NewLowestId = case LowestId of
undefined ->
Id;
_ ->
erlang:min( LowestId, Id )
end,
find_lowest_identifier_in( T, IdentifierTable, NewLowestId )
end.
% Accumulates non-identified elements until, in addition to the specified lower
% bound, and upper bound is found or no element remains; then assigns ordered
% identifiers to all these elements.
%
% (helper)
%
-spec assign_ranged_identifiers( [ identifiable_element() ],
[ identifiable_element() ], sortable_id(), identifier_table() ) ->
identifier_table().
assign_ranged_identifiers( _RemainingElems=[], _ToIdentifyRev=[], _LowerId,
IdentifierTable ) ->
% Last element was identified, nothing pending, already ready:
IdentifierTable;
assign_ranged_identifiers( _RemainingElems=[], ToIdentifyRev, LowerId,
IdentifierTable ) ->
% Here we exhausted the elements, whereas we have still elements to identify
% and no known upper bound, so we rely on the absolute upper bound:
%
MaxId = get_sortable_id_upper_bound(),
ToIdentify = lists:reverse( ToIdentifyRev ),
assign_in_turn_ids( LowerId, MaxId, ToIdentify, IdentifierTable );
% New element to process, maybe identified, maybe not:
assign_ranged_identifiers( _RemainingElems=[ E | T ], ToIdentifyRev, LowerId,
IdentifierTable ) ->
% Here we try to stop accumulating unidentified elements, if E is:
case ?table:lookup_entry( E, IdentifierTable ) of
key_not_found ->
% No, so it is another element yet to identify:
assign_ranged_identifiers( T, [ E | ToIdentifyRev ], LowerId,
IdentifierTable );
{ value, Id } ->
% This element gave us thus an upper bound:
NewIdTable = assign_in_turn_ids( LowerId, Id, ToIdentifyRev,
IdentifierTable ),
assign_ranged_identifiers( T, _ToIdentifyRev=[], LowerId,
NewIdTable )
end.
% Assigns an identifier to each of the specified elements, using specified
% (excluded) identifier bounds for that.
%
% (helper)
%
-spec assign_in_turn_ids( sortable_id(), sortable_id(),
[ identifiable_element() ], identifier_table() ) -> identifier_table().
assign_in_turn_ids( _LowerId, _HigherId, _ElemsToIdentify=[],
IdentifierTable ) ->
IdentifierTable;
assign_in_turn_ids( LowerId, HigherId, _ElemsToIdentify=[ E | T ],
IdentifierTable ) ->
NewId = get_sortable_id_between( LowerId, HigherId ),
trace_utils:debug_fmt( "- assigning to element ~p, between ~s and ~s: ~s",
[ E, sortable_id_to_string( LowerId ),
sortable_id_to_string( HigherId ),
sortable_id_to_string( NewId ) ] ),
NewIdtable = ?table:add_new_entry( E, NewId, IdentifierTable ),
assign_in_turn_ids( NewId, HigherId, T, NewIdtable ).
% Returns a textual representation of specified sortable identifier.
-spec sortable_id_to_string( sortable_id() ) -> text_utils:ustring().
sortable_id_to_string( _Id=?lower_bound_id ) ->
"lower bound";
sortable_id_to_string( _Id=?upper_bound_id ) ->
"upper bound";
sortable_id_to_string( Id ) ->
% Better represented as tuple:
text_utils:format( "~w", [ list_to_tuple( Id ) ] ).
% Returns a textual representation of specified sortable identifiers.
-spec sortable_ids_to_string( [ sortable_id() ] ) -> text_utils:ustring().
sortable_ids_to_string( _Ids=[] ) ->
"(no sortable id)";
sortable_ids_to_string( Ids ) ->
text_utils:strings_to_listed_string(
[ sortable_id_to_string( Id ) || Id <- Ids ] ).
% Returns a textual representation of specified table of sortable identifiers.
-spec identifier_table_to_string( identifier_table() ) -> text_utils:ustring().
identifier_table_to_string( IdentifierTable ) ->
case ?table:enumerate( IdentifierTable ) of
[] ->
"empty identifier table";
ElemIdPairs ->
Strings = [ text_utils:format( "element '~p' associated to "
"identifier ~s",
[ E, sortable_id_to_string( Id ) ] )
|| { E, Id } <- ElemIdPairs ],
text_utils:format( "identifier table having ~B entries: ~s",
[ length( ElemIdPairs ),
text_utils:strings_to_string( Strings ) ] )
end.