%% @doc Utilities for Brazilian passport numbers. %% %% A passport number has 8 characters: 2 uppercase letters followed by %% 6 digits (e.g. `<<"AB123456">>'). There is no check digit — only %% the shape is verified, never existence. %% %% All functions operate on UTF-8 binaries. -module(brutils_passport). -export([remove_symbols/1, is_valid/1, format/1, generate/0]). -type passport() :: <<_:64>>. %% A passport number: 2 uppercase ASCII letters + 6 ASCII digits. -export_type([passport/0]). %% @doc Removes the symbols `-', `.' and spaces from a passport %% string. %% %% Only those three characters are removed; anything else — including %% underscores and slashes — is kept unchanged, and letter case is %% preserved (uppercasing is {@link format/1}'s job). This is a pure %% character filter: it does not validate the input. %% %% ``` %% 1> brutils_passport:remove_symbols(<<"Ab -. 123456">>). %% <<"Ab123456">> %% 2> brutils_passport:remove_symbols(<<"ab_123/456">>). %% <<"ab_123/456">> %% ''' -spec remove_symbols(binary()) -> binary(). remove_symbols(Passport) when is_binary(Passport) -> << <> || <> <= Passport, C =/= $-, C =/= $., C =/= $\s >>. %% @doc Returns whether the given term is a valid passport number: %% exactly 2 uppercase ASCII letters followed by exactly 6 digits. %% %% The check is case-sensitive — lowercase letters are invalid — and %% no symbols are stripped; use {@link format/1} to normalize dressed %% or lowercase input first. (The reference implementation's %% docstring claims mixed case is accepted; its code rejects it, and %% this port follows the code.) There is no check digit, so any %% shape-conforming value is valid — existence is never verified. The %% function is total: any non-binary term returns `false' rather %% than raising. %% %% ``` %% 1> brutils_passport:is_valid(<<"AB123456">>). %% true %% 2> brutils_passport:is_valid(<<"Ab123456">>). %% false %% ''' -spec is_valid(term()) -> boolean(). is_valid(<>) when L1 >= $A, L1 =< $Z, L2 >= $A, L2 =< $Z -> all_digits(Digits); is_valid(_) -> false. %% @doc Normalizes and formats a passport number for display: %% uppercases ASCII letters and strips the symbols `-', `.' and %% spaces, then validates the result. %% %% This is the lenient counterpart to the strict {@link is_valid/1}: %% `format(<<"ab-123456">>)' succeeds where %% `is_valid(<<"ab-123456">>)' is `false'. Input that does not %% normalize into a valid passport — including non-ASCII letters — %% yields `{error, invalid}'. %% %% ``` %% 1> brutils_passport:format(<<"ab-123456">>). %% {ok,<<"AB123456">>} %% 2> brutils_passport:format(<<"111111">>). %% {error,invalid} %% ''' -spec format(binary()) -> {ok, passport()} | {error, invalid}. format(Passport) when is_binary(Passport) -> Normalized = remove_symbols(ascii_uppercase(Passport)), case is_valid(Normalized) of true -> {ok, Normalized}; false -> {error, invalid} end. %% @doc Generates a random valid passport number: 2 uniform uppercase %% letters followed by 6 uniform digits. %% %% With no check digit to compute, every output is valid by %% construction. %% %% ``` %% 1> brutils_passport:generate(). %% <<"HA029151">> %% ''' -spec generate() -> passport(). generate() -> <<(upper_char()), (upper_char()), (digits(6))/binary>>. %%-------------------------------------------------------------------- %% Internal %%-------------------------------------------------------------------- -spec upper_char() -> byte(). upper_char() -> $A + rand:uniform(26) - 1. -spec digits(pos_integer()) -> binary(). digits(N) -> << <<($0 + rand:uniform(10) - 1)>> || _ <- lists:seq(1, N) >>. %% Uppercase ASCII letters only; every other byte is left unchanged %% (multibyte input therefore simply fails the later validation). -spec ascii_uppercase(binary()) -> binary(). ascii_uppercase(Bin) -> << <<(upcase(C))>> || <> <= Bin >>. -spec upcase(byte()) -> byte(). upcase(C) when C >= $a, C =< $z -> C - 32; upcase(C) -> C. -spec all_digits(binary()) -> boolean(). all_digits(<>) when C >= $0, C =< $9 -> all_digits(Rest); all_digits(<<>>) -> true; all_digits(_) -> false.