src/sqid.erl

-module(sqid).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/sqid.gleam").
-export([alphabet/1, new/1, set_minimum_length/2, set_blocklist/2, encode/2, decode/2]).
-export_type([alphabet/0, options/0]).

-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.

-opaque alphabet() :: {alphabet, gleam@dict:dict(integer(), binary())}.

-opaque options() :: {options, alphabet(), integer(), list(binary())}.

-file("src/sqid.gleam", 406).
?DOC(
    " Given a dictionary of letters this returns the codepoint value of the letter\n"
    " at the given index.\n"
    " This would fail if:\n"
    " - the index is not included in the dictionary\n"
    " - the dictionary value is an empty string\n"
    "\n"
    " We only ever use this internally after validating an alphabet and making\n"
    " sure that all the entries are single letters, so under this assumption it\n"
    " should never fail.\n"
).
-spec letter_value(gleam@dict:dict(integer(), binary()), integer()) -> {ok,
        integer()} |
    {error, nil}.
letter_value(Letters, Index) ->
    _pipe = gleam_stdlib:map_get(Letters, Index),
    _pipe@1 = gleam@result:map(_pipe, fun gleam@string:to_utf_codepoints/1),
    _pipe@2 = gleam@result:'try'(_pipe@1, fun gleam@list:first/1),
    gleam@result:map(_pipe@2, fun gleam_stdlib:identity/1).

-file("src/sqid.gleam", 427).
-spec shuffle_alphabet_loop(
    gleam@dict:dict(integer(), binary()),
    integer(),
    integer()
) -> gleam@dict:dict(integer(), binary()).
shuffle_alphabet_loop(Letters, I, J) ->
    case J =< 0 of
        true ->
            Letters;

        false ->
            I_value@1 = case letter_value(Letters, I) of
                {ok, I_value} -> I_value;
                _assert_fail ->
                    erlang:error(#{gleam_error => let_assert,
                                message => <<"i always in range"/utf8>>,
                                file => <<?FILEPATH/utf8>>,
                                module => <<"sqid"/utf8>>,
                                function => <<"shuffle_alphabet_loop"/utf8>>,
                                line => 435,
                                value => _assert_fail,
                                start => 14846,
                                'end' => 14895,
                                pattern_start => 14857,
                                pattern_end => 14868})
            end,
            J_value@1 = case letter_value(Letters, J) of
                {ok, J_value} -> J_value;
                _assert_fail@1 ->
                    erlang:error(#{gleam_error => let_assert,
                                message => <<"j always in range"/utf8>>,
                                file => <<?FILEPATH/utf8>>,
                                module => <<"sqid"/utf8>>,
                                function => <<"shuffle_alphabet_loop"/utf8>>,
                                line => 436,
                                value => _assert_fail@1,
                                start => 14925,
                                'end' => 14974,
                                pattern_start => 14936,
                                pattern_end => 14947})
            end,
            R = case maps:size(Letters) of
                0 -> 0;
                Gleam@denominator -> (((I * J) + I_value@1) + J_value@1) rem Gleam@denominator
            end,
            Old_i@1 = case gleam_stdlib:map_get(Letters, I) of
                {ok, Old_i} -> Old_i;
                _assert_fail@2 ->
                    erlang:error(#{gleam_error => let_assert,
                                message => <<"i always in range"/utf8>>,
                                file => <<?FILEPATH/utf8>>,
                                module => <<"sqid"/utf8>>,
                                function => <<"shuffle_alphabet_loop"/utf8>>,
                                line => 439,
                                value => _assert_fail@2,
                                start => 15070,
                                'end' => 15113,
                                pattern_start => 15081,
                                pattern_end => 15090})
            end,
            Old_r@1 = case gleam_stdlib:map_get(Letters, R) of
                {ok, Old_r} -> Old_r;
                _assert_fail@3 ->
                    erlang:error(#{gleam_error => let_assert,
                                message => <<"r always in range"/utf8>>,
                                file => <<?FILEPATH/utf8>>,
                                module => <<"sqid"/utf8>>,
                                function => <<"shuffle_alphabet_loop"/utf8>>,
                                line => 440,
                                value => _assert_fail@3,
                                start => 15143,
                                'end' => 15186,
                                pattern_start => 15154,
                                pattern_end => 15163})
            end,
            Letters@1 = begin
                _pipe = gleam@dict:insert(Letters, I, Old_r@1),
                gleam@dict:insert(_pipe, R, Old_i@1)
            end,
            shuffle_alphabet_loop(Letters@1, I + 1, J - 1)
    end.

