br_validators 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 46a5c5d1d504c919923fbc23bb01ee2f4c82eeccbf752f8130740baa97ae35e6
4
- data.tar.gz: 8d7dc883a24f170123e0df911397e4981af249941ae48d04d0476ff1d97029c0
3
+ metadata.gz: 7c36775e47d48c4dadc9fae27db78abb372bacdd85cf58ac586d3c005ddc29ae
4
+ data.tar.gz: 5b0a94be147182c7773dd201194041810b06cc4c0a6e784392dee72233dc4797
5
5
  SHA512:
6
- metadata.gz: 66f54a26023c513533a5f1928a7875232d645f44161b0d6457da9cff3579aa666444d5ff09cad07058144c4113f41d89e75c05543433cf62a811fe7f387a1ceb
7
- data.tar.gz: '08a8f1547c74bf7062abb1a7dda42c69e55e7a9f6578f6473871d19155b36e463644a4d03458aed3b972c8fbce1d90b1da74151ec3fb539c0a7c3cc8ab294bc5'
6
+ metadata.gz: b88cefcb1d52ba0f54b77f6a808b9f962449f4fbc636c3491e9502c48747e1dd205e23887bcc126c7661bc48096beabc13ccab9d75695ec1b0bb033f091ceec1
7
+ data.tar.gz: c04d5712613468e91dfe37ad9669634c5ec6991a4e2c36e9eaff43b8b3d21958612cac46cfd93bc8b6eccffbec885511132a601ec91c878bb9c7931542764a8e
data/CHANGELOG.md CHANGED
@@ -3,6 +3,29 @@
3
3
  As mudanças de cada versão ficam aqui. O projeto segue o
