%% -------- Utility Functions --------- %% %% Generally helpful funtions within leveled %% -module(leveled_util). -include("include/leveled.hrl"). -export([generate_uuid/0, integer_now/0, integer_time/1, magic_hash/1, t2b/1, safe_rename/4]). -define(WRITE_OPS, [binary, raw, read, write]). -spec generate_uuid() -> list(). %% @doc %% Generate a new globally unique ID as a string. %% Credit to %% https://github.com/afiskon/erlang-uuid-v4/blob/master/src/uuid.erl generate_uuid() -> <> = leveled_rand:rand_bytes(16), L = io_lib:format("~8.16.0b-~4.16.0b-4~3.16.0b-~4.16.0b-~12.16.0b", [A, B, C band 16#0fff, D band 16#3fff bor 16#8000, E]), binary_to_list(list_to_binary(L)). -spec integer_now() -> non_neg_integer(). %% @doc %% Return now in gregorian seconds integer_now() -> integer_time(os:timestamp()). -spec integer_time (erlang:timestamp()) -> non_neg_integer(). %% @doc %% Return a given time in gergorian seconds integer_time(TS) -> DT = calendar:now_to_universal_time(TS), calendar:datetime_to_gregorian_seconds(DT). -spec magic_hash(any()) -> integer(). %% @doc %% Use DJ Bernstein magic hash function. Note, this is more expensive than %% phash2 but provides a much more balanced result. %% %% Hash function contains mysterious constants, some explanation here as to %% what they are - %% http://stackoverflow.com/questions/10696223/reason-for-5381-number-in-djb-hash-function magic_hash({binary, BinaryKey}) -> H = 5381, hash1(H, BinaryKey) band 16#FFFFFFFF; magic_hash(AnyKey) -> BK = t2b(AnyKey), magic_hash({binary, BK}). hash1(H, <<>>) -> H; hash1(H, <>) -> H1 = H * 33, H2 = H1 bxor B, hash1(H2, Rest). -spec t2b(term()) -> binary(). %% @doc %% term_to_binary with options necessary to ensure backwards compatability %% in the handling of atoms (within OTP 26). %% See https://github.com/martinsumner/leveled/issues/407 %% If the binary() which is outputted is to be hashed for comparison, then %% this must be used. t2b(Term) -> term_to_binary(Term, [{minor_version, 1}]). -spec safe_rename(string(), string(), binary(), boolean()) -> ok. %% @doc %% Write a file, sync it and rename it (and for super-safe mode read it back) %% An attempt to prevent crashes leaving files with empty or partially written %% values safe_rename(TempFN, RealFN, BinData, ReadCheck) -> {ok, TempFH} = file:open(TempFN, ?WRITE_OPS), ok = file:write(TempFH, BinData), ok = file:sync(TempFH), ok = file:close(TempFH), ok = file:rename(TempFN, RealFN), case ReadCheck of true -> {ok, ReadBack} = file:read_file(RealFN), true = (ReadBack == BinData), ok; false -> ok end. %%%============================================================================ %%% Test %%%============================================================================ -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). -define(TEST_AREA, "test/test_area/util/"). magichashperf_test() -> KeyFun = fun(X) -> K = {o, "Bucket", "Key" ++ integer_to_list(X), null}, {K, X} end, KL = lists:map(KeyFun, lists:seq(1, 1000)), {TimeMH, HL1} = timer:tc(lists, map, [fun(K) -> magic_hash(K) end, KL]), io:format(user, "1000 keys magic hashed in ~w microseconds~n", [TimeMH]), {TimePH, _Hl2} = timer:tc(lists, map, [fun(K) -> erlang:phash2(K) end, KL]), io:format(user, "1000 keys phash2 hashed in ~w microseconds~n", [TimePH]), {TimeMH2, HL1} = timer:tc(lists, map, [fun(K) -> magic_hash(K) end, KL]), io:format(user, "1000 keys magic hashed in ~w microseconds~n", [TimeMH2]). safe_rename_test() -> ok = filelib:ensure_dir(?TEST_AREA), TempFN = filename:join(?TEST_AREA, "test_manifest0.pnd"), RealFN = filename:join(?TEST_AREA, "test_manifest0.man"), ok = safe_rename(TempFN, RealFN, <<1:128/integer>>, false), ?assertMatch({ok, <<1:128/integer>>}, file:read_file(RealFN)), TempFN1 = filename:join(?TEST_AREA, "test_manifest1.pnd"), RealFN1 = filename:join(?TEST_AREA, "test_manifest1.man"), ok = safe_rename(TempFN1, RealFN1, <<2:128/integer>>, true), ?assertMatch({ok, <<2:128/integer>>}, file:read_file(RealFN1)). -endif.