-file("src/sqid.gleam", 421).
?DOC(
    " Shuffles an alphabet.\n"
    " Don't be fooled by the name: the shuffle might look random, but it's\n"
    " actually deterministic. Given an alphabet its shuffled version is always\n"
    " going to be the same.\n"
).
-spec shuffle_alphabet(alphabet()) -> alphabet().
shuffle_alphabet(Alphabet) ->
    Letters = shuffle_alphabet_loop(
        erlang:element(2, Alphabet),
        0,
        maps:size(erlang:element(2, Alphabet)) - 1
    ),
    {alphabet, Letters}.

-file("src/sqid.gleam", 96).
-spec is_multi_byte(binary()) -> boolean().
is_multi_byte(String) ->
    erlang:byte_size(String) > 1.

-file("src/sqid.gleam", 104).
-spec has_duplicates_loop(list(DKX), gleam@set:set(DKX)) -> boolean().
has_duplicates_loop(List, Seen_so_far) ->
    case List of
        [] ->
            false;

        [First | Rest] ->
            case gleam@set:contains(Seen_so_far, First) of
                true ->
                    true;

                false ->
                    has_duplicates_loop(
                        Rest,
                        gleam@set:insert(Seen_so_far, First)
                    )
            end
    end.

-file("src/sqid.gleam", 100).
-spec has_duplicates(list(any())) -> boolean().
has_duplicates(List) ->
    has_duplicates_loop(List, gleam@set:new()).

-file("src/sqid.gleam", 79).
?DOC(
    " Creates a new alphabet from the letters making up the given string.\n"
    " An alphabet is what defines what characters can end up in a generated Sqid.\n"
    "\n"
    " This function will fail if:\n"
    " - The alphabet has less than 3 characters\n"
    " - The alphabet contains repeated characters\n"
    " - The alphabet has multi-byte characters\n"
    "\n"
    " ## Examples\n"
    "\n"
    " If you want Sqids that only contains lowercase letters you can define an\n"
    " alphabet like this:\n"
    "\n"
    " ```gleam\n"
    " let assert Ok(alphabet) =\n"
    "   sqid.alphabet(\"abcdefghijklmnopqrstuvwxyz\")\n"
    " ```\n"
    "\n"
    " ```gleam\n"
    " assert Error(Nil) == sqid.alphabet(\"wibble\")\n"
    "   as \"wrong alphabet: it contains duplicates\"\n"
    "\n"
    " assert Error(Nil) == sqid.alphabet(\"dziękuję\")\n"
    "   as \"wrong alphabet: it contains non ascii characters\"\n"
    "\n"
    " assert Error(Nil) == sqid.alphabet(\"01\")\n"
    "   as \"wrong alphabet: it contains less than 3 characters\"\n"
    " ```\n"
).
-spec alphabet(binary()) -> {ok, alphabet()} | {error, nil}.
alphabet(String) ->
    Letters = gleam@string:to_graphemes(String),
    gleam@bool:guard(
        has_duplicates(Letters),
        {error, nil},
        fun() ->
            gleam@bool:guard(
                gleam@list:any(Letters, fun is_multi_byte/1),
                {error, nil},
                fun() -> case Letters of
                        [] ->
                            {error, nil};

                        [_] ->
                            {error, nil};

                        [_, _] ->
                            {error, nil};

                        _ ->
                            Letters@1 = gleam@list:index_fold(
                                Letters,
                                maps:new(),
                                fun(Acc, Letter, Index) ->
                                    gleam@dict:insert(Acc, Index, Letter)
                                end
                            ),
                            {ok, shuffle_alphabet({alphabet, Letters@1})}
                    end end
            )
        end
    ).

-file("src/sqid.gleam", 124).
?DOC(
    " Creates an `Options` object from a valid alphabet.\n"
    " `Options` determine how Sqids are encoded and decoded.\n"
    "\n"
    " If you want to change the generated Sqids you can also use:\n"
    " - [`set_minimum_length`](#set_minimum_length): to pick a minimum length for\n"
    "   the generated Sqids.\n"
    " - [`set_blocklist`](#set_blocklist): to specify a blocklist of words that\n"
    "   will not end up in any of the generated Sqids.\n"
).
-spec new(alphabet()) -> options().
new(Alphabet) ->
    {options, Alphabet, 0, []}.

-file("src/sqid.gleam", 132).
?DOC(
    " Sets the minimum length of the generated Sqids.\n"
    " The value must be between 0 and 255. Any value lower than 0 is considered\n"
    " as 0, while any value higher than 255 is considered as 255.\n"
).
-spec set_minimum_length(options(), integer()) -> options().
set_minimum_length(Options, Minimum_length) ->
    {options,
        erlang:element(2, Options),
        gleam@int:clamp(Minimum_length, 0, 255),
        erlang:element(4, Options)}.

-file("src/sqid.gleam", 459).
-spec alphabet_to_string(alphabet()) -> binary().
alphabet_to_string(Alphabet) ->
    _pipe = maps:to_list(erlang:element(2, Alphabet)),
    _pipe@1 = gleam@list:sort(
        _pipe,
        fun(One, Other) ->
            gleam@int:compare(erlang:element(1, One), erlang:element(1, Other))
        end
    ),
    _pipe@2 = gleam@list:map(_pipe@1, fun(Pair) -> erlang:element(2, Pair) end),
    gleam@string:join(_pipe@2, <<""/utf8>>).

-file("src/sqid.gleam", 151).
?DOC(
    " Sets the list of blocked words that cannot appear inside any of the\n"
    " generated Sqids.\n"
    " This is useful if you want to make sure a generated Sqid cannot contain\n"
    " profanities.\n"
    "\n"
    " There's a couple of things to note:\n"
    " - The blocklist is case insensitive. If `\"gleam\"` is a blocked word,\n"
    "   generated Sqids will not contain any of `\"gLeaM\"`, `\"GLEAM\"`, `\"GLeam\"`,\n"
    "   ... and so on.\n"
    " - All words in the blocklist must be more than three characters long, any\n"
    "   word shorter than that will be ignored.\n"
).
-spec set_blocklist(options(), list(binary())) -> options().
set_blocklist(Options, Blocklist) ->
    Alphabet_string = alphabet_to_string(erlang:element(2, Options)),
    Blocklist@2 = gleam@list:fold(
        Blocklist,
        [],
        fun(Blocklist@1, Word) ->
            Word@1 = string:lowercase(Word),
            case gleam@string:to_graphemes(Word@1) of
                [] ->
                    Blocklist@1;

                [_] ->
                    Blocklist@1;

                [_, _] ->
                    Blocklist@1;

                Letters ->
                    case gleam@list:all(
                        Letters,
                        fun(_capture) ->
                            gleam_stdlib:contains_string(
                                Alphabet_string,
                                _capture
                            )
                        end
                    ) of
                        true ->
                            [Word@1 | Blocklist@1];

                        false ->
                            Blocklist@1
                    end
            end
        end
    ),
    {options,
        erlang:element(2, Options),
        erlang:element(3, Options),
        Blocklist@2}.

-file("src/sqid.gleam", 228).
?DOC(
    " Returns true if the id contains any of the words that are in the options'\n"
    " block list.\n"
    " Checking is case insensitive: if `\"gleam\"` is not allowed then `\"GLeaM\"`\n"
    " would be rejected as well.\n"
).
-spec is_blocked_id(binary(), options()) -> boolean().
is_blocked_id(Id, Options) ->
    Id@1 = string:lowercase(Id),
    Id_size = erlang:byte_size(Id@1),
    gleam@list:any(
        erlang:element(4, Options),
        fun(Blocked_word) ->
            Blocked_word_size = erlang:byte_size(Blocked_word),
            case Blocked_word_size > Id_size of
                true ->
                    false;

                false when (Id_size =< 3) orelse (Blocked_word_size =< 3) ->
                    Id@1 =:= Blocked_word;

                false ->
                    gleam_stdlib:contains_string(Id@1, Blocked_word)
            end
        end
    ).

