Current section
Files
Jump to
Current section
Files
lib/cpfcnpj.ex
defmodule Cpfcnpj do
@moduledoc """
Módulo responsável por realizar todos os cálculos de validação.
## Examples
iex>Cpfcnpj.valid?({:cnpj,"69.103.604/0001-60"})
true
iex>Cpfcnpj.valid?({:cpf,"111.444.777-35"})
true
Com ou sem os caracteres especiais os mesmos serão validados
"""
@division 11
@cpf_length 11
@cpf_algs_1 [10, 9, 8, 7, 6, 5, 4, 3, 2, 0, 0]
@cpf_algs_2 [11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 0]
@cpf_regex ~r/(\d{3})?(\d{3})?(\d{3})?(\d{2})/
@cnpj_length 14
@cnpj_algs_1 [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2, 0, 0]
@cnpj_algs_2 [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2, 0]
@cnpj_regex ~r/([0-9A-Z]{2})[.]?([0-9A-Z]{3})[.]?([0-9A-Z]{3})[\/]?([0-9A-Z]{4})[-]?([0-9A-Z]{2})/
@cnpj_characters 48..57
|> Enum.to_list()
|> Kernel.++(Enum.to_list(65..90))
|> Enum.map(&List.wrap(&1))
@cnpj_character_to_value_map %{
"0" => 0,
"1" => 1,
"2" => 2,
"3" => 3,
"4" => 4,
"5" => 5,
"6" => 6,
"7" => 7,
"8" => 8,
"9" => 9,
"A" => 17,
"B" => 18,
"C" => 19,
"D" => 20,
"E" => 21,
"F" => 22,
"G" => 23,
"H" => 24,
"I" => 25,
"J" => 26,
"K" => 27,
"L" => 28,
"M" => 29,
"N" => 30,
"O" => 31,
"P" => 32,
"Q" => 33,
"R" => 34,
"S" => 35,
"T" => 36,
"U" => 37,
"V" => 38,
"W" => 39,
"X" => 40,
"Y" => 41,
"Z" => 42
}
@doc """
Valida cpf/cnpj caracteres especias não são levados em consideração.
## Examples
iex>Cpfcnpj.valid?({:cnpj,"69.103.604/0001-60"})
true
"""
@spec valid?({:cpf | :cnpj, String.t()}) :: boolean()
def valid?(number_in) do
check_number(number_in) and type_checker(number_in) and special_checker(number_in)
end
defp check_number({_, nil}) do
false
end
# Checks length and if all are equal
defp check_number(tp_cpfcnpj) do
cpfcnpj = String.replace(elem(tp_cpfcnpj, 1), ~r/[\.\/-]/, "")
all_equal? =
cpfcnpj
|> String.replace(String.at(cpfcnpj, 0), "")
|> String.length()
|> Kernel.==(0)
correct_length? =
case tp_cpfcnpj do
{:cpf, _} ->
String.length(cpfcnpj) == @cpf_length
{:cnpj, _} ->
String.length(cpfcnpj) == @cnpj_length
end
correct_length? and not all_equal?
end
# Checks validation digits
defp type_checker({type, string}) do
cpfcnpj = replace_invalid_characters(type, string)
first_char_valid = character_valid(cpfcnpj, {type, :first})
second_char_valid = character_valid(cpfcnpj, {type, :second})
verif = first_char_valid <> second_char_valid
verif == String.slice(cpfcnpj, -2, 2)
end
# Checks special cases
defp special_checker({:cpf, _}) do
true
end
defp special_checker(tp_cpfcnpj = {:cnpj, _}) do
cnpj = String.replace(elem(tp_cpfcnpj, 1), ~r/[\.\/-]/, "")
order = String.slice(cnpj, 8..11)
first_three_digits = String.slice(cnpj, 0..2)
basic = String.slice(cnpj, 0..7)
cond do
order == "0000" ->
false
cnpj_alphanumeric_translation(order) > 300 and first_three_digits == "000" and
basic != "00000000" ->
false
true ->
true
end
end
defp mult_sum(algs, cpfcnpj) do
mult =
cpfcnpj
|> String.codepoints()
|> Enum.with_index()
|> Enum.map(fn {k, v} -> cnpj_alphanumeric_translation(k) * Enum.at(algs, v) end)
Enum.reduce(mult, 0, &+/2)
end
defp character_calc(remainder) do
if remainder < 2, do: 0, else: @division - remainder
end
defp character_valid(cpfcnpj, valid_type) do
valid_type
|> case do
{:cpf, :first} -> @cpf_algs_1
{:cnpj, :first} -> @cnpj_algs_1
{:cpf, :second} -> @cpf_algs_2
{:cnpj, :second} -> @cnpj_algs_2
end
|> mult_sum(cpfcnpj)
|> rem(@division)
|> character_calc()
|> Integer.to_string()
end
@doc """
Valida o Cpf/Cnpj e retorna uma String com o mesmo formatado.
Caso seja inválido retorna `nil`
## Examples
iex> Cpfcnpj.format_number({:cnpj,"69.103.604/0001-60"})
"69.103.604/0001-60"
"""
@spec format_number({:cpf | :cnpj, String.t()}) :: String.t() | nil
def format_number({type, number}) do
if valid?({type, number}) do
tp_cpfcnpj = {type, String.replace(number, ~r/[^0-9A-Z]/, "")}
case tp_cpfcnpj do
{:cpf, cpf} ->
Regex.replace(@cpf_regex, cpf, "\\1.\\2.\\3-\\4")
{:cnpj, cnpj} ->
Regex.replace(@cnpj_regex, cnpj, "\\1.\\2.\\3/\\4-\\5")
end
else
nil
end
end
@doc """
Gerador de cpf/cnpj concatenado com o dígito verificador.
"""
@spec generate(:cpf | :cnpj) :: String.t()
def generate(tp_cpfcnpj) do
numbers = random_characters(tp_cpfcnpj)
first_valid_char = character_valid(numbers, {tp_cpfcnpj, :first})
second_valid_char = character_valid(numbers <> first_valid_char, {tp_cpfcnpj, :second})
result = numbers <> first_valid_char <> second_valid_char
# Chance de gerar um inválido seguindo esse algoritmo é baixa o suficiente que
# vale a pena simplesmente retentar caso o resultado for inválido
if valid?({tp_cpfcnpj, result}) do
result
else
generate(tp_cpfcnpj)
end
end
def random_characters(:cpf) do
random_digit_generator = fn -> Enum.random(0..9) end
random_digit_generator
|> Stream.repeatedly()
|> Enum.take(@cpf_length - 2)
|> Enum.join()
end
def random_characters(:cnpj) do
random_digit_generator = fn ->
Enum.random(@cnpj_characters)
end
random_digit_generator
|> Stream.repeatedly()
|> Enum.take(@cnpj_length - 2)
|> Enum.join()
end
# Cnpj pode ter caracteres alfanuméricos, cpf não
defp replace_invalid_characters(:cnpj, cnpj) do
String.replace(cnpj, ~r/[^a-zA-Z0-9]/, "")
end
defp replace_invalid_characters(:cpf, cpf) do
String.replace(cpf, ~r/[^0-9]/, "")
end
defp cnpj_alphanumeric_translation(string) do
case String.length(string) do
1 ->
Map.get(@cnpj_character_to_value_map, String.upcase(string, :ascii))
_other ->
string
|> String.codepoints()
|> Enum.map(&cnpj_alphanumeric_translation/1)
end
end
end