Skip to content

Instantly share code, notes, and snippets.

@fidelisrafael
Last active June 27, 2016 22:43
Show Gist options
  • Select an option

  • Save fidelisrafael/ebf9403f42dba89100d12a95e8a3ed06 to your computer and use it in GitHub Desktop.

Select an option

Save fidelisrafael/ebf9403f42dba89100d12a95e8a3ed06 to your computer and use it in GitHub Desktop.
CPF & CNPJ validator and formatter
require './documents/base'
require './documents/cpf'
require './documents/cnpj'
module BRDocuments
VERSION = '0.0.1'
end
module BRDocuments
class Base
VERSION = '0.0.1'
CONSTANTS_MAP = [
:first_digit_verify_mask,
:last_digit_verify_mask,
:division_factor_modulo,
:valid_format_regexp,
:pretty_format_mask
]
class << self
def valid?(document_number)
# remove all non digits and return an array to be matched with mask
normalized_document = normalize_document_number(document_number)
# remove last two digits to be verified
last_digits = normalized_document.slice!(-2,2).map(&:to_i)
digits = calculate_last_two_verify_digits(normalized_document)
last_digits == digits
end
def invalid?(document_number)
!valid?(document_number)
end
def calculate_last_two_verify_digits(document_number)
first_mask, last_mask = get_first_digit_verify_mask, get_last_digit_verify_mask
division_modulo = get_division_factor_modulo
document_number = normalize_document_number(document_number, first_mask.size)
first_verify_digit = calculate_verify_digit(document_number, first_mask, division_modulo)
# Add first verify digit to document number to match second digit verify mask
current_document = document_number.dup << first_verify_digit
last_verify_digit = calculate_verify_digit(current_document, last_mask, division_modulo)
[first_verify_digit, last_verify_digit]
end
def calculate_verify_digit(document_number, mask, division_factor)
digits = calculate_digits_data(document_number, mask, division_factor)
digits[:verify_digit].to_i
end
def calculate_digits_data(document_number, mask, division_factor)
sum = calculate_digits_sum(document_number, mask)
quotient = (sum / division_factor)
rest = (sum % division_factor)
{ sum: sum, quotient: quotient, rest: rest, verify_digit: digit_verify(rest, division_factor) }
end
def calculate_digits_sum(document_number, mask)
document_number.each_with_index.map {|n,i| n.to_i * mask[i].to_i }.reduce do |value, total|
total += value
end.to_f
end
def digit_verify(quotient_rest, division_factor)
# thats the rule, if quotient_rest < 2, so its became 0
return 0 if quotient_rest < 2
(division_factor - quotient_rest).to_i
end
def normalize_document_number(document_number, length = nil)
document_number = clear_document_number(document_number).split(//).map(&:to_i)
length.nil? ? document_number : document_number[0, length]
end
# remove any non digit from document number(this will help regexp to be validated)
def clear_document_number(document_number)
document_number.to_s.gsub(/[^(\d+)]/, '')
end
def pretty_formatted(document_number)
return "" if normalize_document_number(document_number).empty?
numbers = document_number.to_s.scan(get_valid_format_regexp).flatten
# document has a value but it's not valid
return "" if numbers.empty?
get_pretty_format_mask % numbers
end
CONSTANTS_MAP.each do |const_identifier|
define_method "get_#{const_identifier}" do
const_name = const_identifier.to_s.upcase
self.const_get(const_identifier.upcase)
end
define_method const_identifier do |arg|
const_name = const_identifier.to_s.upcase
self.const_set(const_name, arg)
end
end
end
end
end
module BRDocuments
class CPF < Base
# MOD 11
division_factor_modulo 11
first_digit_verify_mask %w(10 9 8 7 6 5 4 3 2)
last_digit_verify_mask %w(11 10 9 8 7 6 5 4 3 2)
# match format such as: 999.999.999-99 | 999-999-999-99 | 99999999999
valid_format_regexp %r{(\d{3})[-.]?(\d{3})[-.]?(\d{3})[-.]?(\d{2})}
pretty_format_mask %(%s.%s.%s-%s)
end
end
module BRDocuments
class CNPJ < Base
# MOD 11
division_factor_modulo 11
first_digit_verify_mask %w(5 4 3 2 9 8 7 6 5 4 3 2)
last_digit_verify_mask %w(6 5 4 3 2 9 8 7 6 5 4 3 2)
# match format such as: 99.999.999/9999-99 | 99-999-999/9999-99 | 99999999/999999 | 99999999999999
valid_format_regexp %r{(\d{2})[-.]?(\d{3})[-.]?(\d{3})[\/]?(\d{4})[-.]?(\d{2})}
pretty_format_mask %(%s.%s.%s/%s-%s)
end
end
module BRDocuments
class Base
VERSION = '0.0.1'
CONSTANTS_MAP = [
:first_digit_verify_mask,
:last_digit_verify_mask,
:division_factor_modulo,
:valid_format_regexp,
:pretty_format_mask
]
class << self
def valid?(document_number)
# remove all non digits and return an array to be matched with mask
normalized_document = normalize_document_number(document_number)
# remove last two digits to be verified
last_digits = normalized_document.slice!(-2,2).map(&:to_i)
digits = calculate_last_two_verify_digits(normalized_document)
last_digits == digits
end
def invalid?(document_number)
!valid?(document_number)
end
def calculate_last_two_verify_digits(document_number)
first_mask, last_mask = get_first_digit_verify_mask, get_last_digit_verify_mask
division_modulo = get_division_factor_modulo
document_number = normalize_document_number(document_number, first_mask.size)
first_verify_digit = calculate_verify_digit(document_number, first_mask, division_modulo)
# Add first verify digit to document number to match second digit verify mask
current_document = document_number.dup << first_verify_digit
last_verify_digit = calculate_verify_digit(current_document, last_mask, division_modulo)
[first_verify_digit, last_verify_digit]
end
def calculate_verify_digit(document_number, mask, division_factor)
digits = calculate_digits_data(document_number, mask, division_factor)
digits[:verify_digit].to_i
end
def calculate_digits_data(document_number, mask, division_factor)
sum = calculate_digits_sum(document_number, mask)
quotient = (sum / division_factor)
rest = (sum % division_factor)
{ sum: sum, quotient: quotient, rest: rest, verify_digit: digit_verify(rest, division_factor) }
end
def calculate_digits_sum(document_number, mask)
document_number.each_with_index.map {|n,i| n.to_i * mask[i].to_i }.reduce do |value, total|
total += value
end.to_f
end
def digit_verify(quotient_rest, division_factor)
# thats the rule, if quotient_rest < 2, so its became 0
return 0 if quotient_rest < 2
(division_factor - quotient_rest).to_i
end
def normalize_document_number(document_number, length = nil)
document_number = clear_document_number(document_number).split(//).map(&:to_i)
length.nil? ? document_number : document_number[0, length]
end
# remove any non digit from document number(this will help regexp to be validated)
def clear_document_number(document_number)
document_number.to_s.gsub(/[^(\d+)]/, '')
end
def pretty_formatted(document_number)
return "" if normalize_document_number(document_number).empty?
numbers = document_number.to_s.scan(get_valid_format_regexp).flatten
# document has a value but it's not valid
return "" if numbers.empty?
get_pretty_format_mask % numbers
end
CONSTANTS_MAP.each do |const_identifier|
define_method "get_#{const_identifier}" do
const_name = const_identifier.to_s.upcase
self.const_get(const_identifier.upcase)
end
define_method const_identifier do |arg|
const_name = const_identifier.to_s.upcase
self.const_set(const_name, arg)
end
end
end
end
end
module BRDocuments
class CNPJ < Base
# MOD 11
division_factor_modulo 11
first_digit_verify_mask %w(5 4 3 2 9 8 7 6 5 4 3 2)
last_digit_verify_mask %w(6 5 4 3 2 9 8 7 6 5 4 3 2)
# match format such as: 99.999.999/9999-99 | 99-999-999/9999-99 | 99999999/999999 | 99999999999999
valid_format_regexp %r{(\d{2})[-.]?(\d{3})[-.]?(\d{3})[\/]?(\d{4})[-.]?(\d{2})}
pretty_format_mask %(%s.%s.%s/%s-%s)
end
end
module BRDocuments
class CPF < Base
# MOD 11
division_factor_modulo 11
first_digit_verify_mask %w(10 9 8 7 6 5 4 3 2)
last_digit_verify_mask %w(11 10 9 8 7 6 5 4 3 2)
# match format such as: 999.999.999-99 | 999-999-999-99 | 99999999999
valid_format_regexp %r{(\d{3})[-.]?(\d{3})[-.]?(\d{3})[-.]?(\d{2})}
pretty_format_mask %(%s.%s.%s-%s)
end
end
FILENAME = 'br_documents_all.rb'
files = ['base', 'cpf', 'cnpj']
file_data = ''
files.each do |file|
file_data << File.read(File.join('documents', "#{file}.rb"))
end
File.write(FILENAME, file_data)
puts "File #{FILENAME} generated"
require './br_documents'
module Expectations
def _assert(expression, expected, should_be)
return (expression == expected) == should_be
end
def _assert_p(expression, expected, should_be)
puts _assert(expression, expected, should_be)
end
end
extend Expectations
### CNPJ
_assert_p(BRDocuments::CNPJ.calculate_last_two_verify_digits("112223330001"), [8,1], true) # true
_assert_p(BRDocuments::CNPJ.calculate_last_two_verify_digits("112223330001"), [8,2], false) # false
_assert_p(BRDocuments::CNPJ.valid?("18781203000128"), true, true) # true
_assert_p(BRDocuments::CNPJ.invalid?("18781203000127"), true, true) # true
_assert_p(BRDocuments::CNPJ.calculate_last_two_verify_digits("193894970001­"), [0,0], true) # true
_assert_p(BRDocuments::CNPJ.valid?("19389497000100­"), true, true) # true
_assert_p(BRDocuments::CNPJ.invalid?("193894970001001"), true, true) # true
_assert_p(BRDocuments::CNPJ.invalid?(193894970001001), true, true) # true
_assert_p(BRDocuments::CNPJ.pretty_formatted("193894970001001"), "19.389.497/0001-00", true) # true
_assert_p(BRDocuments::CNPJ.pretty_formatted(""), "", true) # true
### CPF
_assert_p(BRDocuments::CPF.calculate_last_two_verify_digits("110882416"), [1, 1], true) # true
_assert_p(BRDocuments::CPF.valid?("110882416-11"), true, true) # true
_assert_p(BRDocuments::CPF.invalid?("110882416-12"), true, true) # true
_assert_p(BRDocuments::CPF.pretty_formatted("110882416-11"), "110.882.416-11", true) # true
_assert_p(BRDocuments::CPF.pretty_formatted(""), "", true) # true
@fidelisrafael
Copy link
Copy Markdown
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment