%% @doc Utilities for Brazilian vehicle license plates. %% %% Two plate patterns exist: the old format — 3 letters followed by 4 %% digits (`"ABC1234"') — and the Mercosul format — 3 letters, a %% digit, a letter and 2 digits (`"ABC1D23"'). Validation accepts %% lowercase input; formatting and conversion emit uppercase. %% %% All functions operate on UTF-8 binaries. -module(brutils_license_plate). -export([remove_symbols/1, is_valid/1, is_valid/2, get_format/1, convert_to_mercosul/1, format/1, generate/0, generate/1]). -type plate_type() :: old_format | mercosul. %% The two plate patterns: `old_format' is `LLLNNNN', `mercosul' is %% `LLLNLNN'. -type plate() :: <<_:56>>. %% A raw plate: 7 characters in either pattern, e.g. `<<"ABC1D23">>'. -type formatted_plate() :: <<_:56>> | <<_:64>>. %% A display-formatted plate: Mercosul plates are bare %% (`<<"ABC1D23">>'), old-format plates carry a dash %% (`<<"ABC-1234">>'). -export_type([plate_type/0, plate/0, formatted_plate/0]). %% @doc Removes the dash (`-') from a license plate string. %% %% Only the dash is removed — the narrowest cleaning rule in this %% library: dots, spaces and letter case are all kept unchanged. This %% is a pure character filter: it does not validate the input. %% %% ``` %% 1> brutils_license_plate:remove_symbols(<<"ABC-123">>). %% <<"ABC123">> %% 2> brutils_license_plate:remove_symbols(<<"abc.12 3">>). %% <<"abc.12 3">> %% ''' -spec remove_symbols(binary()) -> binary(). remove_symbols(Plate) when is_binary(Plate) -> << <> || <> <= Plate, C =/= $- >>. %% @doc Returns whether the given term is a valid license plate of %% either pattern, old format or Mercosul. %% %% Surrounding ASCII whitespace is trimmed and letter case is %% ignored, so `<<" abc1234 ">>' is valid — but embedded symbols %% such as the display dash are not stripped. Existence is never %% verified. The function is total: any non-binary term returns %% `false' rather than raising. %% %% ``` %% 1> brutils_license_plate:is_valid(<<"ABC1234">>). %% true %% 2> brutils_license_plate:is_valid(<<"abc1d23">>). %% true %% ''' -spec is_valid(term()) -> boolean(). is_valid(Plate) -> is_valid(Plate, old_format) orelse is_valid(Plate, mercosul). %% @doc Returns whether the given term is a valid license plate of %% the given pattern: `old_format' (`LLLNNNN') or `mercosul' %% (`LLLNLNN'). %% %% Trimming and case rules are those of {@link is_valid/1}. The type %% must be one of the two atoms; anything else is out of contract and %% raises. %% %% ``` %% 1> brutils_license_plate:is_valid(<<"ABC1234">>, old_format). %% true %% 2> brutils_license_plate:is_valid(<<"ABC1234">>, mercosul). %% false %% ''' -spec is_valid(term(), plate_type()) -> boolean(). is_valid(Plate, old_format) when is_binary(Plate) -> case normalize(Plate) of <> when L1 >= $A, L1 =< $Z, L2 >= $A, L2 =< $Z, L3 >= $A, L3 =< $Z -> all_digits(Digits); _ -> false end; is_valid(_, old_format) -> false; is_valid(Plate, mercosul) when is_binary(Plate) -> case normalize(Plate) of <> when L1 >= $A, L1 =< $Z, L2 >= $A, L2 =< $Z, L3 >= $A, L3 =< $Z, D1 >= $0, D1 =< $9, L4 >= $A, L4 =< $Z, D2 >= $0, D2 =< $9, D3 >= $0, D3 =< $9 -> true; _ -> false end; is_valid(_, mercosul) -> false. %% @doc Detects the pattern of a license plate: `{ok, old_format}' %% for `LLLNNNN', `{ok, mercosul}' for `LLLNLNN', or %% `{error, invalid}' when the input matches neither. %% %% Trimming and case rules are those of {@link is_valid/1}. The %% result composes with {@link is_valid/2}: a detected type always %% validates the plate it was detected from. (The reference %% implementation returns the pattern strings `"LLLNNNN"' / %% `"LLLNLNN"' instead — this port deliberately returns the %% `plate_type()' atoms.) %% %% ``` %% 1> brutils_license_plate:get_format(<<"abc1234">>). %% {ok,old_format} %% 2> brutils_license_plate:get_format(<<"ABC1D23">>). %% {ok,mercosul} %% ''' -spec get_format(binary()) -> {ok, plate_type()} | {error, invalid}. get_format(Plate) when is_binary(Plate) -> case {is_valid(Plate, old_format), is_valid(Plate, mercosul)} of {true, _} -> {ok, old_format}; {_, true} -> {ok, mercosul}; _ -> {error, invalid} end. %% @doc Converts an old-format plate (`LLLNNNN') to the Mercosul %% pattern (`LLLNLNN') by replacing the digit at position 5 with a %% letter: `0' becomes `A', `1' becomes `B', ... `9' becomes `J'. %% %% The input is normalized (trimmed, uppercased) first and must be a %% valid old-format plate — an already-Mercosul plate yields %% `{error, invalid}', not a no-op. (For whitespace-padded input the %% reference implementation converts the wrong position and keeps the %% padding, an artifact of slicing the unstripped string; this port %% deliberately normalizes first and converts correctly.) %% %% ``` %% 1> brutils_license_plate:convert_to_mercosul(<<"abc4567">>). %% {ok,<<"ABC4F67">>} %% 2> brutils_license_plate:convert_to_mercosul(<<"ABC1D23">>). %% {error,invalid} %% ''' -spec convert_to_mercosul(binary()) -> {ok, plate()} | {error, invalid}. convert_to_mercosul(Plate) when is_binary(Plate) -> case is_valid(Plate, old_format) of true -> <> = normalize(Plate), {ok, <>}; false -> {error, invalid} end. %% @doc Formats a valid license plate for display: old-format plates %% get a dash after the letters (`<<"ABC-1234">>'), Mercosul plates %% come out bare — both uppercased. %% %% The input is normalized (trimmed, uppercased) first. (For %% whitespace-padded input the reference implementation emits a %% mangled result — dash misplaced, padding kept — an artifact of %% slicing the unstripped string; this port deliberately normalizes %% first.) %% %% ``` %% 1> brutils_license_plate:format(<<"abc1234">>). %% {ok,<<"ABC-1234">>} %% 2> brutils_license_plate:format(<<"abc1e34">>). %% {ok,<<"ABC1E34">>} %% ''' -spec format(binary()) -> {ok, formatted_plate()} | {error, invalid}. format(Plate) when is_binary(Plate) -> case get_format(Plate) of {ok, old_format} -> <> = normalize(Plate), {ok, <>}; {ok, mercosul} -> {ok, normalize(Plate)}; {error, invalid} -> {error, invalid} end. %% @doc Generates a random valid Mercosul plate. %% %% Equivalent to `generate(<<"LLLNLNN">>)', but with no error case — %% the default pattern is always valid. %% %% ``` %% 1> brutils_license_plate:generate(). %% {ok,<<"BAR9B69">>} %% ''' -spec generate() -> {ok, plate()}. generate() -> {ok, from_pattern(<<"LLLNLNN">>)}. %% @doc Generates a random valid plate in the given pattern: %% `<<"LLLNNNN">>' (old format) or `<<"LLLNLNN">>' (Mercosul), case %% insensitive. Each `L' becomes a uniform uppercase letter and each %% `N' a uniform digit. %% %% ``` %% 1> brutils_license_plate:generate(<<"LLLNNNN">>). %% {ok,<<"KCM5068">>} %% 2> brutils_license_plate:generate(<<"XXXXXXX">>). %% {error,invalid} %% ''' -spec generate(binary()) -> {ok, plate()} | {error, invalid}. generate(Pattern) when is_binary(Pattern) -> case ascii_uppercase(Pattern) of Upper when Upper =:= <<"LLLNNNN">>; Upper =:= <<"LLLNLNN">> -> {ok, from_pattern(Upper)}; _ -> {error, invalid} end. %%-------------------------------------------------------------------- %% Internal %%-------------------------------------------------------------------- %% Realize a pattern: L -> random uppercase letter, N -> random digit. -spec from_pattern(binary()) -> plate(). from_pattern(Pattern) -> << <<(case C of $L -> $A + rand:uniform(26) - 1; $N -> $0 + rand:uniform(10) - 1 end)>> || <> <= Pattern >>. %% Trim surrounding ASCII whitespace and uppercase ASCII letters — %% the normalization every validating function applies first. -spec normalize(binary()) -> binary(). normalize(Plate) -> ascii_uppercase(trim(Plate)). %% The ASCII whitespace set the reference's strip() removes. -define(IS_WS(C), (C =:= $\s orelse C =:= $\t orelse C =:= $\n orelse C =:= $\r orelse C =:= $\f orelse C =:= $\v)). -spec trim(binary()) -> binary(). trim(<>) when ?IS_WS(C) -> trim(Rest); trim(Bin) -> trim_trailing(Bin). -spec trim_trailing(binary()) -> binary(). trim_trailing(Bin) when byte_size(Bin) > 0 -> case binary:last(Bin) of C when ?IS_WS(C) -> trim_trailing(binary:part(Bin, 0, byte_size(Bin) - 1)); _ -> Bin end; trim_trailing(Bin) -> Bin. -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.