# A fork of the code in https://github.com/vonagam/ash_ulid which seems to be the best implementation but also # depends on the whole framework. Modified heavily, it's not even a ulid anymore. # ORIGINAL LICENSE FOR THIS FILE: # MIT License # Copyright (c) 2023 Dmitry Maganov # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, # modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software # is furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR # IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # MODIFICATIONS BEYOND THE ORIGINAL FILE Copyright (c) 2025, Prefactor Pty Ltd # Our format is like this: # 01an4z07byd9df0k79ka1307sr9x4mv3 # |--------||----||--------------| # Time Part Randomness # 48bits 30b 80bits # The "part" is "partition" and is 30 bits. # The other sections are just like normal ulid # Bit length = 160 (48 + 32 + 80) Note that the partition field is 32 bits, but only 30 bits are used. We're maintaining byte alignment for the fields. # Character length = 32 (256 bits) defmodule Pfid do defmodule PfidError do defexception [:code, :message] def make(code, problem: problem) do %PfidError{ code: code, message: case code do :invalid_binary -> "invalid binary PFID: #{inspect(problem)}" :invalid_pfid -> "invalid PFID: #{inspect(problem)}" :invalid_partition -> "invalid partition: #{inspect(problem)}" end } end end defguard valid_timestamp?(timestamp) when is_integer(timestamp) and timestamp >= 0 and timestamp < 281_474_976_710_656 defguard valid_partition?(partition) when is_integer(partition) and partition >= 0 and partition < 1_073_741_824 @moduledoc """ Helpers for working with PFIDs. """ @typedoc """ A Crockford Base32 encoded PFID string. """ @type t :: <<_::256>> @typedoc """ A raw binary representation of a PFID. """ @type binary_pfid :: <<_::160>> @typedoc """ A partition number for a PFID. """ @type partition :: 0..1_073_741_823 @typedoc """ A timestamp for a PFID. """ @type timestamp :: 0..281_474_976_710_655 @doc """ A zero PFID -- probably don't actually use it, but if you need a placeholder. """ @spec zero() :: t def zero() do "00000000000000000000000000000000" end @doc """ Generate a Crockford Base32 encoded PFID string with current time. """ @spec generate(partition) :: t def generate(partition) when valid_partition?(partition) do unsafe_encode(generate_binary(partition)) end @doc """ Generate a Crockford Base32 encoded PFID string with a provided Unix timestamp. """ @spec generate(partition, timestamp) :: t def generate(partition, timestamp) when valid_partition?(partition) and valid_timestamp?(timestamp) do unsafe_encode(generate_binary(partition, timestamp)) end @doc """ Generate an ID suitable for use in an example -- it's well into the past. """ def generate_example() do generate(123_456_789, 1_234_567_890_000) end @doc """ Generate an ID with the same partition as an existing PFID. """ def generate_related(existing_pfid) do generate(extract_partition!(existing_pfid)) end @doc """ Generate an ID with a random partition. """ def generate_root() do generate(generate_partition()) end @doc """ Generate a binary PFID with current time. """ @spec generate_binary(partition) :: binary_pfid def generate_binary(partition) when valid_partition?(partition) do <> end @doc """ Generate a binary PFID with a provided Unix timestamp. """ @spec generate_binary(partition, timestamp) :: binary_pfid def generate_binary(partition, timestamp) when valid_partition?(partition) and valid_timestamp?(timestamp) do <> end @spec is_pfid?(string :: any) :: boolean() def is_pfid?(string) when is_binary(string) do String.match?(string, ~r/^[0-7][0-9abcdefghjkmnpqrstvwxyz]{31}$/) end def is_pfid?(_) do false end defp unsafe_encode( <> ) do <> end def encode(binary) when is_binary(binary) and byte_size(binary) == 20 do encoded = unsafe_encode(binary) if is_pfid?(encoded) do {:ok, encoded} else {:error, PfidError.make(:invalid_binary, problem: binary)} end end def encode(value) do {:error, PfidError.make(:invalid_binary, problem: value)} end def decode( <> = binary ) when t1 < ?8 do {:ok, <>} catch :error -> {:error, PfidError.make(:invalid_pfid, problem: binary)} end def decode(value) do {:error, PfidError.make(:invalid_pfid, problem: value)} end def decode_partition(<> = binary) do {:ok, :binary.decode_unsigned(<<0::2, d(p1)::5, d(p2)::5, d(p3)::5, d(p4)::5, d(p5)::5, d(p6)::5>>)} catch :error -> {:error, PfidError.make(:invalid_partition, problem: binary)} end def decode_partition(value) do {:error, PfidError.make(:invalid_partition, problem: value)} end def extract_partition(<<_::80, encoded_partition::binary-size(6), _::128>> = string) do if is_pfid?(string) do decode_partition(encoded_partition) else {:error, PfidError.make(:invalid_pfid, problem: string)} end end def extract_partition(value) do {:error, PfidError.make(:invalid_pfid, problem: value)} end def extract_partition!(pfid) do case extract_partition(pfid) do {:ok, value} -> value {:error, error} -> raise error end end def generate_partition() do <<_::2, partition::30>> = :crypto.strong_rand_bytes(4) partition end @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 defp e(16), do: ?g defp e(17), do: ?h defp e(18), do: ?j defp e(19), do: ?k defp e(20), do: ?m defp e(21), do: ?n defp e(22), do: ?p defp e(23), do: ?q defp e(24), do: ?r defp e(25), do: ?s defp e(26), do: ?t defp e(27), do: ?v defp e(28), do: ?w defp e(29), do: ?x defp e(30), do: ?y defp e(31), do: ?z @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(?g), do: 16 defp d(?h), do: 17 defp d(?j), do: 18 defp d(?k), do: 19 defp d(?m), do: 20 defp d(?n), do: 21 defp d(?p), do: 22 defp d(?q), do: 23 defp d(?r), do: 24 defp d(?s), do: 25 defp d(?t), do: 26 defp d(?v), do: 27 defp d(?w), do: 28 defp d(?x), do: 29 defp d(?y), do: 30 defp d(?z), do: 31 defp d(_), do: throw(:error) end