%% @doc Utilities for Brazilian phone numbers. %% %% Numbers are handled without the +55 country code and with the %% two-digit DDD (area code) included. Two shapes exist: mobile %% numbers have 11 digits (DDD + `9' + 8 digits) and landline numbers %% have 10 (DDD + a digit from 2 to 5 + 7 digits). %% %% All functions operate on UTF-8 binaries. -module(brutils_phone). -export([remove_symbols/1, is_valid/1, is_valid/2, format/1, remove_international_dialing_code/1, generate/0, generate/1]). -type phone_type() :: mobile | landline. -export_type([phone_type/0]). %% @doc Removes common phone punctuation from a string: `(', `)', %% `-', `+' and spaces. %% %% Only those five characters are removed; anything else — including %% dots — is kept unchanged. This is a pure character filter: it does %% not validate the input. %% %% ``` %% 1> brutils_phone:remove_symbols(<<"+55 (11) 99402-9275">>). %% <<"5511994029275">> %% 2> brutils_phone:remove_symbols(<<"11.99402.9275">>). %% <<"11.99402.9275">> %% ''' -spec remove_symbols(binary()) -> binary(). remove_symbols(Phone) when is_binary(Phone) -> << <> || <> <= Phone, C =/= $(, C =/= $), C =/= $-, C =/= $+, C =/= $\s >>. %% @doc Returns whether the given term is a valid Brazilian phone %% number of either shape: mobile (11 digits) or landline (10). %% %% Equivalent to `is_valid(Phone, mobile) orelse %% is_valid(Phone, landline)'. It does not verify that the number %% actually exists. Formatting symbols are not stripped; clean the %% input with {@link remove_symbols/1} first. The function is total: %% any non-binary term returns `false' rather than raising. %% %% ``` %% 1> brutils_phone:is_valid(<<"11994029275">>). %% true %% 2> brutils_phone:is_valid(<<"1635014415">>). %% true %% ''' -spec is_valid(term()) -> boolean(). is_valid(Phone) -> is_valid(Phone, mobile) orelse is_valid(Phone, landline). %% @doc Returns whether the given term is a valid Brazilian phone %% number of the given type. %% %% Mobile numbers have 11 digits: a DDD of two digits from 1 to 9, %% then a `9', then 8 digits. Landline numbers have 10: the DDD, then %% a digit from 2 to 5, then 7 digits. %% %% The type must be the atom `mobile' or `landline'; anything else is %% out of contract and raises. (The reference implementation accepts %% any string as the type and silently falls back to checking both %% shapes — this port deliberately tightens that to the two atoms.) %% %% ``` %% 1> brutils_phone:is_valid(<<"11994029275">>, mobile). %% true %% 2> brutils_phone:is_valid(<<"11994029275">>, landline). %% false %% ''' -spec is_valid(term(), phone_type()) -> boolean(). is_valid(<>, mobile) when D1 >= $1, D1 =< $9, D2 >= $1, D2 =< $9, byte_size(Rest) =:= 8 -> all_digits(Rest); is_valid(_, mobile) -> false; is_valid(<>, landline) when D1 >= $1, D1 =< $9, D2 >= $1, D2 =< $9, D3 >= $2, D3 =< $5, byte_size(Rest) =:= 7 -> all_digits(Rest); is_valid(_, landline) -> false. %% @doc Formats a valid phone number for display: the DDD in %% parentheses (no space after them), then the subscriber number with %% a dash before the last four digits. %% %% Mobile numbers come out as `(DD)NNNNN-NNNN' and landlines as %% `(DD)NNNN-NNNN'. The input must be a raw, digits-only number %% accepted by {@link is_valid/1}; anything else yields %% `{error, invalid}'. %% %% ``` %% 1> brutils_phone:format(<<"11994029275">>). %% {ok,<<"(11)99402-9275">>} %% 2> brutils_phone:format(<<"1635014415">>). %% {ok,<<"(16)3501-4415">>} %% ''' -spec format(binary()) -> {ok, binary()} | {error, invalid}. format(Phone) when is_binary(Phone) -> case is_valid(Phone) of true -> %% 2 DDD digits in front, 4 digits after the dash; the %% head is whatever sits between (5 mobile, 4 landline) HeadLen = byte_size(Phone) - 6, <> = Phone, {ok, <<$(, Ddd/binary, $), Head/binary, $-, Tail/binary>>}; false -> {error, invalid} end. %% @doc Removes the Brazilian international dialing code (`55') from %% a phone number, faithfully porting the reference implementation's %% sharp edges. %% %% If the input contains the substring `55' anywhere and is longer %% than 11 characters once spaces are ignored, the FIRST occurrence %% of `55' is removed from the original string; otherwise the input %% is returned unchanged. Consequences to be aware of: %% %%
    %%
  • a leading `+' is kept: `<<"+5511994029275">>' becomes %% `<<"+11994029275">>';
  • %%
  • the removed `55' need not actually be a dialing code — the %% first occurrence goes, wherever it sits;
  • %%
  • spaces are ignored by the length check but preserved in the %% output.
  • %%
%% %% ``` %% 1> brutils_phone:remove_international_dialing_code(<<"5511994029275">>). %% <<"11994029275">> %% 2> brutils_phone:remove_international_dialing_code(<<"1635014415">>). %% <<"1635014415">> %% ''' -spec remove_international_dialing_code(binary()) -> binary(). remove_international_dialing_code(Phone) when is_binary(Phone) -> NoSpaces = << <> || <> <= Phone, C =/= $\s >>, case binary:match(Phone, <<"55">>) of {Pos, 2} when byte_size(NoSpaces) > 11 -> <> = Phone, <>; _ -> Phone end. %% @doc Generates a random valid phone number of a random type %% (mobile or landline, equal probability). %% %% ``` %% 1> brutils_phone:generate(). %% <<"4545670536">> %% ''' -spec generate() -> binary(). generate() -> case rand:uniform(2) of 1 -> generate(mobile); 2 -> generate(landline) end. %% @doc Generates a random valid phone number of the given type. %% %% Mobile: a DDD of two digits from 1 to 9, then `9', then 8 uniform %% digits. Landline: the DDD, then a digit from 2 to 5, then 7 %% uniform digits. The result always satisfies %% {@link is_valid/2} for the requested type. %% %% ``` %% 1> brutils_phone:generate(mobile). %% <<"89918945257">> %% 2> brutils_phone:generate(landline). %% <<"5747087233">> %% ''' -spec generate(phone_type()) -> binary(). generate(mobile) -> <<(ddd())/binary, $9, (digits(8))/binary>>; generate(landline) -> <<(ddd())/binary, ($1 + rand:uniform(4)), (digits(7))/binary>>. %%-------------------------------------------------------------------- %% Internal %%-------------------------------------------------------------------- %% Two DDD digits, each 1..9. -spec ddd() -> binary(). ddd() -> <<($0 + rand:uniform(9)), ($0 + rand:uniform(9))>>. -spec digits(pos_integer()) -> binary(). digits(N) -> << <<($0 + rand:uniform(10) - 1)>> || _ <- lists:seq(1, N) >>. -spec all_digits(binary()) -> boolean(). all_digits(<>) when C >= $0, C =< $9 -> all_digits(Rest); all_digits(<<>>) -> true; all_digits(_) -> false.