-file("src/sqid.gleam", 310).
-spec pad_to_length_loop(binary(), integer(), alphabet()) -> binary().
pad_to_length_loop(Id, Missing, Alphabet) ->
    case Missing > 0 of
        false ->
            Id;

        true ->
            Alphabet@1 = shuffle_alphabet(Alphabet),
            Alphabet_size = maps:size(erlang:element(2, Alphabet@1)),
            Slice_size = gleam@int:min(Missing, Alphabet_size),
            Slice = gleam@string:slice(
                alphabet_to_string(Alphabet@1),
                0,
                Slice_size
            ),
            Id@1 = <<Id/binary, Slice/binary>>,
            Missing@1 = Missing - Slice_size,
            pad_to_length_loop(Id@1, Missing@1, Alphabet@1)
    end.

-file("src/sqid.gleam", 327).
?DOC(
    " Returns the separator of the alphabet, the separator changes based on how\n"
    " the\n"
).
-spec alphabet_separator(alphabet()) -> binary().
alphabet_separator(Alphabet) ->
    Separator@1 = case gleam_stdlib:map_get(erlang:element(2, Alphabet), 0) of
        {ok, Separator} -> Separator;
        _assert_fail ->
            erlang:error(#{gleam_error => let_assert,
                        message => <<"empty alphabet"/utf8>>,
                        file => <<?FILEPATH/utf8>>,
                        module => <<"sqid"/utf8>>,
                        function => <<"alphabet_separator"/utf8>>,
                        line => 328,
                        value => _assert_fail,
                        start => 11196,
                        'end' => 11252,
                        pattern_start => 11207,
                        pattern_end => 11220})
    end,
    Separator@1.

-file("src/sqid.gleam", 298).
-spec pad_to_length(binary(), integer(), alphabet()) -> binary().
pad_to_length(Id, Length, Alphabet) ->
    Id_length = erlang:byte_size(Id),
    case Id_length < Length of
        false ->
            Id;

        true ->
            Id@1 = <<Id/binary, (alphabet_separator(Alphabet))/binary>>,
            Missing = (Length - Id_length) - 1,
            pad_to_length_loop(Id@1, Missing, Alphabet)
    end.

-file("src/sqid.gleam", 271).
-spec number_to_id_loop(integer(), alphabet(), list(binary())) -> binary().
number_to_id_loop(Number, Alphabet, Acc) ->
    Alphabet_size = maps:size(erlang:element(2, Alphabet)) - 1,
    Letter@1 = case gleam_stdlib:map_get(
        erlang:element(2, Alphabet),
        (case Alphabet_size of
            0 -> 0;
            Gleam@denominator -> Number rem Gleam@denominator
        end) + 1
    ) of
        {ok, Letter} -> Letter;
        _assert_fail ->
            erlang:error(#{gleam_error => let_assert,
                        message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
                        file => <<?FILEPATH/utf8>>,
                        module => <<"sqid"/utf8>>,
                        function => <<"number_to_id_loop"/utf8>>,
                        line => 281,
                        value => _assert_fail,
                        start => 9723,
                        'end' => 9809,
                        pattern_start => 9734,
                        pattern_end => 9744})
    end,
    Acc@1 = [Letter@1 | Acc],
    Number@1 = begin
        _pipe = math:floor(case erlang:float(Alphabet_size) of
                +0.0 -> +0.0;
                -0.0 -> -0.0;
                Gleam@denominator@1 -> erlang:float(Number) / Gleam@denominator@1
            end),
        erlang:round(_pipe)
    end,
    case Number@1 > 0 of
        true ->
            number_to_id_loop(Number@1, Alphabet, Acc@1);

        false ->
            gleam@string:join(Acc@1, <<""/utf8>>)
    end.

-file("src/sqid.gleam", 267).
-spec number_to_id(integer(), alphabet()) -> binary().
number_to_id(Number, Alphabet) ->
    number_to_id_loop(Number, Alphabet, []).

-file("src/sqid.gleam", 247).
-spec encode_numbers(list(integer()), alphabet(), binary()) -> {binary(),
    alphabet()}.
encode_numbers(Numbers, Alphabet, Id) ->
    case Numbers of
        [] ->
            {Id, Alphabet};

        [Last] ->
            {<<Id/binary, (number_to_id(Last, Alphabet))/binary>>, Alphabet};

        [First | Rest] ->
            Id@1 = <<Id/binary, (number_to_id(First, Alphabet))/binary>>,
            Id@2 = <<Id@1/binary, (alphabet_separator(Alphabet))/binary>>,
            Alphabet@1 = shuffle_alphabet(Alphabet),
            encode_numbers(Rest, Alphabet@1, Id@2)
    end.

