1defmodule Atproto.TID do
2 @moduledoc """
3 A module for encoding and decoding TIDs.
4
5 [TID](https://atproto.com/specs/tid) stands for "Timestamp Identifier". It is a 13-character string calculated from 53 bits representing a unix timestamp, in microsecond precision, plus 10 bits for an arbitrary "clock identifier", to help with uniqueness in distributed systems.
6
7 The string is encoded as "base32-sortable", meaning that the characters for the base 32 encoding are set up in such a way that string comparisons yield the same result as integer comparisons, i.e. if the integer representation of the timestamp that creates TID "A" is greater than the integer representation of the timestamp that creates TID "B", then "A" > "B" is also true, and vice versa.
8 """
9
10 import Bitwise
11
12 @tid_char_set ~c(234567abcdefghijklmnopqrstuvwxyz)
13 @tid_char_set_length 32
14
15 defstruct [
16 :timestamp,
17 :clock_id,
18 :string
19 ]
20
21 @typedoc """
22 TIDs are composed of two parts: a timestamp and a clock identifier. They also have a human-readable string representation as a "base32-sortable" encoded string.
23 """
24 @type t() :: %__MODULE__{
25 timestamp: integer(),
26 clock_id: integer(),
27 string: binary()
28 }
29
30 @doc """
31 Generates a random 10-bit clock identifier.
32 """
33 @spec random_clock_id() :: integer()
34 def random_clock_id(), do: :rand.uniform(1024) - 1
35
36 @doc """
37 Generates a new TID for the current time.
38
39 This is equivalent to calling `encode(nil)`.
40 """
41 @spec new() :: t()
42 def new(), do: encode(nil)
43
44 @doc """
45 Encodes an integer or DateTime struct into a 13-character string that is "base32-sortable" encoded.
46
47 If `timestamp` is nil, or not provided, the current time will be used as represented by `DateTime.utc_now()`.
48
49 If `clock_id` is nil, or not provided, a random 10-bit integer will be used.
50
51 If `timestamp` is an integer value, it *MUST* be a unix timestamp measured in microseconds. This function does not validate integer values.
52 """
53 @spec encode(nil | integer() | DateTime.t(), nil | integer()) :: t()
54 def encode(timestamp \\ nil, clock_id \\ nil)
55
56 def encode(nil, clock_id), do: encode(DateTime.utc_now(), clock_id)
57
58 def encode(timestamp, nil), do: encode(timestamp, random_clock_id())
59
60 def encode(%DateTime{} = datetime, clock_id) do
61 datetime
62 |> DateTime.to_unix(:microsecond)
63 |> encode(clock_id)
64 end
65
66 def encode(timestamp, clock_id) when is_integer(timestamp) and is_integer(clock_id) do
67 # Ensure we only use the lower 10 bit of clock_id
68 clock_id = clock_id &&& 1023
69 str =
70 timestamp
71 |> bsr(10)
72 |> bsl(10)
73 |> bxor(clock_id)
74 |> do_encode("")
75 %__MODULE__{timestamp: timestamp, clock_id: clock_id, string: str}
76 end
77
78 defp do_encode(0, acc), do: acc
79
80 defp do_encode(number, acc) do
81 c = rem(number, @tid_char_set_length)
82 number = div(number, @tid_char_set_length)
83 do_encode(number, <<Enum.at(@tid_char_set, c)>> <> acc)
84 end
85
86 @doc """
87 Decodes a binary string into a TID struct.
88 """
89 @spec decode(binary()) :: t()
90 def decode(tid) do
91 num = do_decode(tid, 0)
92 %__MODULE__{timestamp: bsr(num, 10), clock_id: num &&& 1023, string: tid}
93 end
94
95 defp do_decode(<<>>, acc), do: acc
96
97 defp do_decode(<<char::utf8, rest::binary>>, acc) do
98 idx = Enum.find_index(@tid_char_set, fn x -> x == char end)
99 do_decode(rest, (acc * @tid_char_set_length) + idx)
100 end
101end
102
103defimpl String.Chars, for: Atproto.TID do
104 def to_string(tid), do: tid.string
105end