diff --git a/lib/latch/tid.ex b/lib/latch/tid.ex index fc93499..5ffb85b 100644 --- a/lib/latch/tid.ex +++ b/lib/latch/tid.ex @@ -71,6 +71,66 @@ defmodule Latch.TID do |> String.pad_leading(13, "2") end + @doc """ + Each TID encodes a timestamp and a 10 bit clock ID. This returns the + timestamp part for you as a DateTime. Note that TIDs can be created + using any date, and it does not necessarily match when it was created + or have any relationship with the data the TID is used for. + + `tid` must be a valid tid, use `valid?/1` to validate first. + + Example: + + Latch.TID.to_datetime("3mup4bbh67n2g") + iex> {:ok, ~U[2026-09-04 13:50:51.404467Z]} + """ + def to_datetime(tid) when is_binary(tid) do + microseconds = to_unix(tid) + # 1788529851404467 + + DateTime.from_unix(microseconds, :microsecond) + # {:ok, ~U[2026-09-04 13:50:51.404467Z]} + end + + @doc """ + Like `to_datetime/1` but raises if the datetime is invalid. + """ + def to_datetime!(tid) when is_binary(tid) do + {:ok, datetime} = to_datetime(tid) + datetime + end + + @doc """ + Each TID encodes a timestamp and a 10 bit clock ID. This returns the + timestamp part for you in the unix microseconds format. Note that TIDs + can be created using any date, and it does not necessarily match when + it was created or have any relationship with the data the TID is used for. + + `tid` must be a valid tid, use `valid?/1` to validate first. + """ + def to_unix(tid) when is_binary(tid) do + # Keeping the sample run that I used to work this out + # here as reference in case I need to come back to it. + + # reference tid: "3mup4bbh67n2g" + codepoints = String.to_charlist(tid) + # [51, 109, 117, 112, 52, 98, 98, 104, 54, 55, 110, 50, 103] + mapped = + Enum.map(codepoints, fn c -> + {pos, 1} = :binary.match(@alphabet, <>) + pos + end) + + # [1, 18, 26, 21, 2, 7, 7, 13, 4, 5, 19, 0, 12] + + int = Integer.undigits(mapped, 32) + # 1831454567838174220 + + # divide out the clock ID, leaving the timestamp + div(int, @offset) + # 1788529851404467 + end + defp random_clock_id do :rand.uniform(@offset) - 1 end diff --git a/test/latch/tid_test.exs b/test/latch/tid_test.exs index aff2038..b712587 100644 --- a/test/latch/tid_test.exs +++ b/test/latch/tid_test.exs @@ -72,4 +72,42 @@ defmodule Latch.TidTest do end end end + + describe "to_unix/1" do + test "extracts the timestamp from a TID" do + assert 1_788_529_851_404_467 == TID.to_unix("3mup4bbh67n2g") + assert 1_788_529_851_376_468 == TID.to_unix("3mup4bbgcuo2c") + assert 1_788_529_851_306_527 == TID.to_unix("3mup4bbe6kz2b") + end + + test "extreme case" do + assert 0 == TID.to_unix("2222222222222") + end + end + + describe "to_datetime/1" do + test "extracts the datetime from a TID" do + assert {:ok, ~U[2026-09-04 13:50:51.404467Z]} == TID.to_datetime("3mup4bbh67n2g") + assert {:ok, ~U[2026-09-04 13:50:51.376468Z]} == TID.to_datetime("3mup4bbgcuo2c") + assert {:ok, ~U[2026-09-04 13:50:51.306527Z]} == TID.to_datetime("3mup4bbe6kz2b") + end + + test "roundtrips" do + tid = TID.at_time(~U[2026-01-01 00:00:00.000000Z], 0) + assert {:ok, ~U[2026-01-01 00:00:00.000000Z]} == TID.to_datetime(tid) + + tid = TID.at_time(~U[2026-01-01 00:00:00.000000Z], 1023) + assert {:ok, ~U[2026-01-01 00:00:00.000000Z]} == TID.to_datetime(tid) + end + + test "extreme case" do + assert {:ok, ~U[1970-01-01 00:00:00.000000Z]} == TID.to_datetime("2222222222222") + end + end + + describe "to_datetime!/1" do + test "unwraps the tuple" do + assert ~U[2026-09-04 13:50:51.404467Z] == TID.to_datetime!("3mup4bbh67n2g") + end + end end