Módulo Ruby sem gems, mais o ActiveModel::EachValidator para plugar direto nos seus models do Rails.
0=0 … 9=9, A=17 … Z=42), os pesos vão de 2 a 9 da direita para a esquerda, soma-se tudo, divide-se por 11 e o dígito é 0 quando o resto é menor que 2, ou 11 menos o resto. Ver a conta detalhada.Código completo
# frozen_string_literal: true
module Cnpj
FORMATO = /\A[0-9A-Z]{12}\d{2}\z/
NAO_ALFANUMERICO = /[^0-9A-Z]/
module_function
# Remove máscara e normaliza para maiúsculas.
def limpar(valor)
valor.to_s.upcase.gsub(NAO_ALFANUMERICO, '')
end
# Calcula um dígito do módulo 11 (valor do caractere = ASCII - 48).
def digito(sequencia)
soma = sequencia.each_char.with_index.sum do |caractere, indice|
peso = ((sequencia.length - 1 - indice) % 8) + 2
(caractere.ord - 48) * peso
end
resto = soma % 11
resto < 2 ? 0 : 11 - resto
end
# Devolve os dois dígitos verificadores da base de 12 caracteres.
def calcular_dv(base)
primeiro = digito(base)
segundo = digito("#{base}#{primeiro}")
"#{primeiro}#{segundo}"
end
# Valida CNPJ numérico ou alfanumérico, com ou sem máscara.
def valido?(valor)
cnpj = limpar(valor)
return false unless cnpj.match?(FORMATO)
return false if cnpj.chars.uniq.size == 1
calcular_dv(cnpj[0, 12]) == cnpj[12, 2]
end
# Aplica a máscara 00.000.000/0000-00.
def formatar(valor)
cnpj = limpar(valor)
return cnpj unless cnpj.length == 14
format('%s.%s.%s/%s-%s', cnpj[0, 2], cnpj[2, 3], cnpj[5, 3], cnpj[8, 4], cnpj[12, 2])
end
end
Testes e integração com o framework
# app/validators/cnpj_validator.rb
class CnpjValidator < ActiveModel::EachValidator
def validate_each(registro, atributo, valor)
return if valor.blank?
registro.errors.add(atributo, :invalid, message: 'não é um CNPJ válido') unless Cnpj.valido?(valor)
end
end
# app/models/empresa.rb
class Empresa < ApplicationRecord
validates :cnpj, presence: true, cnpj: true, uniqueness: true
before_validation { self.cnpj = Cnpj.limpar(cnpj) }
end
# spec/lib/cnpj_spec.rb
RSpec.describe Cnpj do
it 'calcula o DV do exemplo oficial' do
expect(described_class.calcular_dv('12ABC34501DE')).to eq('35')
end
it 'aceita alfanumérico e numérico' do
expect(described_class).to be_valido('12.ABC.345/01DE-35')
expect(described_class).to be_valido('11.222.333/0001-81')
expect(described_class).not_to be_valido('11.222.333/0001-82')
end
end
Detalhes que costumam quebrar em Ruby
- Use
\Ae\zna regex, nunca^e$: em Ruby eles casam início e fim de linha, e"lixo\n12ABC34501DE35"passaria. - O
before_validationnormalizando o campo evita duplicidade quando um usuário digita com máscara e outro sem.
Casos de teste recomendados
Cubra pelo menos estes cenários — eles pegam praticamente todos os erros de implementação:
| Entrada | Esperado | O que testa |
|---|---|---|
12.ABC.345/01DE-35 | válido | exemplo oficial, com máscara |
12abc34501de35 | válido | normalização para maiúsculas |
11.222.333/0001-81 | válido | compatibilidade com o formato numérico |
11.222.333/0001-82 | inválido | dígito verificador errado |
12ABC34501DEAB | inválido | letra no dígito verificador |
12ABC34501DE3 | inválido | tamanho incorreto |
00.000.000/0000-00 | inválido | sequência repetida |
"" / null | inválido | entrada vazia sem exceção |
Precisa de mais massa de teste? O gerador produz até 500 CNPJs alfanuméricos válidos de uma vez, com opção de baixar em .txt.