defmodule AshDoubleEntry.ULID do @moduledoc """ An Ash type for ULID strings. """ use Ash.Type @typedoc """ A hex-encoded ULID string. """ @type t :: <<_::208>> @doc """ The underlying schema type. """ def storage_type, do: :binary @doc """ Casts a string to ULID. """ def cast_input(nil, _), do: {:ok, nil} def cast_input(<<_::bytes-size(16)>> = value, constraints) do case encode(value) do {:ok, encoded} -> cast_input(encoded, constraints) :error -> :error end end def cast_input(<<_::bytes-size(26)>> = value, _) do if valid?(value) do {:ok, value} else :error end end def cast_input(_, _), do: :error @doc """ Converts a Crockford Base32 encoded ULID into a binary. """ def dump_to_native(nil, _), do: {:ok, nil} def dump_to_native(<<_::bytes-size(26)>> = encoded, _), do: decode(encoded) def dump_to_native(_, _), do: :error @doc """ Converts a binary ULID into a Crockford Base32 encoded string. """ def cast_stored(nil, _), do: {:ok, nil} def cast_stored(<<_::unsigned-size(128)>> = bytes, _), do: encode(bytes) def cast_stored(_, _), do: :error @doc false def autogenerate, do: generate() @doc """ Generates a Crockford Base32 encoded ULID. If a value is provided for `timestamp`, the generated ULID will be for the provided timestamp. Otherwise, a ULID will be generated for the current time. Arguments: * `timestamp`: A Unix timestamp with millisecond precision. """ def generate(timestamp \\ System.system_time(:millisecond)) def generate(%DateTime{} = datetime) do datetime |> DateTime.to_unix(:millisecond) |> generate() end def generate(timestamp) do {:ok, ulid} = encode(bingenerate(timestamp)) ulid end @doc """ Generates a Crockford Base32 encoded ULID, guaranteed to sort equal to or after any other ULID generated for the same timestamp. Do not use this for storage, only for generating comparators, i.e "balance as of a given ulid". If a value is provided for `timestamp`, the generated ULID will be for the provided timestamp. Otherwise, a ULID will be generated for the current time. Arguments: * `timestamp`: A Unix timestamp with millisecond precision. """ def generate_last(timestamp \\ System.system_time(:millisecond)) def generate_last(%DateTime{} = datetime) do datetime |> DateTime.to_unix(:millisecond) |> generate_last() end def generate_last(timestamp) do {:ok, ulid} = encode(bingenerate_last(timestamp)) ulid end @doc """ Generates a binary ULID. If a value is provided for `timestamp`, the generated ULID will be for the provided timestamp. Otherwise, a ULID will be generated for the current time. Arguments: * `timestamp`: A Unix timestamp with millisecond precision. """ def bingenerate(timestamp \\ System.system_time(:millisecond)) do <> end @doc """ Generates a binary ULID. Do not use this for storage, only for generating comparators, i.e "balance as of a given ulid". If a value is provided for `timestamp`, the generated ULID will be for the provided timestamp. Otherwise, a ULID will be generated for the current time. Arguments: * `timestamp`: A Unix timestamp with millisecond precision. """ def bingenerate_last(timestamp \\ System.system_time(:millisecond)) do <> end @doc false def encode( <> ) do <> catch :error -> :error else encoded -> {:ok, encoded} end def encode(_), do: :error @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 defp decode( <> ) do <> catch :error -> :error else decoded -> {:ok, decoded} end defp decode(_), do: :error @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) defp valid?( <> ) do v(c1) && v(c2) && v(c3) && v(c4) && v(c5) && v(c6) && v(c7) && v(c8) && v(c9) && v(c10) && v(c11) && v(c12) && v(c13) && v(c14) && v(c15) && v(c16) && v(c17) && v(c18) && v(c19) && v(c20) && v(c21) && v(c22) && v(c23) && v(c24) && v(c25) && v(c26) end defp valid?(_), do: false @compile {:inline, v: 1} defp v(?0), do: true defp v(?1), do: true defp v(?2), do: true defp v(?3), do: true defp v(?4), do: true defp v(?5), do: true defp v(?6), do: true defp v(?7), do: true defp v(?8), do: true defp v(?9), do: true defp v(?A), do: true defp v(?B), do: true defp v(?C), do: true defp v(?D), do: true defp v(?E), do: true defp v(?F), do: true defp v(?G), do: true defp v(?H), do: true defp v(?J), do: true defp v(?K), do: true defp v(?M), do: true defp v(?N), do: true defp v(?P), do: true defp v(?Q), do: true defp v(?R), do: true defp v(?S), do: true defp v(?T), do: true defp v(?V), do: true defp v(?W), do: true defp v(?X), do: true defp v(?Y), do: true defp v(?Z), do: true defp v(_), do: false end