br-utils 0.1.1 → 0.2.0
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 +4 -4
- data/README.md +446 -22
- data/lib/brazilian-utils/area-code-utils.rb +60 -0
- data/lib/brazilian-utils/bank-account-utils.rb +82 -0
- data/lib/brazilian-utils/bank-utils.rb +61 -0
- data/lib/brazilian-utils/boleto-utils.rb +139 -0
- data/lib/brazilian-utils/caepf-utils.rb +91 -0
- data/lib/brazilian-utils/cbo-utils.rb +74 -0
- data/lib/brazilian-utils/cei-utils.rb +79 -0
- data/lib/brazilian-utils/cep-utils.rb +25 -1
- data/lib/brazilian-utils/certidao-utils.rb +171 -0
- data/lib/brazilian-utils/cfop-utils.rb +74 -0
- data/lib/brazilian-utils/cnae-utils.rb +104 -0
- data/lib/brazilian-utils/cnh-utils.rb +53 -0
- data/lib/brazilian-utils/cno-utils.rb +76 -0
- data/lib/brazilian-utils/cnpj-utils.rb +125 -12
- data/lib/brazilian-utils/cns-utils.rb +110 -0
- data/lib/brazilian-utils/cpf-utils.rb +13 -0
- data/lib/brazilian-utils/credit-card-utils.rb +47 -0
- data/lib/brazilian-utils/csosn-utils.rb +55 -0
- data/lib/brazilian-utils/cst-utils.rb +103 -0
- data/lib/brazilian-utils/currency-utils.rb +347 -228
- data/lib/brazilian-utils/data/area_codes.json +676 -0
- data/lib/brazilian-utils/data/banks.json +357 -0
- data/lib/brazilian-utils/data/cbo.json +1 -0
- data/lib/brazilian-utils/data/cfop.json +1 -0
- data/lib/brazilian-utils/data/cnae.json +1 -0
- data/lib/brazilian-utils/data/csosn.json +42 -0
- data/lib/brazilian-utils/data/cst.json +294 -0
- data/lib/brazilian-utils/data/legal_nature.json +900 -0
- data/lib/brazilian-utils/data/municipalities.json +1 -0
- data/lib/brazilian-utils/data/ncm.json +1 -0
- data/lib/brazilian-utils/data/states.json +218 -0
- data/lib/brazilian-utils/date-utils.rb +504 -256
- data/lib/brazilian-utils/iban-utils.rb +120 -0
- data/lib/brazilian-utils/ie-utils.rb +84 -0
- data/lib/brazilian-utils/legal-nature-utils.rb +238 -235
- data/lib/brazilian-utils/legal-process-utils.rb +31 -3
- data/lib/brazilian-utils/license-plate-utils.rb +21 -5
- data/lib/brazilian-utils/municipality-utils.rb +58 -0
- data/lib/brazilian-utils/ncm-utils.rb +91 -0
- data/lib/brazilian-utils/nfe-key-utils.rb +158 -0
- data/lib/brazilian-utils/number-utils.rb +123 -0
- data/lib/brazilian-utils/passport-utils.rb +65 -0
- data/lib/brazilian-utils/phone-utils.rb +491 -280
- data/lib/brazilian-utils/pis-utils.rb +16 -1
- data/lib/brazilian-utils/pix-key-utils.rb +70 -0
- data/lib/brazilian-utils/pix-payload-utils.rb +253 -0
- data/lib/brazilian-utils/registro-profissional-utils.rb +112 -0
- data/lib/brazilian-utils/renavam-utils.rb +14 -0
- data/lib/brazilian-utils/state-utils.rb +105 -0
- data/lib/brazilian-utils/text-utils.rb +105 -0
- data/lib/brazilian-utils/vin-utils.rb +46 -0
- data/lib/brazilian-utils/voter-id-utils.rb +36 -0
- metadata +39 -1
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
module BrazilianUtils
|
|
2
|
+
# Utilities for the matrícula of a certidão de registro civil (art. 473
|
|
3
|
+
# of the Código Nacional de Normas da Corregedoria Nacional de Justiça):
|
|
4
|
+
# a 32-digit number grouped as 6-2-2-4-1-5-3-7-2 (registry CNS, acervo,
|
|
5
|
+
# serviço, ano, tipo do livro, livro, folha, termo, 2 check digits).
|
|
6
|
+
module CertidaoUtils
|
|
7
|
+
# Maps the single-digit "tipo do livro" code to its name.
|
|
8
|
+
BOOK_TYPES = {
|
|
9
|
+
'1' => 'birth',
|
|
10
|
+
'2' => 'marriage',
|
|
11
|
+
'3' => 'religiousMarriage',
|
|
12
|
+
'4' => 'death',
|
|
13
|
+
'5' => 'stillbirth',
|
|
14
|
+
'6' => 'banns',
|
|
15
|
+
'7' => 'other',
|
|
16
|
+
'8' => 'emancipation',
|
|
17
|
+
'9' => 'interdiction'
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
FIELD_SIZES = [6, 2, 2, 4, 1, 5, 3, 7, 2].freeze
|
|
21
|
+
FIELD_KEYS = %i[registryCns acervo service year type book page term checkDigits].freeze
|
|
22
|
+
|
|
23
|
+
# @private
|
|
24
|
+
def self.apply_mask(digits, group_sizes, separators)
|
|
25
|
+
chunks = []
|
|
26
|
+
idx = 0
|
|
27
|
+
group_sizes.each do |size|
|
|
28
|
+
break if idx >= digits.length
|
|
29
|
+
|
|
30
|
+
chunks << digits[idx, size]
|
|
31
|
+
idx += size
|
|
32
|
+
end
|
|
33
|
+
chunks.each_with_index.map { |c, i| i.zero? ? c : "#{separators[i - 1]}#{c}" }.join
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private_class_method :apply_mask
|
|
37
|
+
|
|
38
|
+
# Formats a matrícula into the printed groups (6-2-2-4-1-5-3-7-2),
|
|
39
|
+
# separated by spaces, applied as far as the digits go.
|
|
40
|
+
#
|
|
41
|
+
# @param value [String, Integer]
|
|
42
|
+
# @param options [Hash] `:pad` left-pads with zeros to 32 digits first.
|
|
43
|
+
# @return [String]
|
|
44
|
+
def self.format(value, options = {})
|
|
45
|
+
digits = value.to_s.gsub(/\D/, '')
|
|
46
|
+
digits = digits.rjust(32, '0') if options[:pad] || options['pad']
|
|
47
|
+
return '' if digits.empty?
|
|
48
|
+
|
|
49
|
+
apply_mask(digits, FIELD_SIZES, [' '] * 8)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Removes the formatting of a certidão matrícula and keeps only
|
|
53
|
+
# digits, capped to 32 digits.
|
|
54
|
+
#
|
|
55
|
+
# @param value [String, Integer]
|
|
56
|
+
# @return [String]
|
|
57
|
+
def self.parse(value)
|
|
58
|
+
return '' unless value.is_a?(String) || value.is_a?(Integer)
|
|
59
|
+
|
|
60
|
+
value.to_s.gsub(/\D/, '')[0, 32]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @private
|
|
64
|
+
#
|
|
65
|
+
# Computes a single modulus-11 check digit over `base`: the weight
|
|
66
|
+
# starts at 2 for the rightmost character and increases by 1 moving
|
|
67
|
+
# left (no cap, no wraparound).
|
|
68
|
+
def self.check_digit(base)
|
|
69
|
+
weights = (2...(2 + base.length)).to_a.reverse
|
|
70
|
+
sum = base.chars.each_with_index.sum { |d, i| d.to_i * weights[i] }
|
|
71
|
+
remainder = sum % 11
|
|
72
|
+
remainder < 2 ? 0 : 11 - remainder
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private_class_method :check_digit
|
|
76
|
+
|
|
77
|
+
# @private
|
|
78
|
+
#
|
|
79
|
+
# Only digits and the separators {space, '.', '-', '/'} are accepted
|
|
80
|
+
# anywhere in the string; any other character makes the whole value
|
|
81
|
+
# invalid. Returns the digits-only string, or nil when the input isn't
|
|
82
|
+
# a String/Integer or contains a disallowed character.
|
|
83
|
+
def self.clean_for_validation(value)
|
|
84
|
+
return nil unless value.is_a?(String) || value.is_a?(Integer)
|
|
85
|
+
|
|
86
|
+
raw = value.to_s
|
|
87
|
+
return nil unless raw.match?(%r{\A[\d\s.\-/]*\z})
|
|
88
|
+
|
|
89
|
+
raw.gsub(%r{[\s.\-/]}, '')
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private_class_method :clean_for_validation
|
|
93
|
+
|
|
94
|
+
# @private
|
|
95
|
+
def self.extract_fields(digits)
|
|
96
|
+
fields = {}
|
|
97
|
+
idx = 0
|
|
98
|
+
FIELD_KEYS.each_with_index do |key, i|
|
|
99
|
+
size = FIELD_SIZES[i]
|
|
100
|
+
fields[key] = digits[idx, size]
|
|
101
|
+
idx += size
|
|
102
|
+
end
|
|
103
|
+
fields
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private_class_method :extract_fields
|
|
107
|
+
|
|
108
|
+
# Checks whether a certidão matrícula is structurally valid and its 2
|
|
109
|
+
# modulus-11 check digits match.
|
|
110
|
+
#
|
|
111
|
+
# @param value [String, Integer] The matrícula, bare or separated by
|
|
112
|
+
# any mix of space, `.`, `-` or `/`. Any other character (a letter,
|
|
113
|
+
# for instance) anywhere in the value makes it invalid.
|
|
114
|
+
# @param options [Hash] `:accept` an optional Array narrowing which
|
|
115
|
+
# book-type codes (as the raw digit string, e.g. `"1"`) or names
|
|
116
|
+
# (e.g. `"birth"`) are accepted; defaults to accepting all of 1-9.
|
|
117
|
+
# @return [Boolean]
|
|
118
|
+
def self.is_valid(value, options = {})
|
|
119
|
+
digits = clean_for_validation(value)
|
|
120
|
+
return false unless digits
|
|
121
|
+
return false unless digits.match?(/\A\d{32}\z/)
|
|
122
|
+
|
|
123
|
+
raw = extract_fields(digits)
|
|
124
|
+
return false unless raw[:service] == '55'
|
|
125
|
+
return false unless BOOK_TYPES.key?(raw[:type])
|
|
126
|
+
|
|
127
|
+
accept = options[:accept] || options['accept']
|
|
128
|
+
if accept
|
|
129
|
+
accepted = Array(accept).map(&:to_s)
|
|
130
|
+
return false unless accepted.include?(raw[:type]) || accepted.include?(BOOK_TYPES[raw[:type]])
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
base30 = digits[0, 30]
|
|
134
|
+
dv1 = check_digit(base30)
|
|
135
|
+
dv2 = check_digit(base30 + dv1.to_s)
|
|
136
|
+
|
|
137
|
+
raw[:checkDigits] == "#{dv1}#{dv2}"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
class << self
|
|
141
|
+
alias valid? is_valid
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Parses a certidão matrícula into its fields.
|
|
145
|
+
#
|
|
146
|
+
# @param value [String, Integer] Same accepted forms as {is_valid}.
|
|
147
|
+
# @param options [Hash] Same as {is_valid}.
|
|
148
|
+
# @return [Hash, nil] `nil` whenever {is_valid} would return false;
|
|
149
|
+
# otherwise a Hash with `:registryCns`, `:acervo`, `:service`,
|
|
150
|
+
# `:year` (Integer), `:type` (the book-type name), `:book`, `:page`,
|
|
151
|
+
# `:term` and `:checkDigits`.
|
|
152
|
+
def self.get_info(value, options = {})
|
|
153
|
+
return nil unless is_valid(value, options)
|
|
154
|
+
|
|
155
|
+
digits = clean_for_validation(value)
|
|
156
|
+
raw = extract_fields(digits)
|
|
157
|
+
|
|
158
|
+
{
|
|
159
|
+
registryCns: raw[:registryCns],
|
|
160
|
+
acervo: raw[:acervo],
|
|
161
|
+
service: raw[:service],
|
|
162
|
+
year: raw[:year].to_i,
|
|
163
|
+
type: BOOK_TYPES[raw[:type]],
|
|
164
|
+
book: raw[:book],
|
|
165
|
+
page: raw[:page],
|
|
166
|
+
term: raw[:term],
|
|
167
|
+
checkDigits: raw[:checkDigits]
|
|
168
|
+
}
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
module BrazilianUtils
|
|
4
|
+
# Utilities for the CFOP (Código Fiscal de Operações e Prestações) table,
|
|
5
|
+
# the consolidated Anexo II of Convênio SINIEF s/nº 1970 in force. Only
|
|
6
|
+
# operable codes are included (group/subgroup headings are excluded).
|
|
7
|
+
module CFOPUtils
|
|
8
|
+
DATA_FILE = File.join(File.dirname(__FILE__), 'data', 'cfop.json')
|
|
9
|
+
|
|
10
|
+
# @private
|
|
11
|
+
def self.load_data
|
|
12
|
+
@data ||= JSON.parse(File.read(DATA_FILE))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private_class_method :load_data
|
|
16
|
+
|
|
17
|
+
# Normalizes a CFOP value to its bare 4 digits, honoring the `N.NNN`
|
|
18
|
+
# single-separator mask; returns nil for anything else (nothing is
|
|
19
|
+
# padded, since no CFOP starts with a zero).
|
|
20
|
+
#
|
|
21
|
+
# @private
|
|
22
|
+
def self.normalize(value)
|
|
23
|
+
return nil if value.nil?
|
|
24
|
+
return nil unless value.is_a?(String) || value.is_a?(Integer)
|
|
25
|
+
|
|
26
|
+
raw = value.to_s.strip
|
|
27
|
+
return raw if raw.match?(/\A\d{4}\z/)
|
|
28
|
+
|
|
29
|
+
m = raw.match(%r{\A(\d)[ .\-/](\d{3})\z})
|
|
30
|
+
m && "#{m[1]}#{m[2]}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private_class_method :normalize
|
|
34
|
+
|
|
35
|
+
# Checks whether a CFOP code exists in the official table (group and
|
|
36
|
+
# subgroup headings are not operable codes and are rejected).
|
|
37
|
+
#
|
|
38
|
+
# @param value [String, Integer]
|
|
39
|
+
# @return [Boolean]
|
|
40
|
+
def self.is_valid(value)
|
|
41
|
+
code = normalize(value)
|
|
42
|
+
return false unless code
|
|
43
|
+
|
|
44
|
+
load_data.key?(code)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class << self
|
|
48
|
+
alias valid? is_valid
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Looks a CFOP code up and returns its code and description.
|
|
52
|
+
#
|
|
53
|
+
# @param value [String, Integer]
|
|
54
|
+
# @return [Hash, nil]
|
|
55
|
+
def self.get(value)
|
|
56
|
+
code = normalize(value)
|
|
57
|
+
return nil unless code
|
|
58
|
+
|
|
59
|
+
description = load_data[code]
|
|
60
|
+
description && { code: code, description: description }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Removes CFOP formatting and keeps only digits, capped to 4 digits
|
|
64
|
+
# (nothing is padded).
|
|
65
|
+
#
|
|
66
|
+
# @param value [String, Integer]
|
|
67
|
+
# @return [String]
|
|
68
|
+
def self.parse(value)
|
|
69
|
+
return '' unless value.is_a?(String) || value.is_a?(Integer)
|
|
70
|
+
|
|
71
|
+
value.to_s.gsub(/\D/, '')[0, 4]
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
module BrazilianUtils
|
|
4
|
+
# Utilities for the CNAE-Subclasses 2.3 table (the current subclass
|
|
5
|
+
# revision of CNAE 2.0), sourced from the IBGE CONCLA API.
|
|
6
|
+
module CNAEUtils
|
|
7
|
+
DATA_FILE = File.join(File.dirname(__FILE__), 'data', 'cnae.json')
|
|
8
|
+
|
|
9
|
+
# @private
|
|
10
|
+
def self.load_data
|
|
11
|
+
@data ||= JSON.parse(File.read(DATA_FILE))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private_class_method :load_data
|
|
15
|
+
|
|
16
|
+
# @private
|
|
17
|
+
def self.apply_mask(digits, group_sizes, separators)
|
|
18
|
+
chunks = []
|
|
19
|
+
idx = 0
|
|
20
|
+
group_sizes.each do |size|
|
|
21
|
+
break if idx >= digits.length
|
|
22
|
+
|
|
23
|
+
chunks << digits[idx, size]
|
|
24
|
+
idx += size
|
|
25
|
+
end
|
|
26
|
+
chunks.each_with_index.map { |c, i| i.zero? ? c : "#{separators[i - 1]}#{c}" }.join
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private_class_method :apply_mask
|
|
30
|
+
|
|
31
|
+
# Normalizes a CNAE value to its bare 7 digits: bare digits (string or
|
|
32
|
+
# integer) are left-padded with zeros to 7; the `NNNN-N/NN` mask is
|
|
33
|
+
# read as written. Any other string is rejected.
|
|
34
|
+
#
|
|
35
|
+
# @private
|
|
36
|
+
def self.normalize(value)
|
|
37
|
+
return nil if value.nil?
|
|
38
|
+
return value.to_s.rjust(7, '0') if value.is_a?(Integer)
|
|
39
|
+
return nil unless value.is_a?(String)
|
|
40
|
+
|
|
41
|
+
raw = value.strip
|
|
42
|
+
return raw.rjust(7, '0') if raw.match?(/\A\d+\z/)
|
|
43
|
+
|
|
44
|
+
m = raw.match(%r{\A(\d{4})-(\d)/(\d{2})\z})
|
|
45
|
+
m && "#{m[1]}#{m[2]}#{m[3]}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private_class_method :normalize
|
|
49
|
+
|
|
50
|
+
# Formats a CNAE subclass code with the mask `NNNN-N/NN`; the mask is
|
|
51
|
+
# applied as far as the digits go (only the structure changes — use
|
|
52
|
+
# {is_valid} to check the code).
|
|
53
|
+
#
|
|
54
|
+
# @param value [String, Integer]
|
|
55
|
+
# @param options [Hash] `:pad` left-pads with zeros to 7 digits first.
|
|
56
|
+
# @return [String] An empty string when there is no digit at all.
|
|
57
|
+
def self.format(value, options = {})
|
|
58
|
+
digits = value.to_s.gsub(/\D/, '')
|
|
59
|
+
digits = digits.rjust(7, '0') if options[:pad] || options['pad']
|
|
60
|
+
return '' if digits.empty?
|
|
61
|
+
|
|
62
|
+
apply_mask(digits, [4, 1, 2], ['-', '/'])
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Checks whether a CNAE subclass code exists in the table.
|
|
66
|
+
#
|
|
67
|
+
# @param value [String, Integer]
|
|
68
|
+
# @return [Boolean]
|
|
69
|
+
def self.is_valid(value)
|
|
70
|
+
code = normalize(value)
|
|
71
|
+
return false unless code
|
|
72
|
+
|
|
73
|
+
load_data.key?(code)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
class << self
|
|
77
|
+
alias valid? is_valid
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Looks a CNAE subclass code up and returns its (bare, 7-digit) code
|
|
81
|
+
# and official description.
|
|
82
|
+
#
|
|
83
|
+
# @param value [String, Integer]
|
|
84
|
+
# @return [Hash, nil]
|
|
85
|
+
def self.get(value)
|
|
86
|
+
code = normalize(value)
|
|
87
|
+
return nil unless code
|
|
88
|
+
|
|
89
|
+
description = load_data[code]
|
|
90
|
+
description && { code: code, description: description }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Removes CNAE formatting and keeps only digits, capped to 7 digits
|
|
94
|
+
# (nothing is left-padded).
|
|
95
|
+
#
|
|
96
|
+
# @param value [String, Integer]
|
|
97
|
+
# @return [String]
|
|
98
|
+
def self.parse(value)
|
|
99
|
+
return '' unless value.is_a?(String) || value.is_a?(Integer)
|
|
100
|
+
|
|
101
|
+
value.to_s.gsub(/\D/, '')[0, 7]
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -84,5 +84,58 @@ module BrazilianUtils
|
|
|
84
84
|
end
|
|
85
85
|
|
|
86
86
|
private_class_method :calculate_first_rest, :check_second_verificator
|
|
87
|
+
|
|
88
|
+
# Formats a CNH number as `000000000-00` (9 digits, hyphen, 2 check
|
|
89
|
+
# digits). The mask is applied as far as the digits go.
|
|
90
|
+
#
|
|
91
|
+
# @param value [String, Integer] The value to format.
|
|
92
|
+
# @param options [Hash] `:pad` left-pads the value with zeros to 11
|
|
93
|
+
# digits first.
|
|
94
|
+
# @return [String]
|
|
95
|
+
#
|
|
96
|
+
# @example
|
|
97
|
+
# format("00000000119") #=> "000000001-19"
|
|
98
|
+
def self.format(value, options = {})
|
|
99
|
+
digits = value.to_s.gsub(/\D/, '')
|
|
100
|
+
digits = digits.rjust(11, '0') if options[:pad] || options['pad']
|
|
101
|
+
return digits if digits.length <= 9
|
|
102
|
+
|
|
103
|
+
"#{digits[0, 9]}-#{digits[9..-1]}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Removes CNH formatting and keeps only digits, capped to 11 digits.
|
|
107
|
+
#
|
|
108
|
+
# @param value [String, Integer]
|
|
109
|
+
# @return [String]
|
|
110
|
+
#
|
|
111
|
+
# @example
|
|
112
|
+
# parse("000000001-19") #=> "00000000119"
|
|
113
|
+
def self.parse(value)
|
|
114
|
+
return '' unless value.is_a?(String) || value.is_a?(Integer)
|
|
115
|
+
|
|
116
|
+
value.to_s.gsub(/\D/, '')[0, 11]
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Generates a valid random CNH number (11 digits, unformatted).
|
|
120
|
+
#
|
|
121
|
+
# @return [String]
|
|
122
|
+
def self.generate
|
|
123
|
+
loop do
|
|
124
|
+
base9 = 9.times.map { rand(0..9) }
|
|
125
|
+
next if base9.uniq.length == 1
|
|
126
|
+
|
|
127
|
+
first_rest = calculate_first_rest(base9)
|
|
128
|
+
first_verificator = first_rest > 9 ? 0 : first_rest
|
|
129
|
+
|
|
130
|
+
sum2 = 0
|
|
131
|
+
9.times { |i| sum2 += base9[i] * (i + 1) }
|
|
132
|
+
result = sum2 % 11
|
|
133
|
+
result = (result - 2).negative? ? result + 9 : result - 2 if first_rest > 9
|
|
134
|
+
second_verificator = result > 9 ? 0 : result
|
|
135
|
+
|
|
136
|
+
cnh = "#{base9.join}#{first_verificator}#{second_verificator}"
|
|
137
|
+
return cnh if valid?(cnh)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
87
140
|
end
|
|
88
141
|
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
module BrazilianUtils
|
|
2
|
+
# Utilities for the CNO (Cadastro Nacional de Obras), which replaced the
|
|
3
|
+
# CEI for construction works and kept its numbering/check-digit rule.
|
|
4
|
+
module CNOUtils
|
|
5
|
+
WEIGHTS = [7, 4, 1, 8, 5, 2, 1, 6, 3, 7, 4].freeze
|
|
6
|
+
|
|
7
|
+
# @private
|
|
8
|
+
def self.apply_mask(digits, group_sizes, separators)
|
|
9
|
+
chunks = []
|
|
10
|
+
idx = 0
|
|
11
|
+
group_sizes.each do |size|
|
|
12
|
+
break if idx >= digits.length
|
|
13
|
+
|
|
14
|
+
chunks << digits[idx, size]
|
|
15
|
+
idx += size
|
|
16
|
+
end
|
|
17
|
+
chunks.each_with_index.map { |c, i| i.zero? ? c : "#{separators[i - 1]}#{c}" }.join
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private_class_method :apply_mask
|
|
21
|
+
|
|
22
|
+
# @private
|
|
23
|
+
def self.check_digit(base)
|
|
24
|
+
sum = base.chars.each_with_index.sum { |d, i| d.to_i * WEIGHTS[i] }
|
|
25
|
+
last_two = sum % 100
|
|
26
|
+
combined = (last_two / 10) + (last_two % 10)
|
|
27
|
+
(10 - (combined % 10)) % 10
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private_class_method :check_digit
|
|
31
|
+
|
|
32
|
+
# Validates a CNO: same rules as CEIUtils#is_valid.
|
|
33
|
+
#
|
|
34
|
+
# @param value [String, Integer]
|
|
35
|
+
# @return [Boolean]
|
|
36
|
+
def self.is_valid(value)
|
|
37
|
+
return false unless value.is_a?(String) || value.is_a?(Integer)
|
|
38
|
+
|
|
39
|
+
raw = value.to_s.strip
|
|
40
|
+
return false unless raw.match?(%r{\A[\d\s.\-/]+\z})
|
|
41
|
+
|
|
42
|
+
digits = raw.gsub(%r{[\s.\-/]}, '')
|
|
43
|
+
return false unless digits.match?(/\A\d{12}\z/)
|
|
44
|
+
return false if digits.chars.uniq.length == 1
|
|
45
|
+
|
|
46
|
+
digits[11].to_i == check_digit(digits[0, 11])
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class << self
|
|
50
|
+
alias valid? is_valid
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Formats a CNO with the mask `00.000.00000/00` (the CEI numbering).
|
|
54
|
+
#
|
|
55
|
+
# @param value [String, Integer]
|
|
56
|
+
# @param options [Hash] `:pad` left-pads with zeros to 12 digits first.
|
|
57
|
+
# @return [String]
|
|
58
|
+
def self.format(value, options = {})
|
|
59
|
+
digits = value.to_s.gsub(/\D/, '')
|
|
60
|
+
digits = digits.rjust(12, '0') if options[:pad] || options['pad']
|
|
61
|
+
return '' if digits.empty?
|
|
62
|
+
|
|
63
|
+
apply_mask(digits, [2, 3, 5, 2], ['.', '.', '/'])
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Removes CNO formatting and keeps only digits, capped to 12 digits.
|
|
67
|
+
#
|
|
68
|
+
# @param value [String, Integer]
|
|
69
|
+
# @return [String]
|
|
70
|
+
def self.parse(value)
|
|
71
|
+
return '' unless value.is_a?(String) || value.is_a?(Integer)
|
|
72
|
+
|
|
73
|
+
value.to_s.gsub(/\D/, '')[0, 12]
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -120,34 +120,123 @@ module BrazilianUtils
|
|
|
120
120
|
# validates the format of the string.
|
|
121
121
|
#
|
|
122
122
|
# @param cnpj [String] The CNPJ to be validated, a 14-digit string
|
|
123
|
+
# (v1, numeric) or a 14-character alphanumeric string (v2, when
|
|
124
|
+
# `version: 2` is given).
|
|
125
|
+
# @param version [Integer] `1` (default) validates the classic
|
|
126
|
+
# all-numeric CNPJ; `2` validates the alphanumeric CNPJ introduced by
|
|
127
|
+
# IN RFB 2.119.
|
|
123
128
|
# @return [Boolean] true if the checksum digits match the base number, false otherwise.
|
|
124
129
|
#
|
|
125
130
|
# @example
|
|
126
|
-
# valid?("03560714000142")
|
|
127
|
-
# valid?("00111222000133")
|
|
128
|
-
|
|
129
|
-
|
|
131
|
+
# valid?("03560714000142") #=> true
|
|
132
|
+
# valid?("00111222000133") #=> false
|
|
133
|
+
# valid?("12ABC34501DE35", version: 2)
|
|
134
|
+
def self.valid?(cnpj, version: 1)
|
|
135
|
+
return false unless cnpj.is_a?(String)
|
|
136
|
+
|
|
137
|
+
version.to_i == 2 ? valid_v2?(cnpj) : validate(cnpj)
|
|
130
138
|
end
|
|
131
139
|
|
|
132
140
|
# Generates a random valid CNPJ digit string.
|
|
133
141
|
#
|
|
134
|
-
# An optional branch number parameter can be given; it defaults to 1
|
|
142
|
+
# An optional branch number parameter can be given; it defaults to 1
|
|
143
|
+
# (v1) or a random branch 1-9999 (v2, when not given).
|
|
135
144
|
#
|
|
136
|
-
# @param branch [Integer] An optional branch number to be included
|
|
145
|
+
# @param branch [Integer, nil] An optional branch number to be included
|
|
146
|
+
# in the CNPJ.
|
|
147
|
+
# @param version [Integer] `1` (default) generates the classic
|
|
148
|
+
# all-numeric CNPJ; `2` generates the alphanumeric CNPJ.
|
|
137
149
|
# @return [String] A randomly generated valid CNPJ string.
|
|
138
150
|
#
|
|
139
151
|
# @example
|
|
140
|
-
# generate()
|
|
141
|
-
# generate(1234)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
branch
|
|
145
|
-
|
|
152
|
+
# generate() #=> "30180536000105"
|
|
153
|
+
# generate(branch: 1234) #=> "01745284123455"
|
|
154
|
+
# generate(version: 2) #=> "12ABC34501DE35"
|
|
155
|
+
def self.generate(branch: nil, version: 1)
|
|
156
|
+
return generate_v2(branch) if version.to_i == 2
|
|
157
|
+
|
|
158
|
+
branch_num = branch.nil? ? 1 : branch % 10_000
|
|
159
|
+
branch_num = 1 if branch_num.zero?
|
|
160
|
+
branch_str = branch_num.to_s.rjust(4, '0')
|
|
146
161
|
base = format('%08d', rand(100_000_000)) + branch_str
|
|
147
162
|
|
|
148
163
|
base + checksum(base)
|
|
149
164
|
end
|
|
150
165
|
|
|
166
|
+
# V2 (ALPHANUMERIC, IN RFB 2.119)
|
|
167
|
+
#################################
|
|
168
|
+
|
|
169
|
+
# Characters usable in the alphanumeric CNPJ's base (positions 0-11).
|
|
170
|
+
V2_CHARSET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'.freeze
|
|
171
|
+
|
|
172
|
+
V2_WEIGHTS_DV1 = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2].freeze
|
|
173
|
+
V2_WEIGHTS_DV2 = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2].freeze
|
|
174
|
+
|
|
175
|
+
# Maps a base-36 character (`0`-`9`, `A`-`Z`) to its numeric value for
|
|
176
|
+
# the v2 checksum: `'0'`..`'9'` -> 0..9, `'A'`..`'Z'` -> 17..42 (its
|
|
177
|
+
# ASCII code minus 48).
|
|
178
|
+
#
|
|
179
|
+
# @private
|
|
180
|
+
def self.v2_char_value(char)
|
|
181
|
+
char.ord - 48
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
private_class_method :v2_char_value
|
|
185
|
+
|
|
186
|
+
# Computes a single modulus-11 check digit over `chars` (a String of
|
|
187
|
+
# `0`-`9`/`A`-`Z` characters) using the given per-position weights.
|
|
188
|
+
#
|
|
189
|
+
# @private
|
|
190
|
+
def self.v2_hashdigit(chars, weights)
|
|
191
|
+
sum = chars.chars.each_with_index.sum { |c, i| v2_char_value(c) * weights[i] }
|
|
192
|
+
mod = sum % 11
|
|
193
|
+
mod < 2 ? 0 : 11 - mod
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
private_class_method :v2_hashdigit
|
|
197
|
+
|
|
198
|
+
# Validates a 14-character alphanumeric CNPJ (case-insensitive).
|
|
199
|
+
#
|
|
200
|
+
# @param cnpj [String]
|
|
201
|
+
# @return [Boolean]
|
|
202
|
+
#
|
|
203
|
+
# @private
|
|
204
|
+
def self.valid_v2?(cnpj)
|
|
205
|
+
upper = cnpj.to_s.upcase
|
|
206
|
+
return false unless upper.match?(/\A[0-9A-Z]{14}\z/)
|
|
207
|
+
|
|
208
|
+
dv1 = v2_hashdigit(upper[0, 12], V2_WEIGHTS_DV1)
|
|
209
|
+
return false unless dv1 == v2_char_value(upper[12])
|
|
210
|
+
|
|
211
|
+
dv2 = v2_hashdigit(upper[0, 13], V2_WEIGHTS_DV2)
|
|
212
|
+
dv2 == v2_char_value(upper[13])
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
private_class_method :valid_v2?
|
|
216
|
+
|
|
217
|
+
# Generates a random valid alphanumeric (v2) CNPJ.
|
|
218
|
+
#
|
|
219
|
+
# @param branch [Integer, nil] An optional branch number (1-9999); a
|
|
220
|
+
# random one is used when not given.
|
|
221
|
+
# @return [String]
|
|
222
|
+
#
|
|
223
|
+
# @private
|
|
224
|
+
def self.generate_v2(branch = nil)
|
|
225
|
+
branch_num = branch.nil? ? rand(1..9999) : branch % 10_000
|
|
226
|
+
branch_num = 1 if branch_num.zero?
|
|
227
|
+
branch_str = branch_num.to_s.rjust(4, '0')
|
|
228
|
+
|
|
229
|
+
base8 = Array.new(8) { V2_CHARSET[rand(V2_CHARSET.length)] }.join
|
|
230
|
+
base12 = base8 + branch_str
|
|
231
|
+
|
|
232
|
+
dv1 = v2_hashdigit(base12, V2_WEIGHTS_DV1)
|
|
233
|
+
dv2 = v2_hashdigit(base12 + dv1.to_s, V2_WEIGHTS_DV2)
|
|
234
|
+
|
|
235
|
+
"#{base12}#{dv1}#{dv2}"
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
private_class_method :generate_v2
|
|
239
|
+
|
|
151
240
|
# PRIVATE METHODS
|
|
152
241
|
#################
|
|
153
242
|
|
|
@@ -198,5 +287,29 @@ module BrazilianUtils
|
|
|
198
287
|
end
|
|
199
288
|
|
|
200
289
|
private_class_method :hashdigit, :checksum
|
|
290
|
+
|
|
291
|
+
# Removes CNPJ formatting and returns the normalized value, capped to 14
|
|
292
|
+
# characters.
|
|
293
|
+
#
|
|
294
|
+
# @param value [String, Integer] A CNPJ, with or without formatting.
|
|
295
|
+
# @param options [Hash] `:version` `1` (default) keeps digits only; `2`
|
|
296
|
+
# keeps letters and digits, upper-cased (the alphanumeric CNPJ format).
|
|
297
|
+
# @return [String] The parsed value.
|
|
298
|
+
#
|
|
299
|
+
# @example
|
|
300
|
+
# parse("46.843.485/0001-86") #=> "46843485000186"
|
|
301
|
+
def self.parse(value, options = {})
|
|
302
|
+
return '' unless value.is_a?(String) || value.is_a?(Integer)
|
|
303
|
+
|
|
304
|
+
version = (options[:version] || options['version'] || 1).to_i
|
|
305
|
+
|
|
306
|
+
cleaned = if version == 2
|
|
307
|
+
value.to_s.gsub(/[^a-zA-Z0-9]/, '').upcase
|
|
308
|
+
else
|
|
309
|
+
value.to_s.gsub(/\D/, '')
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
cleaned[0, 14]
|
|
313
|
+
end
|
|
201
314
|
end
|
|
202
315
|
end
|