@@ -129,7 +129,8 @@ if Code.ensure_loaded?(Ecto.ParameterizedType) do
129129
130130 @ rfc_variant 2
131131
132- @ spec bingenerate ( ) :: uuid_binary
132+ @ doc false
133+ @ spec bingenerate :: uuid_binary
133134 def bingenerate do
134135 time = System . system_time ( :millisecond )
135136 << rand_a :: 12 , rand_b :: 62 , _ :: 6 >> = :crypto . strong_rand_bytes ( 10 )
@@ -175,6 +176,36 @@ if Code.ensure_loaded?(Ecto.ParameterizedType) do
175176 end
176177 end
177178
179+ @ doc """
180+ Convert a UUID, regardless of current format, into its raw binary form
181+
182+ Same as `to_binary/1` but raises if conversion fails.
183+
184+ ## Examples
185+
186+ iex> Together.ID.to_binary!(<<1, 147, 48, 97, 106, 163, 123, 39, 141, 46, 234, 126, 170, 90, 115, 70>>)
187+ <<1, 147, 48, 97, 106, 163, 123, 39, 141, 46, 234, 126, 170, 90, 115, 70>>
188+
189+ iex> Together.ID.to_binary!("01933061-6aa3-7b27-8d2e-ea7eaa5a7346")
190+ <<1, 147, 48, 97, 106, 163, 123, 39, 141, 46, 234, 126, 170, 90, 115, 70>>
191+
192+ iex> Together.ID.to_binary!("test_CHErKsMSgQVrdQxEj7nmB")
193+ <<1, 147, 48, 97, 106, 163, 123, 39, 141, 46, 234, 126, 170, 90, 115, 70>>
194+
195+ iex> Together.ID.to_binary!("bad data")
196+ ** (ArgumentError) invalid UUID format
197+
198+ """
199+ @ spec to_binary! ( uuid_binary ) :: uuid_binary | no_return
200+ @ spec to_binary! ( uuid_string ) :: uuid_binary | no_return
201+ @ spec to_binary! ( uuid_slug ) :: uuid_binary | no_return
202+ def to_binary! ( uuid ) do
203+ case to_binary ( uuid ) do
204+ { :ok , uuid_binary } -> uuid_binary
205+ :error -> raise ArgumentError , "invalid UUID format"
206+ end
207+ end
208+
178209 @ doc """
179210 Convert a UUID, regardless of current format, to a standard string form
180211
@@ -209,6 +240,36 @@ if Code.ensure_loaded?(Ecto.ParameterizedType) do
209240 end
210241 end
211242
243+ @ doc """
244+ Convert a UUID, regardless of current format, to a standard string form
245+
246+ Same as `to_uuid/1` but raises if conversion fails.
247+
248+ ## Examples
249+
250+ iex> Together.ID.to_uuid!(<<1, 147, 48, 97, 106, 163, 123, 39, 141, 46, 234, 126, 170, 90, 115, 70>>)
251+ "01933061-6aa3-7b27-8d2e-ea7eaa5a7346"
252+
253+ iex> Together.ID.to_uuid!("01933061-6aa3-7b27-8d2e-ea7eaa5a7346")
254+ "01933061-6aa3-7b27-8d2e-ea7eaa5a7346"
255+
256+ iex> Together.ID.to_uuid!("test_CHErKsMSgQVrdQxEj7nmB")
257+ "01933061-6aa3-7b27-8d2e-ea7eaa5a7346"
258+
259+ iex> Together.ID.to_uuid!("bad data")
260+ ** (ArgumentError) invalid UUID format
261+
262+ """
263+ @ spec to_uuid! ( uuid_binary ) :: uuid_string | no_return
264+ @ spec to_uuid! ( uuid_string ) :: uuid_string | no_return
265+ @ spec to_uuid! ( uuid_slug ) :: uuid_string | no_return
266+ def to_uuid! ( uuid ) do
267+ case to_uuid ( uuid ) do
268+ { :ok , uuid_string } -> uuid_string
269+ :error -> raise ArgumentError , "invalid UUID format"
270+ end
271+ end
272+
212273 @ doc """
213274 Convert a UUID, regardless of current format, to a base-58 encoded slug without prefix
214275
@@ -244,6 +305,36 @@ if Code.ensure_loaded?(Ecto.ParameterizedType) do
244305 end
245306 end
246307
308+ @ doc """
309+ Convert a UUID, regardless of current format, to a base-58 encoded slug without prefix
310+
311+ Same as `to_slug/1` but raises if conversion fails.
312+
313+ ## Examples
314+
315+ iex> Together.ID.to_slug!(<<1, 147, 48, 97, 106, 163, 123, 39, 141, 46, 234, 126, 170, 90, 115, 70>>)
316+ "CHErKsMSgQVrdQxEj7nmB"
317+
318+ iex> Together.ID.to_slug!("01933061-6aa3-7b27-8d2e-ea7eaa5a7346")
319+ "CHErKsMSgQVrdQxEj7nmB"
320+
321+ iex> Together.ID.to_slug!("test_CHErKsMSgQVrdQxEj7nmB")
322+ "test_CHErKsMSgQVrdQxEj7nmB"
323+
324+ iex> Together.ID.to_slug!("bad data")
325+ ** (ArgumentError) invalid UUID format
326+
327+ """
328+ @ spec to_slug! ( uuid_binary ) :: uuid_slug | no_return
329+ @ spec to_slug! ( uuid_string ) :: uuid_slug | no_return
330+ @ spec to_slug! ( uuid_slug ) :: uuid_slug | no_return
331+ def to_slug! ( uuid ) do
332+ case to_slug ( uuid ) do
333+ { :ok , uuid_slug } -> uuid_slug
334+ :error -> raise ArgumentError , "invalid UUID format"
335+ end
336+ end
337+
247338 @ doc """
248339 Convert a UUID, regardless of current format, to a base-58 encoded slug with prefix
249340
@@ -279,6 +370,36 @@ if Code.ensure_loaded?(Ecto.ParameterizedType) do
279370 end
280371 end
281372
373+ @ doc """
374+ Convert a UUID, regardless of current format, to a base-58 encoded slug with prefix
375+
376+ Same as `to_slug/2` but raises if conversion fails.
377+
378+ ## Examples
379+
380+ iex> Together.ID.to_slug!(<<1, 147, 48, 97, 106, 163, 123, 39, 141, 46, 234, 126, 170, 90, 115, 70>>, "test")
381+ "test_CHErKsMSgQVrdQxEj7nmB"
382+
383+ iex> Together.ID.to_slug!("01933061-6aa3-7b27-8d2e-ea7eaa5a7346", "test")
384+ "test_CHErKsMSgQVrdQxEj7nmB"
385+
386+ iex> Together.ID.to_slug!("test_CHErKsMSgQVrdQxEj7nmB", "test")
387+ "test_CHErKsMSgQVrdQxEj7nmB"
388+
389+ iex> Together.ID.to_slug!("bad data", "test")
390+ ** (ArgumentError) invalid UUID format
391+
392+ """
393+ @ spec to_slug! ( uuid_binary , String . t ( ) ) :: uuid_slug | no_return
394+ @ spec to_slug! ( uuid_string , String . t ( ) ) :: uuid_slug | no_return
395+ @ spec to_slug! ( uuid_slug , String . t ( ) ) :: uuid_slug | no_return
396+ def to_slug! ( uuid , prefix ) do
397+ case to_slug ( uuid , prefix ) do
398+ { :ok , uuid_slug } -> uuid_slug
399+ :error -> raise ArgumentError , "invalid UUID format"
400+ end
401+ end
402+
282403 @ spec binary_to_slug ( uuid_binary , String . t ( ) ) :: { :ok , uuid_slug } | :error
283404 defp binary_to_slug ( << _ :: 128 >> = uuid_binary , "" ) , do: { :ok , encode_base58 ( uuid_binary ) }
284405
@@ -304,7 +425,7 @@ if Code.ensure_loaded?(Ecto.ParameterizedType) do
304425 end
305426
306427 @ spec slug_to_uuid ( uuid_slug ) :: { :ok , String . t ( ) , uuid_string } | :error
307- def slug_to_uuid ( uuid_slug ) do
428+ defp slug_to_uuid ( uuid_slug ) do
308429 with { :ok , prefix , uuid_binary } <- slug_to_binary ( uuid_slug ) ,
309430 { :ok , uuid_string } <- binary_to_uuid ( uuid_binary ) do
310431 { :ok , prefix , uuid_string }
@@ -384,6 +505,31 @@ if Code.ensure_loaded?(Ecto.ParameterizedType) do
384505 # Helpers
385506 #
386507
508+ @ doc """
509+ Extract the timestamp embedded in the UUIDv7
510+
511+ ## Examples
512+
513+ iex> Together.ID.extract_timestamp("01933061-6aa3-7b27-8d2e-ea7eaa5a7346")
514+ {:ok, ~U[2024-11-15 15:11:50.947Z]}
515+
516+ iex> Together.ID.extract_timestamp("test_CQGFY5NK2muwxrpHsNJ28")
517+ {:ok, ~U[2025-06-18 19:38:02.580Z]}
518+
519+ iex> Together.ID.extract_timestamp("bad data")
520+ :error
521+
522+ """
523+ @ spec extract_timestamp ( uuid_binary ) :: { :ok , DateTime . t ( ) } | :error
524+ def extract_timestamp ( uuid ) do
525+ with { :ok , << timestamp :: big - unsigned - integer - size ( 48 ) , _rest :: binary >> } <- to_binary ( uuid ) ,
526+ { :ok , datetime } <- DateTime . from_unix ( timestamp , :millisecond ) do
527+ { :ok , datetime }
528+ else
529+ _ -> :error
530+ end
531+ end
532+
387533 @ doc """
388534 Get minimum and maximum UUID values for a given creation date
389535
@@ -394,7 +540,7 @@ if Code.ensure_loaded?(Ecto.ParameterizedType) do
394540 The lower bound is inclusive, meaning it is possible (though extremely unlikely) to generate
395541 an ID with that value. The upper bound is exclusive, as it belongs to the next calendar date.
396542
397- ## Examples
543+ ## Example
398544
399545 iex> Together.ID.min_max(~D[2025-03-01])
400546 {"01954f00-b000-7000-8000-000000000000", "01955427-0c00-7000-8000-000000000000"}
0 commit comments