defmodule FirebasePushid do @moduledoc """ Generates a timestamp based id as Firebase does. If same timestamp has already generate id it generates new id sortable. """ alias FirebasePushid.Cache alias FirebasePushid.Bump @doc """ Generates Firebase id. It caches with previous timestamp. If same timestamp is stored in the cache it will generate an incremental id in base 64 trying to increment the last possible character. ex: 1) -Kyukibfm7T0jJT_Deyr 2) -Kyukibfm7T0jJT_Deys If last can not be incremented will update the next ex: 1) -KyukibfiPUIPSaeXo9z 2) -KyukibfiPUIPSaeXoA- 3) -KyukibfiPUIPSaeXoA0 ... x) -KyukibfiPUIPSaeXoAz x) -KyukibfiPUIPSaeXoB- x) -KyukibfiPUIPSaeXoB0 """ def generate do # { :ok, data } = Cache.start_link(:independent) next_id(Cache, seed()) end @push_charts "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz" @charts_list (@push_charts |> String.codepoints) @doc """ Generates Firebase id with previous cached data (FirebasePushid.Cache) and timestamp. """ def next_id(data, ts) do is_dup = (ts == Cache.prev_ts(data)) Cache.update(data, :prev_ts, ts) id = build_ts_char_list(ts) |> Enum.join if is_dup do increment_list(data) else Cache.set_random_nums(data, random_nums(12)) end tail = Cache.random_nums(data) |> string_by_nums id = id <> tail unless String.length(id) == 20 do raise "not valid" end id end @doc """ Updates last possible index to generate a not colisioning id example: iex> FirebasePushid.increment_list([1, 2, 3, 4]) [1, 2, 3, 5] iex> FirebasePushid.increment_list([2, 63]) [3, 0] """ def increment_list(list) when is_list(list), do: Bump.call(list) def increment_list(data) when is_pid(data) do new_list = Cache.random_nums(data) |> Bump.call() Cache.set_random_nums(data, new_list) end @doc """ Returns a `List` with 8 characters based on unix timestamp example: iex> 1510647799 |> FirebasePushid.build_ts_char_list ["-", "-", "0", "P", "1", "e", "U", "r"] """ def build_ts_char_list(ts) do { ts_chars, 0 } = [] |> build_ts_char_list(ts, 8) ts_chars end defp build_ts_char_list(acc, ts, 0), do: { acc, ts } defp build_ts_char_list(acc, ts, i) do char = @charts_list |> Enum.at(rem(ts, 64)) next_ts = Float.floor(ts / 64) |> round [char | acc] |> build_ts_char_list(next_ts, i - 1) end @doc """ Generates the amount of random numbers all to the max of 63 output example: [41, 7, 5, 22, 40, 5, 35, 42] example: iex> FirebasePushid.random_nums(8) ...> |> Enum.count 8 """ def random_nums(amount), do: random_nums([], amount) def random_nums(acc, 0), do: acc def random_nums(acc, i) do num = (:rand.uniform() * 64) |> Float.floor |> round [num | acc] |> random_nums(i - 1) end @doc """ Given a array of numbers until 63, generates a string example: iex> [41, 7, 5, 22, 40, 5, 35, 42] ...> |> FirebasePushid.string_by_nums "d64Lc4Ye" """ def string_by_nums(int_list) do char_by_nums(int_list) |> Enum.reverse |> Enum.join end defp char_by_nums(random_int_list) do char_by_nums(random_int_list, []) end defp char_by_nums([], acc), do: acc defp char_by_nums([head | tail], acc) do char = @charts_list |> Enum.at(head) char_by_nums(tail, [char | acc]) end @doc """ Returns the current timestamp in milliseconds """ def seed do DateTime.utc_now |> DateTime.to_unix(:millisecond) end end