4
4
  [versionamento semântico](https://semver.org/lang/pt-BR/).
5
5
 
6
+ ## [0.3.1] - 2026-10-31
7
+
8
+ ### Corrigido
9
+
10
+ - `Consulta.banco` e `Consulta.ddd` aceitam número e texto em qualquer encoding:
11
+ `Consulta.banco("341".encode("UTF-16LE"))` levantava `Encoding::CompatibilityError`.
12
+
13
+ ### Mudado
14
+
15
+ - Mais rápido, com os mesmos resultados (conferido em 72 mil entradas contra a 0.3.0):
16
+ `format`, `uf`, `ddd`, `segmento` e `modelo` tiram a máscara uma vez só, e as contas
17
+ de dígito verificador não criam uma lista por caractere. `CPF.format` ficou cerca de
18
+ 3 vezes mais rápido; a chave de acesso e o boleto, cerca de 2 vezes.
19
+ - A conta de módulo 11 com pesos de 2 a 9, que estava repetida no CNPJ, no RENAVAM, na
20
+ chave de acesso e no boleto, ficou num lugar só (`digitos.rb`, interno da gem).
21
+
22
+ ### Outros
23
+
24
+ - Página de teste com visual novo, de documento: fonte datilografada, a trama dos papéis
25
+ de segurança no topo, o formulário como uma ficha e o resultado como um carimbo. O
26
+ campo mostra o modelo do formato, o resultado aparece na tela no celular e a página
27
+ tem imagem para quando o link é compartilhado.
28
+
6
29
  ## [0.3.0] - 2026-10-31
7
30
 
8
31
  ### Novo
data/README.md CHANGED
@@ -189,8 +189,11 @@ pode abrir direto num documento: `?documento=placa`, `?documento=boleto`... A p
189
189
  mostra o resultado, explica por que um número é inválido e monta o trecho de código
190
190
  Ruby que faz o mesmo.
191
191
 
192
- A página (`public/`) é HTML e JavaScript puro e chama uma função Ruby da Vercel
193
- (`api/validar.rb`) que usa a própria gem, então as regras ficam num lugar só:
192
+ A página (`public/`) é HTML, CSS e JavaScript puro, sem build, e chama uma função Ruby
193
+ da Vercel (`api/validar.rb`) que usa a própria gem, então as regras ficam num lugar só.
194
+ O visual imita um documento brasileiro; a fonte datilografada é a
195
+ [Courier Prime](https://github.com/quoteunquoteapps/CourierPrime) (licença OFL, em
196
+ `public/fontes`).
194
197
 
195
198
  ```
196
199
  GET /api/validar?documento=cnpj&valor=12.ABC.345/01DE-35
@@ -30,11 +30,7 @@ module BrValidators
30
30
 
31
31
  # :cobranca, :arrecadacao ou nil se a linha for inválida.
32
32
  def self.type(value)
33
- line = strip(value)
34
- return :cobranca if line.match?(/\A\d{47}\z/) && valid_cobranca?(line)
35
- return :arrecadacao if line.match?(/\A8\d{47}\z/) && valid_arrecadacao?(line)
36
-
37
- nil
33
+ type_stripped(strip(value))
38
34
  end
39
35
 
40
36
  def self.valid?(value)
@@ -46,12 +42,19 @@ module BrValidators
46
42
  # arrecadação: "82670000001-9 00000000000-0 00000000000-0 00000000000-0"
47
43
  def self.format(value)
48
44
  line = strip(value)
49
- case type(line)
45
+ case type_stripped(line)
50
46
  when :cobranca then format_cobranca(line)
51
47
  when :arrecadacao then line.scan(/\d{12}/).map { |block| "#{block[0, 11]}-#{block[11]}" }.join(" ")
52
48
  end
53
49
  end
54
50
 
51
+ # A regra, com a linha já sem máscara.
52
+ def self.type_stripped(line)
53
+ return :cobranca if line.match?(/\A\d{47}\z/) && valid_cobranca?(line)
54
+
55
+ :arrecadacao if line.match?(/\A8\d{47}\z/) && valid_arrecadacao?(line)
56
+ end
57
+
55
58
  def self.format_cobranca(line)
56
59
  "#{line[0, 5]}.#{line[5, 5]} #{line[10, 5]}.#{line[15, 6]} " \
57
60
  "#{line[21, 5]}.#{line[26, 6]} #{line[32]} #{line[33, 14]}"
@@ -102,33 +105,30 @@ module BrValidators
102
105
 
103
106
  # Pesos 2, 1, 2, 1... da direita para a esquerda; produtos com dois algarismos
104
107
  # entram somados (12 vira 1 + 2). O dígito é o que falta para a próxima dezena.
108
+ # getbyte - 48 é o valor do dígito (explicado em digitos.rb).
105
109
  def self.mod10(digits)
106
- sum = digits.reverse.chars.each_with_index.sum do |digit, index|
107
- product = digit.to_i * (index.even? ? 2 : 1)
110
+ last = digits.size - 1
111
+ sum = (0..last).sum do |index|
112
+ product = (digits.getbyte(last - index) - 48) * (index.even? ? 2 : 1)
108
113
  (product / 10) + (product % 10)
109
114
  end
110
115
  (10 - (sum % 10)) % 10
111
116
  end
112
117
 
113
- # Pesos 2 a 9, da direita para a esquerda, voltando ao 2 depois do 9.
114
- def self.mod11_sum(digits)
115
- digits.reverse.chars.each_with_index.sum { |digit, index| digit.to_i * ((index % 8) + 2) }
116
- end
117
-
118
- # Na cobrança, quando a conta dá 0, 10 ou 11, o dígito vira 1 (nunca é 0).
118
+ # Na cobrança, a soma é a do módulo 11 com pesos de 2 a 9 (Digitos, em
119
+ # digitos.rb), mas quando a conta dá 0, 10 ou 11 o dígito vira 1 (nunca é 0).
119
120
  def self.mod11_cobranca(digits)
120
- digit = 11 - (mod11_sum(digits) % 11)
121
+ digit = 11 - (Digitos.soma_modulo11(digits) % 11)
121
122
  [0, 10, 11].include?(digit) ? 1 : digit
122
123
  end
123
124
 
124
- # Na arrecadação, resto 0 ou 1 vira dígito 0.
125
+ # Na arrecadação, é o módulo 11 comum: resto 0 ou 1 vira dígito 0.
125
126
  def self.mod11_arrecadacao(digits)
126
- rest = mod11_sum(digits) % 11
127
- rest < 2 ? 0 : 11 - rest
127
+ Digitos.modulo11(digits)
128
128
  end
129
129
 
130
- private_class_method :format_cobranca, :valid_cobranca?, :cobranca_barcode, :valid_arrecadacao?,
131
- :arrecadacao_check, :check_digit_ok?, :without, :mod10, :mod11_sum,
130
+ private_class_method :type_stripped, :format_cobranca, :valid_cobranca?, :cobranca_barcode,
131
+ :valid_arrecadacao?, :arrecadacao_check, :check_digit_ok?, :without, :mod10,
132
132
  :mod11_cobranca, :mod11_arrecadacao
133
133
  end
134
134
  end
@@ -24,16 +24,21 @@ module BrValidators
24
24
  # 8 dígitos. "00000-000" é o CEP que sistemas usam como "não informado";
25
25
  # nenhuma região dos Correios começa com 00000.
26
26
  def self.valid?(value)
27
- cep = strip(value)
28
- cep.match?(/\A\d{8}\z/) && cep != "00000000"
27
+ valid_stripped?(strip(value))
29
28
  end
30
29
 
31
30
  # Devolve "01001-000", ou nil se o CEP for inválido.
32
31
  def self.format(value)
33
- return nil unless valid?(value)
34
-
35
32
  cep = strip(value)
33
+ return nil unless valid_stripped?(cep)
34
+
36
35
  "#{cep[0, 5]}-#{cep[5, 3]}"
37
36
  end
37
+
38
+ def self.valid_stripped?(cep)
39
+ cep.match?(/\A\d{8}\z/) && cep != "00000000"
40
+ end
41
+
42
+ private_class_method :valid_stripped?
38
43
  end
39
44
  end
@@ -48,31 +48,32 @@ module BrValidators
48
48
  end
49
49
 
50
50
  def self.valid?(value)
51
- chave = strip(value)
52
- return false unless chave.match?(PATTERN)
53
- return false unless UFS_IBGE.include?(chave[0, 2]) && chave[4, 2].between?("01", "12")
54
-
55
- chave[43] == check_digit(chave[0, 43])
51
+ valid_stripped?(strip(value))
56
52
  end
57
53
 
58
54
  # Devolve a chave em 11 grupos de 4 ("3526 1011 2223 ..."), ou nil se for inválida.
59
55
  def self.format(value)
60
- return nil unless valid?(value)
56
+ chave = strip(value)
57
+ return nil unless valid_stripped?(chave)
61
58
 
62
- strip(value).scan(/.{4}/).join(" ")
59
+ chave.scan(/.{4}/).join(" ")
63
60
  end
64
61
 
65
62
  # O código do modelo ("55" para NF-e, "65" para NFC-e...), ou nil se for inválida.
66
63
  def self.modelo(value)
67
- valid?(value) ? strip(value)[20, 2] : nil
64
+ chave = strip(value)
65
+ valid_stripped?(chave) ? chave[20, 2] : nil
68
66
  end
69
67
 
70
- def self.check_digit(chars)
71
- sum = chars.reverse.chars.each_with_index.sum { |char, index| (char.ord - 48) * ((index % 8) + 2) }
72
- rest = sum % 11
73
- (rest < 2 ? 0 : 11 - rest).to_s
68
+ # A regra, com a chave já sem máscara. O dígito é o módulo 11 com pesos de 2 a 9
69
+ # (Digitos, em digitos.rb), com as letras do CNPJ valendo ASCII menos 48.
70
+ def self.valid_stripped?(chave)
71
+ return false unless chave.match?(PATTERN)
72
+ return false unless UFS_IBGE.include?(chave[0, 2]) && chave[4, 2].between?("01", "12")
73
+
74
+ chave[43] == Digitos.modulo11(chave[0, 43]).to_s
74
75
  end
75
76
 
76
- private_class_method :check_digit
77
+ private_class_method :valid_stripped?
77
78
  end
78
79
  end
@@ -33,7 +33,19 @@ module BrValidators
33
33
  end
34
34
 
35
35
  def self.valid?(value)
36
+ valid_stripped?(strip(value))
37
+ end
38
+
39
+ # Devolve "12.ABC.345/01DE-35", ou nil se o CNPJ for inválido.
40
+ def self.format(value)
36
41
  cnpj = strip(value)
42
+ return nil unless valid_stripped?(cnpj)
43
+
44
+ "#{cnpj[0, 2]}.#{cnpj[2, 3]}.#{cnpj[5, 3]}/#{cnpj[8, 4]}-#{cnpj[12, 2]}"
45
+ end
46
+
47
+ # A regra, com o CNPJ já sem máscara e em maiúsculas.
48
+ def self.valid_stripped?(cnpj)
37
49
  return false unless cnpj.match?(PATTERN)
38
50
 
39
51
  # 00.000.000/0000-00 e parecidos passam na conta, mas não existem.
@@ -42,29 +54,14 @@ module BrValidators
42
54
  cnpj[12, 2] == check_digits(cnpj[0, 12])
43
55
  end
44
56
 
45
- # Devolve "12.ABC.345/01DE-35", ou nil se o CNPJ for inválido.
46
- def self.format(value)
47
- return nil unless valid?(value)
48
-
49
- cnpj = strip(value)
50
- "#{cnpj[0, 2]}.#{cnpj[2, 3]}.#{cnpj[5, 3]}/#{cnpj[8, 4]}-#{cnpj[12, 2]}"
51
- end
52
-
57
+ # Cada dígito é o módulo 11 com pesos de 2 a 9 (Digitos, em digitos.rb) dos
58
+ # caracteres anteriores; o segundo conta também o primeiro.
53
59
  def self.check_digits(base)
54
- first = check_digit(base)
55
- second = check_digit("#{base}#{first}")
60
+ first = Digitos.modulo11(base)
61
+ second = Digitos.modulo11("#{base}#{first}")
56
62
  "#{first}#{second}"
57
63
  end
58
64
 
59
- def self.check_digit(chars)
60
- # Pesos 2, 3, ..., 9, 2, 3... a partir do último caractere.
61
- sum = chars.reverse.chars.each_with_index.sum do |char, index|
62
- (char.ord - 48) * ((index % 8) + 2)
63
- end
64
- rest = sum % 11
65
- (rest < 2 ? 0 : 11 - rest).to_s
66
- end
67
-
68
- private_class_method :check_digits, :check_digit
65
+ private_class_method :valid_stripped?, :check_digits
69
66
  end
70
67
  end
@@ -28,7 +28,20 @@ module BrValidators
28
28
  end
29
29
 
30
30
  def self.valid?(value)
31
+ valid_stripped?(strip(value))
32
+ end
33
+
34
+ # Devolve o CNS em grupos de 3, 4, 4 e 4 dígitos, como aparece impresso no
35
+ # cartão ("123 4567 8901 0000"), ou nil se for inválido.
36
+ def self.format(value)
31
37
  cns = strip(value)
38
+ return nil unless valid_stripped?(cns)
39
+
40
+ "#{cns[0, 3]} #{cns[3, 4]} #{cns[7, 4]} #{cns[11, 4]}"
41
+ end
42
+
43
+ # A regra, com o CNS já sem máscara.
44
+ def self.valid_stripped?(cns)
32
45
  return false unless cns.match?(/\A\d{15}\z/)
33
46
 
34
47
  case cns[0]
@@ -38,15 +51,6 @@ module BrValidators
38
51
  end
39
52
  end
40
53
 
41
- # Devolve o CNS em grupos de 3, 4, 4 e 4 dígitos, como aparece impresso no
42
- # cartão ("123 4567 8901 0000"), ou nil se for inválido.
43
- def self.format(value)
44
- return nil unless valid?(value)
45
-
46
- cns = strip(value)
47
- "#{cns[0, 3]} #{cns[3, 4]} #{cns[7, 4]} #{cns[11, 4]}"
48
- end
49
-
50
54
  # Monta o CNS esperado para um PIS, como na rotina do Ministério da Saúde.
51
55
  def self.from_pis(pis)
52
56
  sum = weighted_sum(pis)
@@ -59,11 +63,12 @@ module BrValidators
59
63
  "#{pis}001#{digit}"
60
64
  end
61
65
 
62
- # Pesos começando em 15 e diminuindo de 1 em 1 a cada dígito.
66
+ # Pesos começando em 15 e diminuindo de 1 em 1 a cada dígito. getbyte - 48 é o
67
+ # valor do dígito (explicado em digitos.rb).
63
68
  def self.weighted_sum(digits)
64
- digits.chars.each_with_index.sum { |digit, index| digit.to_i * (15 - index) }
69
+ (0...digits.size).sum { |index| (digits.getbyte(index) - 48) * (15 - index) }
65
70
  end
66
71
 
67
- private_class_method :from_pis, :weighted_sum
72
+ private_class_method :valid_stripped?, :from_pis, :weighted_sum
68
73
  end
69
74
  end
@@ -61,7 +61,8 @@ module BrValidators
61
61
 
62
62
  # Banco pelo código de compensação ("001", "237", 341): code, name, fullName e ispb.
63
63
  def self.banco(codigo)
64
- codigo = codigo.to_s.strip
64
+ # BrValidators.texto: aceita número e texto em qualquer encoding (UTF-16 dava erro).
65
+ codigo = BrValidators.texto(codigo).strip
65
66
  return nil unless codigo.match?(/\A\d{1,3}\z/)
66
67
 
67
68
  buscar("/banks/v1/#{codigo.to_i}")
@@ -69,7 +70,7 @@ module BrValidators
69
70
 
70
71
  # UF e cidades de um DDD: state e cities.
71
72
  def self.ddd(valor)
72
- ddd = valor.to_s.strip
73
+ ddd = BrValidators.texto(valor).strip
73
74
  return nil unless Telefone::DDDS.include?(ddd)
74
75
 
75
76
  buscar("/ddd/v1/#{ddd}")
@@ -24,24 +24,28 @@ module BrValidators
24
24
  end
25
25
 
26
26
  def self.valid?(value)
27
- digits = strip(value)
28
- return false unless digits.match?(/\A\d{11}\z/)
29
-
30
- # 111.111.111-11, 222.222.222-22... passam na conta, mas não são CPFs reais.
31
- return false if digits.squeeze.size == 1
32
-
33
- digits[9, 2] == check_digits(digits[0, 9])
27
+ valid_stripped?(strip(value))
34
28
  end
35
29
 
36
30
  # Devolve "123.456.789-09", ou nil se o CPF for inválido
37
31
  # (formatar um número errado esconderia o erro de quem chamou).
38
32
  def self.format(value)
39
- return nil unless valid?(value)
40
-
41
33
  digits = strip(value)
34
+ return nil unless valid_stripped?(digits)
35
+
42
36
  "#{digits[0, 3]}.#{digits[3, 3]}.#{digits[6, 3]}-#{digits[9, 2]}"
43
37
  end
44
38
 
39
+ # A regra, com o CPF já sem máscara.
40
+ def self.valid_stripped?(digits)
41
+ return false unless digits.match?(/\A\d{11}\z/)
42
+
43
+ # 111.111.111-11, 222.222.222-22... passam na conta, mas não são CPFs reais.
44
+ return false if digits.squeeze.size == 1
45
+
46
+ digits[9, 2] == check_digits(digits[0, 9])
47
+ end
48
+
45
49
  def self.check_digits(base)
46
50
  first = check_digit(base)
47
51
  second = check_digit("#{base}#{first}")
@@ -49,14 +53,15 @@ module BrValidators
49
53
  end
50
54
 
51
55
  # Pesos decrescentes até 2, começando por (tamanho + 1): 10..2 para 9 dígitos,
52
- # 11..2 para 10 dígitos.
56
+ # 11..2 para 10 dígitos. getbyte devolve o código ASCII do dígito; menos 48 dá
57
+ # o valor dele ("0" é 48), sem criar uma string por dígito.
53
58
  def self.check_digit(digits)
54
- weights = (2..(digits.size + 1)).to_a.reverse
55
- sum = digits.chars.zip(weights).sum { |digit, weight| digit.to_i * weight }
59
+ first_weight = digits.size + 1
60
+ sum = (0...digits.size).sum { |index| (digits.getbyte(index) - 48) * (first_weight - index) }
56
61
  rest = sum % 11
57
62
  (rest < 2 ? 0 : 11 - rest).to_s
58
63
  end
59
64
 
60
- private_class_method :check_digits, :check_digit
65
+ private_class_method :valid_stripped?, :check_digits, :check_digit
61
66
  end
62
67
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrValidators
4
+ # Contas de dígito verificador usadas por mais de um documento. É interno da gem
5
+ # (private_constant no fim): quem usa a gem não precisa dele.
6
+ #
7
+ # O valor de cada caractere é o código ASCII dele menos 48: "0" vale 0, "9" vale
8
+ # 9 e "A" vale 17, como manda a regra do CNPJ alfanumérico. Para dígitos, dá o
9
+ # mesmo que to_i, sem criar uma string por caractere (getbyte devolve o código).
10
+ module Digitos
11
+ # Soma do "módulo 11 com pesos de 2 a 9": da direita para a esquerda, cada
12
+ # caractere é multiplicado por 2, 3, ..., 9, e o peso volta ao 2 depois do 9.
13
+ # Usada no CNPJ, no RENAVAM, na chave de acesso e no boleto.
14
+ def self.soma_modulo11(texto)
15
+ ultimo = texto.size - 1
16
+ (0..ultimo).sum { |posicao| (texto.getbyte(ultimo - posicao) - 48) * ((posicao % 8) + 2) }
17
+ end
18
+
19
+ # O dígito mais comum do módulo 11: resto 0 ou 1 vira 0; senão, 11 menos o resto.
20
+ def self.modulo11(texto)
21
+ resto = soma_modulo11(texto) % 11
22
+ resto < 2 ? 0 : 11 - resto
23
+ end
24
+ end
25
+
26
+ private_constant :Digitos
27
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "version"
4
4
  require_relative "texto"
5
+ require_relative "digitos"
5
6
  require_relative "cpf"
6
7
  require_relative "cnpj"
7
8
  require_relative "cns"
@@ -27,6 +28,10 @@ require_relative "chave_acesso"
27
28
  # Alguns têm métodos a mais: type (Pix, Boleto, Telefone, Placa), uf (TituloEleitor),
28
29
  # segmento (ProcessoCNJ), modelo (ChaveAcesso), ddd (Telefone), mercosul e antiga (Placa).
29
30
  #
31
+ # Dentro de cada módulo, os métodos públicos tiram a máscara uma vez só e passam
32
+ # o texto limpo para um método privado com a regra (valid_stripped? ou
33
+ # type_stripped). Assim format, uf, modelo... não refazem o mesmo trabalho.
34
+ #
30
35
  # Este arquivo carrega só os documentos, sem os validadores do Rails. Serve para o
31
36
  # app que já tem um CpfValidator (ou outro com o mesmo nome) e quer continuar com o
32
37
  # dele: no Gemfile, gem "br_validators", require: "br_validators/documentos".
@@ -28,11 +28,7 @@ module BrValidators
28
28
 
29
29
  # :antiga, :mercosul ou nil se a placa for inválida.
30
30
  def self.type(value)
31
- placa = strip(value)
32
- return :antiga if placa.match?(ANTIGA)
33
- return :mercosul if placa.match?(MERCOSUL)
34
-
35
- nil
31
+ type_stripped(strip(value))
36
32
  end
37
33
 
38
34
  def self.valid?(value)
@@ -42,7 +38,7 @@ module BrValidators
42
38
  # Devolve "ABC-1234" (antiga) ou "ABC1D23" (Mercosul), ou nil se for inválida.
43
39
  def self.format(value)
44
40
  placa = strip(value)
45
- case type(placa)
41
+ case type_stripped(placa)
46
42
  when :antiga then "#{placa[0, 3]}-#{placa[3, 4]}"
47
43
  when :mercosul then placa
48
44
  end
@@ -52,7 +48,7 @@ module BrValidators
52
48
  # mercosul("ABC-1234") # => "ABC1C34"
53
49
  def self.mercosul(value)
54
50
  placa = strip(value)
55
- case type(placa)
51
+ case type_stripped(placa)
56
52
  when :antiga then placa[0, 4] + ("A".ord + placa[4].to_i).chr + placa[5, 2]
57
53
  when :mercosul then placa
58
54
  end
@@ -64,10 +60,21 @@ module BrValidators
64
60
  # antiga("ABC1C34") # => "ABC1234"
65
61
  def self.antiga(value)
66
62
  placa = strip(value)
67
- return placa if type(placa) == :antiga
68
- return nil unless type(placa) == :mercosul && placa[4].between?("A", "J")
63
+ case type_stripped(placa)
64
+ when :antiga then placa
65
+ when :mercosul
66
+ # Só as letras de A a J na quinta posição vieram da conversão.
67
+ placa[0, 4] + (placa[4].ord - "A".ord).to_s + placa[5, 2] if placa[4].between?("A", "J")
68
+ end
69
+ end
70
+
71
+ # A regra, com a placa já sem máscara e em maiúsculas.
72
+ def self.type_stripped(placa)
73
+ return :antiga if placa.match?(ANTIGA)
69
74
 
70
- placa[0, 4] + (placa[4].ord - "A".ord).to_s + placa[5, 2]
75
+ :mercosul if placa.match?(MERCOSUL)
71
76
  end
77
+
78
+ private_class_method :type_stripped
72
79
  end
73
80
  end
@@ -38,26 +38,33 @@ module BrValidators
38
38
  end
39
39
 
40
40
  def self.valid?(value)
41
- numero = strip(value)
42
- return false unless numero.match?(/\A\d{20}\z/)
43
- return false unless SEGMENTOS.key?(numero[13])
44
-
45
- # NNNNNNN + AAAA J TR OOOO + DD
46
- (numero[0, 7] + numero[9, 11] + numero[7, 2]).to_i % 97 == 1
41
+ valid_stripped?(strip(value))
47
42
  end
48
43
 
49
44
  # Devolve "0000123-45.2024.8.26.0100", ou nil se o número for inválido.
50
45
  def self.format(value)
51
- return nil unless valid?(value)
52
-
53
46
  numero = strip(value)
47
+ return nil unless valid_stripped?(numero)
48
+
54
49
  "#{numero[0, 7]}-#{numero[7, 2]}.#{numero[9, 4]}.#{numero[13]}.#{numero[14, 2]}.#{numero[16, 4]}"
55
50
  end
56
51
 
57
52
  # O nome do segmento do Judiciário ("Justiça do Trabalho"), ou nil se o número
58
53
  # for inválido.
59
54
  def self.segmento(value)
60
- valid?(value) ? SEGMENTOS[strip(value)[13]] : nil
55
+ numero = strip(value)
56
+ valid_stripped?(numero) ? SEGMENTOS[numero[13]] : nil
61
57
  end
58
+
59
+ # A regra, com o número já sem máscara.
60
+ def self.valid_stripped?(numero)
61
+ return false unless numero.match?(/\A\d{20}\z/)
62
+ return false unless SEGMENTOS.key?(numero[13])
63
+
64
+ # NNNNNNN + AAAA J TR OOOO + DD
65
+ (numero[0, 7] + numero[9, 11] + numero[7, 2]).to_i % 97 == 1
66
+ end
67
+
68
+ private_class_method :valid_stripped?
62
69
  end
63
70
  end
@@ -23,26 +23,26 @@ module BrValidators
23
23
  end
24
24
 
25
25
  def self.valid?(value)
26
- renavam = strip(value)
27
- return false unless renavam.match?(/\A\d{11}\z/)
28
-
29
- # 00000000000, 11111111111... não são códigos reais.
30
- return false if renavam.squeeze.size == 1
31
-
32
- renavam[10] == check_digit(renavam[0, 10])
26
+ valid_stripped?(strip(value))
33
27
  end
34
28
 
35
29
  # O RENAVAM não tem máscara: devolve os 11 dígitos, ou nil se for inválido.
36
30
  def self.format(value)
37
- valid?(value) ? strip(value) : nil
31
+ renavam = strip(value)
32
+ valid_stripped?(renavam) ? renavam : nil
38
33
  end
39
34
 
40
- def self.check_digit(digits)
41
- sum = digits.reverse.chars.each_with_index.sum { |digit, index| digit.to_i * ((index % 8) + 2) }
42
- rest = sum % 11
43
- (rest < 2 ? 0 : 11 - rest).to_s
35
+ # A regra, com o RENAVAM já sem máscara. O dígito é o módulo 11 com pesos de
36
+ # 2 a 9 (Digitos, em digitos.rb).
37
+ def self.valid_stripped?(renavam)
38
+ return false unless renavam.match?(/\A\d{11}\z/)
39
+
40
+ # 00000000000, 11111111111... não são códigos reais.
41
+ return false if renavam.squeeze.size == 1
42
+
43
+ renavam[10] == Digitos.modulo11(renavam[0, 10]).to_s
44
44
  end
45
45
 
46
- private_class_method :check_digit
46
+ private_class_method :valid_stripped?
47
47
  end
48
48
  end
@@ -47,14 +47,7 @@ module BrValidators
47
47
 
48
48
  # :celular, :fixo ou nil se o número for inválido.
49
49
  def self.type(value)
50
- numero = nacional(value)
51
- return nil unless numero && DDDS.include?(numero[0, 2])
52
-
53
- assinante = numero[2..]
54
- return :celular if assinante.match?(CELULAR)
55
- return :fixo if assinante.match?(FIXO)
56
-
57
- nil
50
+ type_nacional(nacional(value))
58
51
  end
59
52
 
60
53
  def self.valid?(value)
@@ -63,15 +56,26 @@ module BrValidators
63
56
 
64
57
  # Devolve "(11) 98765-4321" ou "(11) 3456-7890", ou nil se for inválido.
65
58
  def self.format(value)
66
- return nil unless valid?(value)
67
-
68
59
  numero = nacional(value)
60
+ return nil unless type_nacional(numero)
61
+
69
62
  "(#{numero[0, 2]}) #{numero[2...-4]}-#{numero[-4..]}"
70
63
  end
71
64
 
72
65
  # O DDD ("11"), ou nil se o número for inválido.
73
66
  def self.ddd(value)
74
- valid?(value) ? nacional(value)[0, 2] : nil
67
+ numero = nacional(value)
68
+ type_nacional(numero) ? numero[0, 2] : nil
69
+ end
70
+
71
+ # A regra, com o número já como devolvido por nacional (ou nil).
72
+ def self.type_nacional(numero)
73
+ return nil unless numero && DDDS.include?(numero[0, 2])
74
+
75
+ assinante = numero[2..]
76
+ return :celular if assinante.match?(CELULAR)
77
+
78
+ :fixo if assinante.match?(FIXO)
75
79
  end
76
80
 
77
81
  # O número só com DDD e assinante (10 ou 11 dígitos), sem o código do país
@@ -88,6 +92,6 @@ module BrValidators
88
92
  digits.size.between?(10, 11) ? digits : nil
89
93
  end
90
94
 
91
- private_class_method :nacional
95
+ private_class_method :type_nacional, :nacional
92
96
  end
93
97
  end
@@ -36,32 +36,39 @@ module BrValidators
36
36
  end
37
37
 
38
38
  def self.valid?(value)
39
- titulo = strip(value)
40
- return false unless titulo.match?(/\A\d{12}\z/)
41
-
42
- uf = titulo[8, 2]
43
- return false unless UFS.key?(uf)
44
-
45
- first = check_digit(weighted_sum(titulo[0, 8], 2..9), uf)
46
- second = check_digit(weighted_sum("#{uf}#{first}", 7..9), uf)
47
- titulo[10, 2] == "#{first}#{second}"
39
+ valid_stripped?(strip(value))
48
40
  end
49
41
 
50
42
  # Devolve "1023 8501 0671", ou nil se o título for inválido.
51
43
  def self.format(value)
52
- return nil unless valid?(value)
53
-
54
44
  titulo = strip(value)
45
+ return nil unless valid_stripped?(titulo)
46
+
55
47
  "#{titulo[0, 4]} #{titulo[4, 4]} #{titulo[8, 4]}"
56
48
  end
57
49
 
58
50
  # A sigla da UF onde o título foi emitido ("SP", "ZZ" para o exterior), ou nil.
59
51
  def self.uf(value)
60
- valid?(value) ? UFS[strip(value)[8, 2]] : nil
52
+ titulo = strip(value)
53
+ valid_stripped?(titulo) ? UFS[titulo[8, 2]] : nil
54
+ end
55
+
56
+ # A regra, com o título já sem máscara.
57
+ def self.valid_stripped?(titulo)
58
+ return false unless titulo.match?(/\A\d{12}\z/)
59
+
60
+ uf = titulo[8, 2]
61
+ return false unless UFS.key?(uf)
62
+
63
+ first = check_digit(weighted_sum(titulo[0, 8], 2), uf)
64
+ second = check_digit(weighted_sum("#{uf}#{first}", 7), uf)
65
+ titulo[10, 2] == "#{first}#{second}"
61
66
  end
62
67
 
63
- def self.weighted_sum(digits, weights)
64
- digits.chars.zip(weights.to_a).sum { |digit, weight| digit.to_i * weight }
68
+ # Pesos crescentes da esquerda para a direita, a partir de first_weight (2 no
69
+ # primeiro dígito, 7 no segundo). getbyte - 48 é o valor do dígito (digitos.rb).
70
+ def self.weighted_sum(digits, first_weight)
71
+ (0...digits.size).sum { |index| (digits.getbyte(index) - 48) * (first_weight + index) }
65
72
  end
66
73
 
67
74
  # codigo_uf: "01" (SP) e "02" (MG) têm a regra especial do resto 0.
@@ -72,6 +79,6 @@ module BrValidators
72
79
  rest == 10 ? 0 : rest
73
80
  end
74
81
 
75
- private_class_method :weighted_sum, :check_digit
82
+ private_class_method :valid_stripped?, :weighted_sum, :check_digit
76
83
  end
77
84
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Versão da gem, lida também pelo gemspec.
4
4
  module BrValidators
5
- VERSION = "0.3.0"
5
+ VERSION = "0.3.1"
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: br_validators
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pedro Pedrassolli
@@ -31,6 +31,7 @@ files:
31
31
  - lib/br_validators/cns.rb
32
32
  - lib/br_validators/consulta.rb
33
33
  - lib/br_validators/cpf.rb
34
+ - lib/br_validators/digitos.rb
34
35
  - lib/br_validators/documentos.rb
35
36
  - lib/br_validators/locale/en.yml
36
37
  - lib/br_validators/locale/pt-BR.yml