defmodule Ash.UUIDv7 do @moduledoc """ Helpers for working with UUIDs version 7. [RFC 9562](https://www.rfc-editor.org/rfc/rfc9562#name-uuid-version-7) Used for generating UUIDs version 7 with increased clock precision for better monotonicity, as described by method 3 of the [Section 6.2](https://www.rfc-editor.org/rfc/rfc9562#name-monotonicity-and-counters Inspired by the work of [Ryan Winchester](https://github.com/ryanwinchester/) on [uuidv7](https://github.com/ryanwinchester/uuidv7) ## Examples iex> UUIDv7.generate() "018e90d8-06e8-7f9f-bfd7-6730ba98a51b" iex> UUIDv7.bingenerate() <<1, 142, 144, 216, 6, 232, 127, 159, 191, 215, 103, 48, 186, 152, 165, 27>> """ @typedoc """ A hex-encoded UUID string. """ @type t :: <<_::288>> @typedoc """ A raw binary representation of a UUID. """ @type raw :: <<_::128>> @version 7 @variant 2 @doc """ Generates a version 7 UUID using submilliseconds for increased clock precision. ## Example iex> UUIDv7.generate() "018e90d8-06e8-7f9f-bfd7-6730ba98a51b" """ @spec generate() :: t def generate, do: bingenerate() |> encode() @doc """ Generates a version 7 UUID in the binary format. ## Example iex> UUIDv7.bingenerate() <<1, 142, 144, 216, 6, 232, 127, 159, 191, 215, 103, 48, 186, 152, 165, 27>> """ @spec bingenerate() :: raw def bingenerate do timestamp_nanoseconds = System.system_time(:nanosecond) timestamp_milliseconds = trunc(timestamp_nanoseconds / 1_000_000) nanoseconds = timestamp_nanoseconds - timestamp_milliseconds * 1_000_000 rand_a = trunc(4096 * (nanoseconds / 1_000_000)) <> = :crypto.strong_rand_bytes(8) << timestamp_milliseconds::big-unsigned-48, @version::4, rand_a::12, @variant::2, rand_b::62 >> end @doc """ Extract the millisecond timestamp from the UUID. ## Example iex> UUIDv7.extract_timestamp("018ecb40-c457-73e6-a400-000398daddd9") 1712807003223 """ @spec extract_timestamp(t | raw) :: integer def extract_timestamp(<>), do: timestamp_ms def extract_timestamp(<<_::288>> = uuid) do uuid |> decode() |> extract_timestamp() end @doc """ Encode a raw UUID to the string representation. ## Example iex> UUIDv7.encode(<<1, 142, 144, 216, 6, 232, 127, 159, 191, 215, 103, 48, 186, 152, 165, 27>>) "018e90d8-06e8-7f9f-bfd7-6730ba98a51b" """ @spec encode(t | raw) :: t | :error def encode( <<_, _, _, _, _, _, _, _, ?-, _, _, _, _, ?-, _, _, _, _, ?-, _, _, _, _, ?-, _, _, _, _, _, _, _, _, _, _, _, _>> = hex_uuid ), do: hex_uuid def encode( <> ) do <> end def encode(_), do: :error @compile {:inline, e: 1} defp e(0), do: ?0 defp e(1), do: ?1 defp e(2), do: ?2 defp e(3), do: ?3 defp e(4), do: ?4 defp e(5), do: ?5 defp e(6), do: ?6 defp e(7), do: ?7 defp e(8), do: ?8 defp e(9), do: ?9 defp e(10), do: ?a defp e(11), do: ?b defp e(12), do: ?c defp e(13), do: ?d defp e(14), do: ?e defp e(15), do: ?f @doc """ Decode a string representation of a UUID to the raw binary version. ## Example iex> UUIDv7.decode("018e90d8-06e8-7f9f-bfd7-6730ba98a51b") <<1, 142, 144, 216, 6, 232, 127, 159, 191, 215, 103, 48, 186, 152, 165, 27>> """ @spec decode(t | raw) :: raw | :error def decode( <<_::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4>> = raw_uuid ), do: raw_uuid def decode( <> ) do <> catch :error -> :error end def decode(_), do: :error @compile {:inline, d: 1} defp d(?0), do: 0 defp d(?1), do: 1 defp d(?2), do: 2 defp d(?3), do: 3 defp d(?4), do: 4 defp d(?5), do: 5 defp d(?6), do: 6 defp d(?7), do: 7 defp d(?8), do: 8 defp d(?9), do: 9 defp d(?A), do: 10 defp d(?B), do: 11 defp d(?C), do: 12 defp d(?D), do: 13 defp d(?E), do: 14 defp d(?F), do: 15 defp d(?a), do: 10 defp d(?b), do: 11 defp d(?c), do: 12 defp d(?d), do: 13 defp d(?e), do: 14 defp d(?f), do: 15 defp d(_), do: throw(:error) end