Current section

Files

Jump to
cpf lib cpf.ex
Raw

lib/cpf.ex

defmodule CPF do
@moduledoc """
CPF module that provides functions to verify if a CPF is valid.
"""
defstruct [:digits]
# NOTE: Remove Elixir 1.5 support when lauching CPF 1.0
# defguardp is_pos_integer(number) when is_integer(number) and number >= 0
@typep digit :: pos_integer()
@typedoc """
The CPF type. It' composed of eleven digits(0-9]).
"""
@opaque t :: %__MODULE__{
digits: {digit, digit, digit, digit, digit, digit, digit, digit, digit, digit, digit}
}
@doc """
Initializes a CPF.
## Examples
iex> CPF.new(563_606_676_73)
#CPF<563.606.676-73>
iex> CPF.new("56360667673")
#CPF<563.606.676-73>
This function doesn't check if CPF numbers are valid, only use this function
if the given `String.t` or the integer was validated before.
"""
@spec new(String.t() | pos_integer) :: t
def new(cpf) when is_integer(cpf) and cpf >= 0 do
%__MODULE__{
digits: to_digits(cpf)
}
end
def new(cpf) when is_binary(cpf) do
%__MODULE__{
digits: cpf |> String.to_integer() |> to_digits()
}
end
@doc """
Returns `true` the given `cpf` is valid, otherwise `false`.
## Examples
iex> CPF.valid?(563_606_676_73)
true
iex> CPF.valid?(563_606_676_72)
false
iex> CPF.valid?("563.606.676-73")
true
iex> CPF.valid?("563/60.6-676/73")
false
iex> CPF.valid?("563.606.676-72")
false
iex> CPF.valid?("56360667673")
true
iex> CPF.valid?("56360667672")
false
"""
@spec valid?(input :: String.t() | pos_integer) :: boolean
def valid?(cpf) when is_integer(cpf) and cpf >= 0 do
cpf_digits = Integer.digits(cpf)
padding = 11 - length(cpf_digits)
same_digits? = cpf_digits |> Enum.uniq() |> length() == 1
if padding >= 0 && !same_digits? do
cpf_digits = cpf_digits |> add_padding(padding) |> List.to_tuple()
{v1, v2} = calculate_verifier_digits(cpf_digits)
{given_v1, given_v2} = extract_given_verifier_digits(cpf_digits)
v1 == given_v1 && v2 == given_v2
else
false
end
end
def valid?(
<<left_digits::bytes-size(3)>> <>
"." <>
<<middle_digits::bytes-size(3)>> <>
"." <>
<<right_digits::bytes-size(3)>> <>
"-" <>
<<verifier_digits::bytes-size(2)>>
) do
valid?(left_digits <> middle_digits <> right_digits <> verifier_digits)
end
def valid?(cpf) when is_binary(cpf) do
case Integer.parse(cpf) do
{cpf_int, ""} ->
valid?(cpf_int)
_ ->
false
end
end
def valid?(_cpf), do: false
@doc """
Returns a formatted string from a given `cpf`.
## Examples
iex> 563_606_676_73 |> CPF.new() |> CPF.format()
"563.606.676-73"
"""
@spec format(cpf :: t()) :: String.t()
def format(%CPF{digits: digits}) do
{dig_1, dig_2, dig_3, dig_4, dig_5, dig_6, dig_7, dig_8, dig_9, dig_10, dig_11} = digits
to_string(dig_1) <>
to_string(dig_2) <>
to_string(dig_3) <>
"." <>
to_string(dig_4) <>
to_string(dig_5) <>
to_string(dig_6) <>
"." <>
to_string(dig_7) <>
to_string(dig_8) <> to_string(dig_9) <> "-" <> to_string(dig_10) <> to_string(dig_11)
end
defp add_padding(digits, 0), do: digits
defp add_padding(digits, padding) do
add_padding([0 | digits], padding - 1)
end
defp calculate_verifier_digits(digits) do
{v1_sum, v2_sum} = sum_digits(digits)
v1 = digit_verifier(v1_sum)
v2_sum = v2_sum + 2 * v1
v2 = digit_verifier(v2_sum)
{v1, v2}
end
defp sum_digits(digits) do
v1_weight = 10
v2_weight = 11
Enum.reduce(0..8, {0, 0}, fn i, {v1_sum, v2_sum} ->
v1 = (v1_weight - i) * elem(digits, i)
v2 = (v2_weight - i) * elem(digits, i)
{v1_sum + v1, v2_sum + v2}
end)
end
defp digit_verifier(sum) do
rem = rem(sum, 11)
if rem in [0, 1], do: 0, else: 11 - rem
end
defp extract_given_verifier_digits(digits) do
{elem(digits, 9), elem(digits, 10)}
end
defp to_digits(integer) do
digits = Integer.digits(integer)
padding = 11 - length(digits)
if padding >= 0 do
digits
|> add_padding(padding)
|> List.to_tuple()
else
raise ArgumentError, "it has more than 11 digits"
end
end
defimpl Inspect do
import Inspect.Algebra
def inspect(cpf, opts) do
formatted_cpf = cpf |> CPF.format() |> color(:atom, opts)
concat(["#CPF<", formatted_cpf, ">"])
end
end
end