-file("src/sqid.gleam", 449).
?DOC(
    " This reverses an alphabet: if the initial alphabet is \"abcd\", the reversed\n"
    " alphabet is going to be \"dcba\".\n"
).
-spec reverse_alphabet(alphabet()) -> alphabet().
reverse_alphabet(Alphabet) ->
    Size = maps:size(erlang:element(2, Alphabet)),
    Letters@1 = gleam@dict:fold(
        erlang:element(2, Alphabet),
        maps:new(),
        fun(Letters, Index, Letter) ->
            gleam@dict:insert(Letters, (Size - 1) - Index, Letter)
        end
    ),
    {alphabet, Letters@1}.

-file("src/sqid.gleam", 336).
?DOC(
    " Shifts the alphabet by the given offset.\n"
    " For example if alphabet is `\"abcde\"` and I end up shifting it by 2 to the\n"
    " left it ends up being `\"cdeab\"`.\n"
).
-spec shift_alphabet(alphabet(), integer()) -> alphabet().
shift_alphabet(Alphabet, Offset) ->
    Alphabet_size = maps:size(erlang:element(2, Alphabet)),
    Letters@1 = gleam@dict:fold(
        erlang:element(2, Alphabet),
        maps:new(),
        fun(Letters, Index, Letter) ->
            New_index = case Index < Offset of
                true ->
                    (Alphabet_size - Offset) + Index;

                false ->
                    Index - Offset
            end,
            gleam@dict:insert(Letters, New_index, Letter)
        end
    ),
    {alphabet, Letters@1}.

-file("src/sqid.gleam", 376).
?DOC(
    " This computes an offset from a list of numbers, the Sqid spec calls this\n"
    " \"semi random\": it's random looking but it's actually deterministically\n"
    " decided by the numbers and alphabet we pass as arguments.\n"
    " This returns an error if any of the numbers is negative.\n"
).
-spec semi_random_offset(alphabet(), list(integer())) -> {ok, integer()} |
    {error, nil}.
semi_random_offset(Alphabet, Numbers) ->
    Alphabet_size = maps:size(erlang:element(2, Alphabet)),
    _pipe = gleam@list:try_fold(
        Numbers,
        {erlang:length(Numbers), 0},
        fun(Pair, Number) ->
            {Acc, Index} = Pair,
            case Number < 0 of
                true ->
                    {error, nil};

                false ->
                    Letter_value@1 = case letter_value(
                        erlang:element(2, Alphabet),
                        case Alphabet_size of
                            0 -> 0;
                            Gleam@denominator -> Number rem Gleam@denominator
                        end
                    ) of
                        {ok, Letter_value} -> Letter_value;
                        _assert_fail ->
                            erlang:error(#{gleam_error => let_assert,
                                        message => <<"index outside of dictionary"/utf8>>,
                                        file => <<?FILEPATH/utf8>>,
                                        module => <<"sqid"/utf8>>,
                                        function => <<"semi_random_offset"/utf8>>,
                                        line => 386,
                                        value => _assert_fail,
                                        start => 13405,
                                        'end' => 13499,
                                        pattern_start => 13416,
                                        pattern_end => 13432})
                    end,
                    {ok, {(Letter_value@1 + Index) + Acc, Index + 1}}
            end
        end
    ),
    gleam@result:map(_pipe, fun(Pair@1) -> erlang:element(1, Pair@1) end).

-file("src/sqid.gleam", 185).
-spec do_encode(options(), list(integer()), integer()) -> {ok, binary()} |
    {error, nil}.
