lib/brazilian_documents.ex

defmodule BrazilianDocuments do
  @moduledoc """
  Documentation for BrazilianDocuments.
  """

  @doc """
  Check if value is a valid CPF.

  ## Examples

      iex> BrazilianDocuments.valid_cpf?("366.418.768-70")
      true

      iex> BrazilianDocuments.valid_cpf?("36641876870")
      true

      iex> BrazilianDocuments.valid_cpf?("366.418")
      false

      iex> BrazilianDocuments.valid_cpf?("213.198.013-20")
      false

      iex> BrazilianDocuments.valid_cpf?("2131201872781")
      false

      iex> BrazilianDocuments.valid_cpf?("11111111111")
      false

  """
  @spec valid_cpf?(value :: String.t()) :: boolean()
  def valid_cpf?(value) when is_binary(value) do
    if Regex.match?(~r/^(\d{11}|\d{3}\.\d{3}\.\d{3}\-\d{2})$/, value) do
      numbers = to_numbers_list(value)

      not all_numbers_equal?(numbers) and
        valid_checker_digits?(numbers, 9, [11, 10, 9, 8, 7, 6, 5, 4, 3, 2])
    else
      false
    end
  end

  @doc """
  Check if value is a valid CNPJ.

  ## Examples

      iex> BrazilianDocuments.valid_cnpj?("69.103.604/0001-60")
      true

      iex> BrazilianDocuments.valid_cnpj?("41142260000189")
      true

      iex> BrazilianDocuments.valid_cnpj?("411407182")
      false

      iex> BrazilianDocuments.valid_cnpj?("11.111.111/1111-11")
      false

  """
  @spec valid_cnpj?(value :: String.t()) :: boolean()
  def valid_cnpj?(value) when is_binary(value) do
    if Regex.match?(~r/^(\d{14}|\d{2}\.\d{3}\.\d{3}\/\d{4}\-\d{2})$/, value) do
      numbers = to_numbers_list(value)

      not all_numbers_equal?(numbers) and
        valid_checker_digits?(numbers, 12, [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2])
    else
      false
    end
  end

  @doc """
  Format a CPF value.

  ## Examples

      iex> BrazilianDocuments.format_cpf("21987198433")
      {:ok, "219.871.984-33"}

      iex> BrazilianDocuments.format_cpf("219.871.984-33")
      {:ok, "219.871.984-33"}

      iex> BrazilianDocuments.format_cpf("62653322064594")
      :error

      iex> BrazilianDocuments.format_cpf("123")
      :error

      iex> BrazilianDocuments.format_cpf("invalid")
      :error

  """
  @spec format_cpf(value :: String.t()) :: {:ok, String.t()} | :error
  def format_cpf(value) when is_binary(value) do
    if valid_cpf?(value) do
      {:ok,
       String.replace(value, ~r/^(\d{3})(\d{3})(\d{3})(\d{2})$/, "\\g{1}.\\g{2}.\\g{3}-\\g{4}")}
    else
      :error
    end
  end

  @doc """
  Format a CNPJ value.

  ## Examples

      iex> BrazilianDocuments.format_cnpj("28603414938513")
      {:ok, "28.603.414/9385-13"}

      iex> BrazilianDocuments.format_cnpj("57.120.949/4422-42")
      {:ok, "57.120.949/4422-42"}

      iex> BrazilianDocuments.format_cnpj("91084416506")
      :error

      iex> BrazilianDocuments.format_cnpj("123")
      :error

      iex> BrazilianDocuments.format_cnpj("invalid")
      :error

  """
  @spec format_cnpj(value :: String.t()) :: {:ok, String.t()} | :error
  def format_cnpj(value) when is_binary(value) do
    if valid_cnpj?(value) do
      {:ok,
       String.replace(
         value,
         ~r/^(\d{2})(\d{3})(\d{3})(\d{4})(\d{2})$/,
         "\\g{1}.\\g{2}.\\g{3}/\\g{4}-\\g{5}"
       )}
    else
      :error
    end
  end

  @doc """
  Generate a unformatted CPF.

  ## Examples

      iex> cnpj = BrazilianDocuments.generate_cpf()
      iex> BrazilianDocuments.valid_cpf?(cnpj)
      true

  """
  @spec generate_cpf :: String.t()
  def generate_cpf do
    numbers = random_numbers(9)

    {first_digit, second_digit} = checker_digits(numbers, 9, [11, 10, 9, 8, 7, 6, 5, 4, 3, 2])

    numbers |> Enum.concat([first_digit, second_digit]) |> Enum.join("")
  end

  @doc """
  Generate a unformatted CNPJ.

  ## Examples

      iex> cnpj = BrazilianDocuments.generate_cnpj()
      iex> BrazilianDocuments.valid_cnpj?(cnpj)
      true

  """
  @spec generate_cnpj :: String.t()
  def generate_cnpj do
    numbers = random_numbers(12)

    {first_digit, second_digit} =
      checker_digits(numbers, 12, [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2])

    numbers |> Enum.concat([first_digit, second_digit]) |> Enum.join("")
  end

  defp random_numbers(amount) do
    (&random_number_generator_fun/0)
    |> Stream.repeatedly()
    |> Enum.take(amount)
  end

  defp random_number_generator_fun, do: Enum.random(0..9)

  defp valid_checker_digits?(numbers, size, verifiers) do
    {first_digit, second_digit} = checker_digits(numbers, size, verifiers)

    Enum.at(numbers, size) == first_digit and Enum.at(numbers, size + 1) == second_digit
  end

  defp all_numbers_equal?(numbers) do
    Enum.all?(numbers, fn x -> x == Enum.at(numbers, 0) end)
  end

  defp to_numbers_list(value) do
    value
    |> String.replace(~r/\D/, "")
    |> String.split("", trim: true)
    |> Enum.map(&String.to_integer/1)
  end

  defp checker_digits(numbers, size, verifiers) do
    {_, first_digit_verifiers} = List.pop_at(verifiers, 0)

    first_digit =
      numbers
      |> Enum.take(size)
      |> Enum.zip(first_digit_verifiers)
      |> Enum.map(fn {a, b} -> a * b end)
      |> Enum.sum()
      |> rem(11)
      |> eleven_minus()

    second_digit =
      numbers
      |> Enum.take(size)
      |> Enum.concat([first_digit])
      |> Enum.zip(verifiers)
      |> Enum.map(fn {a, b} -> a * b end)
      |> Enum.sum()
      |> rem(11)
      |> eleven_minus()

    {first_digit, second_digit}
  end

  defp eleven_minus(num) when num < 2, do: 0
  defp eleven_minus(num), do: 11 - num
end