Packages

Base62 encoding and decoding for Gleam

Current section

Files

Jump to
sixtytwo src sixtytwo.erl
Raw

src/sixtytwo.erl

-module(sixtytwo).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/sixtytwo.gleam").
-export([encode/1, decode/1]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Base62 encoding and decoding for Gleam, targeting both Erlang and JavaScript.\n"
"\n"
" Uses the standard `0-9A-Za-z` alphabet with Bitcoin/base-x style leading\n"
" zero preservation, compatible with `cryptocoinjs/base-x`.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" sixtytwo.encode(<<\"hello\":utf8>>)\n"
" // -> \"7tQLFHz\"\n"
"\n"
" sixtytwo.decode(\"7tQLFHz\")\n"
" // -> Ok(<<\"hello\":utf8>>)\n"
" ```\n"
).
-file("src/sixtytwo.gleam", 62).
-spec count_leading_zeros(bitstring(), integer()) -> integer().
count_leading_zeros(Input, Count) ->
case Input of
<<0, Rest/binary>> ->
count_leading_zeros(Rest, Count + 1);
_ ->
Count
end.
-file("src/sixtytwo.gleam", 69).
-spec count_leading_zero_values(list(integer())) -> integer().
count_leading_zero_values(Values) ->
case Values of
[0 | Rest] ->
1 + count_leading_zero_values(Rest);
_ ->
0
end.
-file("src/sixtytwo.gleam", 87).
-spec codepoint_to_index(integer()) -> {ok, integer()} | {error, nil}.
codepoint_to_index(Code) ->
case Code of
C when (C >= 48) andalso (C =< 57) ->
{ok, C - 48};
C@1 when (C@1 >= 65) andalso (C@1 =< 90) ->
{ok, (C@1 - 65) + 10};
C@2 when (C@2 >= 97) andalso (C@2 =< 122) ->
{ok, (C@2 - 97) + 36};
_ ->
{error, nil}
end.
-file("src/sixtytwo.gleam", 80).
-spec char_to_value(binary()) -> {ok, integer()} | {error, nil}.
char_to_value(Char) ->
case gleam@string:to_utf_codepoints(Char) of
[Cp] ->
codepoint_to_index(gleam_stdlib:identity(Cp));
_ ->
{error, nil}
end.
-file("src/sixtytwo.gleam", 123).
-spec do_multiply_and_add(list(integer()), integer(), integer(), integer()) -> {list(integer()),
integer()}.
do_multiply_and_add(Digits, Multiplier, Addend, Digit_base) ->
case Digits of
[] ->
{[], Addend};
[Digit | Rest] ->
{Processed_rest, Carry} = do_multiply_and_add(
Rest,
Multiplier,
Addend,
Digit_base
),
Value = (Digit * Multiplier) + Carry,
New_digit = case Digit_base of
0 -> 0;
Gleam@denominator -> Value rem Gleam@denominator
end,
New_carry = case Digit_base of
0 -> 0;
Gleam@denominator@1 -> Value div Gleam@denominator@1
end,
{[New_digit | Processed_rest], New_carry}
end.
-file("src/sixtytwo.gleam", 142).
-spec emit_carry(integer(), integer(), list(integer())) -> list(integer()).
emit_carry(Carry, Digit_base, Acc) ->
case Carry of
0 ->
Acc;
_ ->
Digit = case Digit_base of
0 -> 0;
Gleam@denominator -> Carry rem Gleam@denominator
end,
Remaining = case Digit_base of
0 -> 0;
Gleam@denominator@1 -> Carry div Gleam@denominator@1
end,
emit_carry(Remaining, Digit_base, [Digit | Acc])
end.
-file("src/sixtytwo.gleam", 112).
-spec multiply_and_add(list(integer()), integer(), integer(), integer()) -> list(integer()).
multiply_and_add(Digits, Multiplier, Addend, Digit_base) ->
{Result, Carry} = do_multiply_and_add(
Digits,
Multiplier,
Addend,
Digit_base
),
emit_carry(Carry, Digit_base, Result).
-file("src/sixtytwo.gleam", 76).
-spec index_to_char(integer()) -> binary().
index_to_char(Index) ->
gleam@string:slice(
<<"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"/utf8>>,
Index,
1
).
-file("src/sixtytwo.gleam", 96).
-spec bytes_to_base_digits(bitstring(), list(integer())) -> list(integer()).
bytes_to_base_digits(Input, Digits) ->
case Input of
<<Byte, Rest/binary>> ->
New_digits = multiply_and_add(Digits, 256, Byte, 62),
bytes_to_base_digits(Rest, New_digits);
_ ->
Digits
end.
-file("src/sixtytwo.gleam", 28).
?DOC(
" Encode a bit array using base62. The input must be byte-aligned.\n"
" Leading zero bytes are preserved as `0` characters in the output.\n"
).
-spec encode(bitstring()) -> binary().
encode(Input) ->
gleam@bool:guard(
erlang:byte_size(Input) =:= 0,
<<""/utf8>>,
fun() ->
Leading_zeros = count_leading_zeros(Input, 0),
Prefix = gleam@string:repeat(<<"0"/utf8>>, Leading_zeros),
Digits = bytes_to_base_digits(Input, []),
case Digits of
[] ->
Prefix;
_ ->
Encoded = begin
_pipe = gleam@list:map(Digits, fun index_to_char/1),
erlang:list_to_binary(_pipe)
end,
<<Prefix/binary, Encoded/binary>>
end
end
).
-file("src/sixtytwo.gleam", 106).
-spec base_digits_to_bytes(list(integer())) -> list(integer()).
base_digits_to_bytes(Digits) ->
gleam@list:fold(
Digits,
[],
fun(Bytes, Digit) -> multiply_and_add(Bytes, 62, Digit, 256) end
).
-file("src/sixtytwo.gleam", 46).
?DOC(" Decode a base62 string. Returns `Error(Nil)` on invalid input.\n").
-spec decode(binary()) -> {ok, bitstring()} | {error, nil}.
decode(Input) ->
gleam@bool:guard(
Input =:= <<""/utf8>>,
{ok, <<>>},
fun() ->
Chars = gleam@string:to_graphemes(Input),
gleam@result:'try'(
gleam@list:try_map(Chars, fun char_to_value/1),
fun(Values) ->
Leading_zeros = count_leading_zero_values(Values),
Bytes = base_digits_to_bytes(Values),
Decoded = begin
_pipe = gleam@list:repeat(0, Leading_zeros),
_pipe@1 = lists:append(_pipe, Bytes),
_pipe@2 = gleam@list:map(_pipe@1, fun(B) -> <<B>> end),
gleam_stdlib:bit_array_concat(_pipe@2)
end,
{ok, Decoded}
end
)
end
).