do_encode(Options, Numbers, Attempts) ->
    case Attempts >= maps:size(erlang:element(2, erlang:element(2, Options))) of
        true ->
            {error, nil};

        false ->
            Alphabet_size = maps:size(
                erlang:element(2, erlang:element(2, Options))
            ),
            gleam@result:'try'(
                semi_random_offset(erlang:element(2, Options), Numbers),
                fun(Offset) ->
                    Offset@1 = case Alphabet_size of
                        0 -> 0;
                        Gleam@denominator -> Offset rem Gleam@denominator
                    end,
                    Offset@2 = case Alphabet_size of
                        0 -> 0;
                        Gleam@denominator@1 -> (Offset@1 + Attempts) rem Gleam@denominator@1
                    end,
                    Alphabet = shift_alphabet(
                        erlang:element(2, Options),
                        Offset@2
                    ),
                    Id = alphabet_separator(Alphabet),
                    Alphabet@1 = reverse_alphabet(Alphabet),
                    {Id@1, Alphabet@2} = encode_numbers(Numbers, Alphabet@1, Id),
                    Id@2 = pad_to_length(
                        Id@1,
                        erlang:element(3, Options),
                        Alphabet@2
                    ),
                    case is_blocked_id(Id@2, Options) of
                        true ->
                            do_encode(Options, Numbers, Attempts + 1);

                        false ->
                            {ok, Id@2}
                    end
                end
            )
    end.

-file("src/sqid.gleam", 178).
?DOC(
    " Encodes a list of numbers into a readable string using the given options.\n"
    " This function might fail if it's not possible to generate a string that is\n"
    " not excluded by the options' blocklist.\n"
).
-spec encode(options(), list(integer())) -> {ok, binary()} | {error, nil}.
encode(Options, Numbers) ->
    case Numbers of
        [] ->
            {ok, <<""/utf8>>};

        _ ->
            do_encode(Options, Numbers, 0)
    end.

-file("src/sqid.gleam", 528).
-spec index(binary(), binary()) -> {ok, integer()} | {error, nil}.
index(String, Separator) ->
    gleam@result:'try'(
        gleam@string:split_once(String, Separator),
        fun(_use0) ->
            {Before, _} = _use0,
            {ok, erlang:byte_size(Before)}
        end
    ).

-file("src/sqid.gleam", 518).
-spec id_to_number(binary(), alphabet()) -> {ok, integer()} | {error, nil}.
id_to_number(Id, Alphabet) ->
    Alphabet_size = maps:size(erlang:element(2, Alphabet)) - 1,
    Alphabet_string = alphabet_to_string(Alphabet),
    gleam@list:try_fold(
        gleam@string:to_graphemes(Id),
        0,
        fun(Acc, Letter) ->
            gleam@result:'try'(
                index(Alphabet_string, Letter),
                fun(Index) -> {ok, ((Acc * Alphabet_size) + Index) - 1} end
            )
        end
    ).

-file("src/sqid.gleam", 494).
-spec decode_loop(alphabet(), binary(), list(integer())) -> {ok,
        list(integer())} |
    {error, nil}.
decode_loop(Alphabet, Sqid, Numbers) ->
    Separator = alphabet_separator(Alphabet),
    case gleam@string:split(Sqid, Separator) of
        [] ->
            {ok, lists:reverse(Numbers)};

        [<<""/utf8>> | _] ->
            {ok, lists:reverse(Numbers)};

        [Id | Rest] ->
            case id_to_number(Id, Alphabet) of
                {error, _} ->
                    {error, nil};

                {ok, Number} ->
                    Numbers@1 = [Number | Numbers],
                    Alphabet@1 = shuffle_alphabet(Alphabet),
                    Sqid@1 = gleam@string:join(Rest, Separator),
                    decode_loop(Alphabet@1, Sqid@1, Numbers@1)
            end
    end.

-file("src/sqid.gleam", 475).
?DOC(
    " Decodes a Sqid into the list of integers that was used to generate it.\n"
    " To decode a Sqid, always use the same options that were used to encode it,\n"
    " otherwise you will get nonsense results!\n"
    "\n"
    " This function will fail if the Sqid has carachters that are not allowed by\n"
    " these options, meaning that the Sqid was encoded with a different alphabet.\n"
).
-spec decode(options(), binary()) -> {ok, list(integer())} | {error, nil}.
decode(Options, Sqid) ->
    case gleam_stdlib:string_pop_grapheme(Sqid) of
        {error, _} ->
            {ok, []};

        {ok, {Separator, Sqid@1}} ->
            case index(
                alphabet_to_string(erlang:element(2, Options)),
                Separator
            ) of
                {error, _} ->
                    {error, nil};

                {ok, Offset} ->
                    _pipe = shift_alphabet(erlang:element(2, Options), Offset),
                    _pipe@1 = reverse_alphabet(_pipe),
                    decode_loop(_pipe@1, Sqid@1, [])
            end
    end.