Music streaming on ATProto!
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