Current section

Files

Jump to
brcpfcnpj lib cpfcnpj.ex
Raw

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/(\d{2})?(\d{3})?(\d{3})?(\d{4})?(\d{2})/
@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(tp_cpfcnpj) do
cpfcnpj = String.replace(elem(tp_cpfcnpj, 1), ~r/[^0-9]/, "")
first_char_valid = character_valid(cpfcnpj, {elem(tp_cpfcnpj, 0), :first})
second_char_valid = character_valid(cpfcnpj, {elem(tp_cpfcnpj, 0), :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
String.to_integer(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} -> String.to_integer(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
array =
case valid_type do
{:cpf, :first} -> @cpf_algs_1
{:cnpj, :first} -> @cnpj_algs_1
{:cpf, :second} -> @cpf_algs_2
{:cnpj, :second} -> @cnpj_algs_2
end
mult_sum(array, 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(number_in) do
if valid?(number_in) do
tp_cpfcnpj = {elem(number_in, 0), String.replace(elem(number_in, 1), ~r/[^0-9]/, "")}
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_numbers(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
defp random_numbers(tp_cpfcnpj) do
random_digit_generator = fn -> Enum.random(0..9) end
random_digit_generator
|> Stream.repeatedly()
|> Enum.take(if(tp_cpfcnpj == :cpf, do: @cpf_length, else: @cnpj_length) - 2)
|> Enum.join()
end
end