br-utils 0.1.0 → 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/.github/workflows/ci.yml +27 -0
- 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 +34 -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 +68 -15
- data/lib/brazilian-utils/cno-utils.rb +76 -0
- data/lib/brazilian-utils/cnpj-utils.rb +126 -13
- data/lib/brazilian-utils/cns-utils.rb +110 -0
- data/lib/brazilian-utils/cpf-utils.rb +14 -1
- 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 -226
- 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 -244
- data/lib/brazilian-utils/email-utils.rb +20 -9
- 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 +35 -5
- data/lib/brazilian-utils/license-plate-utils.rb +24 -4
- 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 -272
- 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 +70 -19
- metadata +54 -2
- data/.travis.yml +0 -5
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
module BrazilianUtils
|
|
4
|
+
# Utilities for looking up Brazilian area codes (DDD), sourced from the
|
|
5
|
+
# Anatel Plano Geral de Numeração.
|
|
6
|
+
module AreaCodeUtils
|
|
7
|
+
DATA_FILE = File.join(File.dirname(__FILE__), 'data', 'area_codes.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.to_symbolized(row)
|
|
18
|
+
{
|
|
19
|
+
areaCode: row['areaCode'],
|
|
20
|
+
stateCode: row['stateCode'],
|
|
21
|
+
stateName: row['stateName'],
|
|
22
|
+
regionCode: row['regionCode'],
|
|
23
|
+
regionName: row['regionName'],
|
|
24
|
+
stateCodes: row['stateCodes']
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private_class_method :to_symbolized
|
|
29
|
+
|
|
30
|
+
# Returns the state and region a DDD (area code) belongs to.
|
|
31
|
+
#
|
|
32
|
+
# @param area_code [String, Integer]
|
|
33
|
+
# @return [Hash, nil]
|
|
34
|
+
def self.get_info(area_code)
|
|
35
|
+
return nil if area_code.nil?
|
|
36
|
+
|
|
37
|
+
str = area_code.to_s.strip
|
|
38
|
+
return nil unless str.match?(/\A\d+\z/)
|
|
39
|
+
|
|
40
|
+
row = load_data.find { |r| r['areaCode'] == str.to_i }
|
|
41
|
+
row && to_symbolized(row)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Returns every DDD that serves a state, sorted ascending.
|
|
45
|
+
#
|
|
46
|
+
# @param state_code [String]
|
|
47
|
+
# @return [Array<Integer>]
|
|
48
|
+
def self.list_by_state(state_code)
|
|
49
|
+
return [] unless state_code.is_a?(String)
|
|
50
|
+
|
|
51
|
+
normalized = state_code.strip.upcase
|
|
52
|
+
return [] if normalized.empty?
|
|
53
|
+
|
|
54
|
+
load_data
|
|
55
|
+
.select { |r| r['stateCodes'].include?(normalized) }
|
|
56
|
+
.map { |r| r['areaCode'] }
|
|
57
|
+
.sort
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
require_relative 'bank-utils'
|
|
2
|
+
|
|
3
|
+
module BrazilianUtils
|
|
4
|
+
# Utilities for validating Brazilian bank accounts (bank code, agency,
|
|
5
|
+
# account and check digit).
|
|
6
|
+
#
|
|
7
|
+
# @note This implementation validates the account's *structure* (a known
|
|
8
|
+
# COMPE `bankCode`, agency/account digit-count limits, an allowed
|
|
9
|
+
# `digit` character) and a generic modulus 10 / modulus 11 fallback
|
|
10
|
+
# check digit. The distinct, published check-digit algorithms some
|
|
11
|
+
# banks use (Banco do Brasil, Santander, Banrisul, Caixa, Bradesco,
|
|
12
|
+
# Nubank/Verhoeff, Itaú, HSBC/Kirton, Citibank) are **not** implemented:
|
|
13
|
+
# there were no verifiable test vectors available to confirm a from-
|
|
14
|
+
# scratch reproduction of each algorithm, and shipping an unverified
|
|
15
|
+
# guess would be worse than this honestly-partial generic check.
|
|
16
|
+
module BankAccountUtils
|
|
17
|
+
# @private
|
|
18
|
+
def self.mod10_digit(digits)
|
|
19
|
+
sum = 0
|
|
20
|
+
digits.reverse.each_char.with_index do |ch, i|
|
|
21
|
+
d = ch.to_i
|
|
22
|
+
d *= 2 if i.even?
|
|
23
|
+
d -= 9 if d > 9
|
|
24
|
+
sum += d
|
|
25
|
+
end
|
|
26
|
+
(10 - (sum % 10)) % 10
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private_class_method :mod10_digit
|
|
30
|
+
|
|
31
|
+
# @private
|
|
32
|
+
def self.mod11_digit(digits)
|
|
33
|
+
weights = [2, 3, 4, 5, 6, 7, 8, 9]
|
|
34
|
+
sum = 0
|
|
35
|
+
digits.reverse.each_char.with_index do |ch, i|
|
|
36
|
+
sum += ch.to_i * weights[i % weights.length]
|
|
37
|
+
end
|
|
38
|
+
rest = 11 - (sum % 11)
|
|
39
|
+
rest >= 10 ? 0 : rest
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private_class_method :mod11_digit
|
|
43
|
+
|
|
44
|
+
# Validates a Brazilian bank account.
|
|
45
|
+
#
|
|
46
|
+
# @param params [Hash] `:bankCode` (3 digits), `:agency` (1-5 digits),
|
|
47
|
+
# `:account` (1-13 digits) and `:digit` (1-2 characters, or the
|
|
48
|
+
# literal `X`/`P` used by some banks), all strings.
|
|
49
|
+
# @return [Boolean]
|
|
50
|
+
def self.is_valid(params)
|
|
51
|
+
return false unless params.is_a?(Hash)
|
|
52
|
+
|
|
53
|
+
bank_code = params[:bankCode] || params['bankCode']
|
|
54
|
+
agency = params[:agency] || params['agency']
|
|
55
|
+
account = params[:account] || params['account']
|
|
56
|
+
digit = params[:digit] || params['digit']
|
|
57
|
+
|
|
58
|
+
return false unless [bank_code, agency, account, digit].all? { |v| v.is_a?(String) }
|
|
59
|
+
return false unless BankUtils.get_by_code(bank_code)
|
|
60
|
+
return false unless agency.match?(/\A\d{1,5}\z/)
|
|
61
|
+
return false unless account.match?(/\A\d{1,13}\z/)
|
|
62
|
+
return false unless digit.match?(/\A[0-9A-Za-z]{1,2}\z/)
|
|
63
|
+
|
|
64
|
+
digit_upcase = digit.upcase
|
|
65
|
+
return true if %w[X P].include?(digit_upcase) && digit.length == 1
|
|
66
|
+
|
|
67
|
+
return false unless digit.match?(/\A\d{1,2}\z/)
|
|
68
|
+
|
|
69
|
+
if digit.length == 2
|
|
70
|
+
first_ok = digit[0].to_i == mod10_digit(account)
|
|
71
|
+
second_ok = digit[1].to_i == mod11_digit(account + digit[0])
|
|
72
|
+
first_ok && second_ok
|
|
73
|
+
else
|
|
74
|
+
digit.to_i == mod10_digit(account) || digit.to_i == mod11_digit(account)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
class << self
|
|
79
|
+
alias valid? is_valid
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
module BrazilianUtils
|
|
4
|
+
# Utilities for looking up Brazilian banks by COMPE code or ISPB, sourced
|
|
5
|
+
# from the Banco Central do Brasil STR participants list.
|
|
6
|
+
module BankUtils
|
|
7
|
+
DATA_FILE = File.join(File.dirname(__FILE__), 'data', 'banks.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.to_symbolized(row)
|
|
18
|
+
{ code: row['code'], ispb: row['ispb'], name: row['name'] }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private_class_method :to_symbolized
|
|
22
|
+
|
|
23
|
+
# Looks a bank up by its 3-digit COMPE code.
|
|
24
|
+
#
|
|
25
|
+
# @param code [String, Integer] A number is read as the zero-padded
|
|
26
|
+
# 3-digit code.
|
|
27
|
+
# @return [Hash, nil]
|
|
28
|
+
def self.get_by_code(code)
|
|
29
|
+
return nil if code.nil?
|
|
30
|
+
|
|
31
|
+
str = code.to_s.strip
|
|
32
|
+
return nil unless str.match?(/\A\d+\z/)
|
|
33
|
+
|
|
34
|
+
padded = str.rjust(3, '0')
|
|
35
|
+
row = load_data.find { |r| r['code'] == padded }
|
|
36
|
+
row && to_symbolized(row)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Looks a bank up by its 8-digit ISPB.
|
|
40
|
+
#
|
|
41
|
+
# @param value [String, Integer] With or without leading zeros.
|
|
42
|
+
# @return [Hash, nil]
|
|
43
|
+
def self.get_by_ispb(value)
|
|
44
|
+
return nil if value.nil?
|
|
45
|
+
|
|
46
|
+
str = value.to_s.strip
|
|
47
|
+
return nil unless str.match?(/\A\d+\z/)
|
|
48
|
+
|
|
49
|
+
padded = str.rjust(8, '0')
|
|
50
|
+
row = load_data.find { |r| r['ispb'] == padded }
|
|
51
|
+
row && to_symbolized(row)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Returns every bank with a COMPE code.
|
|
55
|
+
#
|
|
56
|
+
# @return [Array<Hash>]
|
|
57
|
+
def self.list
|
|
58
|
+
load_data.map { |row| to_symbolized(row) }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'date'
|
|
4
|
+
|
|
3
5
|
module BrazilianUtils
|
|
4
6
|
module BoletoUtils
|
|
7
|
+
# The fator de vencimento epoch (day 1000) before the 2025-02-22 cycle
|
|
8
|
+
# reset, and how many days apart the two candidate dates for a given
|
|
9
|
+
# factor are, per the contract's own description. Not independently
|
|
10
|
+
# verified against a reference implementation (no test cases were
|
|
11
|
+
# available for `boleto.getInfo`).
|
|
12
|
+
FATOR_VENCIMENTO_OLD_EPOCH = Date.new(1997, 10, 7)
|
|
13
|
+
FATOR_VENCIMENTO_CYCLE_GAP_DAYS = 9000
|
|
5
14
|
# Every Digitable Line from Boleto has exactly 47 characters
|
|
6
15
|
DIGITABLE_LINE_LENGTH = 47
|
|
7
16
|
|
|
@@ -52,6 +61,136 @@ module BrazilianUtils
|
|
|
52
61
|
# Alias for is_valid
|
|
53
62
|
alias valid? is_valid
|
|
54
63
|
|
|
64
|
+
# Removes boleto formatting and keeps only digits, capped to 47
|
|
65
|
+
# digits (48 for a boleto de arrecadação, recognized by a leading
|
|
66
|
+
# `8`).
|
|
67
|
+
#
|
|
68
|
+
# @param value [String, Integer]
|
|
69
|
+
# @return [String]
|
|
70
|
+
def parse(value)
|
|
71
|
+
return '' unless value.is_a?(String) || value.is_a?(Integer)
|
|
72
|
+
|
|
73
|
+
digits = value.to_s.gsub(/\D/, '')
|
|
74
|
+
return '' if digits.empty?
|
|
75
|
+
|
|
76
|
+
cap = digits[0] == '8' ? 48 : 47
|
|
77
|
+
digits[0, cap]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @private
|
|
81
|
+
def apply_mask(digits, group_sizes, separators)
|
|
82
|
+
chunks = []
|
|
83
|
+
idx = 0
|
|
84
|
+
group_sizes.each do |size|
|
|
85
|
+
break if idx >= digits.length
|
|
86
|
+
|
|
87
|
+
chunks << digits[idx, size]
|
|
88
|
+
idx += size
|
|
89
|
+
end
|
|
90
|
+
chunks.each_with_index.map { |c, i| i.zero? ? c : "#{separators[i - 1]}#{c}" }.join
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Formats a boleto linha digitável with its printed mask.
|
|
94
|
+
#
|
|
95
|
+
# @param value [String, Integer]
|
|
96
|
+
# @param options [Hash] `:pad` left-pads the value with zeros to the
|
|
97
|
+
# length of the pattern before masking.
|
|
98
|
+
# @return [String]
|
|
99
|
+
def format(value, options = {})
|
|
100
|
+
digits = value.to_s.gsub(/\D/, '')
|
|
101
|
+
pad = options[:pad] || options['pad']
|
|
102
|
+
|
|
103
|
+
if pad
|
|
104
|
+
target = digits[0] == '8' ? 48 : 47
|
|
105
|
+
digits = digits.rjust(target, '0')
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
return '' if digits.empty?
|
|
109
|
+
|
|
110
|
+
if digits.length == 48 && digits[0] == '8'
|
|
111
|
+
digits.chars.each_slice(12).map { |b| b.join }.map do |block|
|
|
112
|
+
block.length > 11 ? "#{block[0, 11]}-#{block[11]}" : block
|
|
113
|
+
end.join(' ')
|
|
114
|
+
else
|
|
115
|
+
apply_mask(digits, [5, 5, 5, 6, 5, 6, 1, 14], ['.', ' ', '.', ' ', '.', ' ', ' '])
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Generates a valid random boleto number.
|
|
120
|
+
#
|
|
121
|
+
# @param params [Hash] `:type` set to `"arrecadacao"` generates a
|
|
122
|
+
# 48-digit boleto de arrecadação instead of the default 47-digit
|
|
123
|
+
# cobrança bancária linha digitável.
|
|
124
|
+
# @return [String, nil] `nil` for `type: "arrecadacao"`: it is not
|
|
125
|
+
# implemented (this module's {is_valid} itself only recognizes the
|
|
126
|
+
# 47-digit cobrança bancária form, so a generated arrecadação
|
|
127
|
+
# number could not be verified to round-trip through it).
|
|
128
|
+
def generate(params = {})
|
|
129
|
+
type = params[:type] || params['type']
|
|
130
|
+
return nil if type.to_s == 'arrecadacao'
|
|
131
|
+
|
|
132
|
+
banco = sprintf('%03d', rand(1..999))
|
|
133
|
+
moeda = '9'
|
|
134
|
+
campo_livre = 25.times.map { rand(0..9) }.join
|
|
135
|
+
fator_vencimento = sprintf('%04d', rand(1000..9999))
|
|
136
|
+
valor = sprintf('%010d', rand(0..9_999_999_999))
|
|
137
|
+
|
|
138
|
+
campo1_free = campo_livre[0, 5]
|
|
139
|
+
campo2 = campo_livre[5, 10]
|
|
140
|
+
campo3 = campo_livre[15, 10]
|
|
141
|
+
|
|
142
|
+
campo1 = "#{banco}#{moeda}#{campo1_free}"
|
|
143
|
+
dv1 = get_mod10(campo1)
|
|
144
|
+
dv2 = get_mod10(campo2)
|
|
145
|
+
dv3 = get_mod10(campo3)
|
|
146
|
+
|
|
147
|
+
barcode_without_dv = "#{banco}#{moeda}#{fator_vencimento}#{valor}#{campo_livre}"
|
|
148
|
+
dv_geral = get_mod11(barcode_without_dv)
|
|
149
|
+
|
|
150
|
+
"#{campo1}#{dv1}#{campo2}#{dv2}#{campo3}#{dv3}#{dv_geral}#{fator_vencimento}#{valor}"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Extracts the amount, due date and bank code from a boleto.
|
|
154
|
+
#
|
|
155
|
+
# @note Only the 47-digit cobrança bancária form is supported; a
|
|
156
|
+
# boleto de arrecadação (48-digit linha digitável or 44-digit
|
|
157
|
+
# barcode) returns nil. The due-date resolution (fator de
|
|
158
|
+
# vencimento, including the 2025-02-22 cycle reset) has no
|
|
159
|
+
# available test cases and is unverified against a reference
|
|
160
|
+
# implementation.
|
|
161
|
+
#
|
|
162
|
+
# @param value [String]
|
|
163
|
+
# @param options [Hash] `:referenceDate` resolves the fator de
|
|
164
|
+
# vencimento cycle as of that date (default: today).
|
|
165
|
+
# @return [Hash, nil]
|
|
166
|
+
def get_info(value, options = {})
|
|
167
|
+
return nil unless is_valid(value)
|
|
168
|
+
|
|
169
|
+
digits = extract_only_numbers(value)
|
|
170
|
+
return nil unless digits.length == DIGITABLE_LINE_LENGTH
|
|
171
|
+
|
|
172
|
+
barcode = parse_digitable_line(digits)
|
|
173
|
+
bank_code = barcode[0, 3]
|
|
174
|
+
fator_vencimento = barcode[5, 4].to_i
|
|
175
|
+
amount_cents = barcode[9, 10].to_i
|
|
176
|
+
|
|
177
|
+
reference_date = options[:referenceDate] || options['referenceDate'] || Date.today
|
|
178
|
+
reference_date = reference_date.to_date if reference_date.respond_to?(:to_date)
|
|
179
|
+
|
|
180
|
+
due_date =
|
|
181
|
+
if fator_vencimento >= 1000
|
|
182
|
+
date_a = FATOR_VENCIMENTO_OLD_EPOCH + (fator_vencimento - 1000)
|
|
183
|
+
date_b = date_a + FATOR_VENCIMENTO_CYCLE_GAP_DAYS
|
|
184
|
+
(date_a - reference_date).abs <= (date_b - reference_date).abs ? date_a : date_b
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
{
|
|
188
|
+
bankCode: bank_code,
|
|
189
|
+
amount: amount_cents,
|
|
190
|
+
dueDate: due_date
|
|
191
|
+
}
|
|
192
|
+
end
|
|
193
|
+
|
|
55
194
|
private
|
|
56
195
|
|
|
57
196
|
# Extract only numeric characters from a string.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
module BrazilianUtils
|
|
2
|
+
# Utilities for the CAEPF (Cadastro de Atividade Econômica da Pessoa
|
|
3
|
+
# Física): 14 digits — a 9-digit CPF base, a 3-digit sequence and 2 check
|
|
4
|
+
# digits.
|
|
5
|
+
module CAEPFUtils
|
|
6
|
+
# @private
|
|
7
|
+
def self.apply_mask(digits, group_sizes, separators)
|
|
8
|
+
chunks = []
|
|
9
|
+
idx = 0
|
|
10
|
+
group_sizes.each do |size|
|
|
11
|
+
break if idx >= digits.length
|
|
12
|
+
|
|
13
|
+
chunks << digits[idx, size]
|
|
14
|
+
idx += size
|
|
15
|
+
end
|
|
16
|
+
chunks.each_with_index.map { |c, i| i.zero? ? c : "#{separators[i - 1]}#{c}" }.join
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private_class_method :apply_mask
|
|
20
|
+
|
|
21
|
+
# Computes a CNPJ-style modulus 11 check digit (the same algorithm
|
|
22
|
+
# `CNPJUtils` uses) over an arbitrary-length base.
|
|
23
|
+
#
|
|
24
|
+
# @private
|
|
25
|
+
def self.hashdigit(base, position)
|
|
26
|
+
weights = []
|
|
27
|
+
(position - 8).downto(2) { |w| weights << w }
|
|
28
|
+
9.downto(2) { |w| weights << w }
|
|
29
|
+
|
|
30
|
+
val = base.chars.first(position - 1).zip(weights).sum { |d, w| d.to_i * w } % 11
|
|
31
|
+
val < 2 ? 0 : 11 - val
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private_class_method :hashdigit
|
|
35
|
+
|
|
36
|
+
# @private
|
|
37
|
+
def self.check_digits(base12)
|
|
38
|
+
first = hashdigit(base12, 13)
|
|
39
|
+
second = hashdigit(base12 + first.to_s, 14)
|
|
40
|
+
((first * 10 + second) + 12) % 100
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private_class_method :check_digits
|
|
44
|
+
|
|
45
|
+
# Validates a CAEPF: 14 digits, with both check digits following the
|
|
46
|
+
# CNPJ modulus 11 rule, shifted by 12 (wrapping around 100).
|
|
47
|
+
#
|
|
48
|
+
# @param value [String, Integer] Masked as `000.000.000/000-00` or not.
|
|
49
|
+
# @return [Boolean]
|
|
50
|
+
def self.is_valid(value)
|
|
51
|
+
return false unless value.is_a?(String) || value.is_a?(Integer)
|
|
52
|
+
|
|
53
|
+
raw = value.to_s.strip
|
|
54
|
+
return false unless raw.match?(%r{\A[\d\s.\-/]+\z})
|
|
55
|
+
|
|
56
|
+
digits = raw.gsub(%r{[\s.\-/]}, '')
|
|
57
|
+
return false unless digits.match?(/\A\d{14}\z/)
|
|
58
|
+
return false if digits[0, 9].chars.uniq.length == 1
|
|
59
|
+
|
|
60
|
+
digits[12, 2].to_i == check_digits(digits[0, 12])
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
class << self
|
|
64
|
+
alias valid? is_valid
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Formats a CAEPF with the mask `000.000.000/000-00`, applied as far as
|
|
68
|
+
# the digits go.
|
|
69
|
+
#
|
|
70
|
+
# @param value [String, Integer]
|
|
71
|
+
# @param options [Hash] `:pad` left-pads with zeros to 14 digits first.
|
|
72
|
+
# @return [String]
|
|
73
|
+
def self.format(value, options = {})
|
|
74
|
+
digits = value.to_s.gsub(/\D/, '')
|
|
75
|
+
digits = digits.rjust(14, '0') if options[:pad] || options['pad']
|
|
76
|
+
return '' if digits.empty?
|
|
77
|
+
|
|
78
|
+
apply_mask(digits, [3, 3, 3, 3, 2], ['.', '.', '/', '-'])
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Removes CAEPF formatting and keeps only digits, capped to 14 digits.
|
|
82
|
+
#
|
|
83
|
+
# @param value [String, Integer]
|
|
84
|
+
# @return [String]
|
|
85
|
+
def self.parse(value)
|
|
86
|
+
return '' unless value.is_a?(String) || value.is_a?(Integer)
|
|
87
|
+
|
|
88
|
+
value.to_s.gsub(/\D/, '')[0, 14]
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
module BrazilianUtils
|
|
4
|
+
# Utilities for the CBO (Classificação Brasileira de Ocupações) 2002
|
|
5
|
+
# occupation table (6-digit "ocupação" granularity).
|
|
6
|
+
module CBOUtils
|
|
7
|
+
DATA_FILE = File.join(File.dirname(__FILE__), 'data', 'cbo.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
|
+
# Normalizes a CBO value to its bare 6 digits: bare digits (string or
|
|
17
|
+
# integer) are left-padded with zeros to 6; the `NNNN-NN` mask (a
|
|
18
|
+
# single separator between the groups) is read as written. Any other
|
|
19
|
+
# string is rejected (its digits are not picked out).
|
|
20
|
+
#
|
|
21
|
+
# @private
|
|
22
|
+
def self.normalize(value)
|
|
23
|
+
return nil if value.nil?
|
|
24
|
+
return value.to_s.rjust(6, '0') if value.is_a?(Integer)
|
|
25
|
+
return nil unless value.is_a?(String)
|
|
26
|
+
|
|
27
|
+
raw = value.strip
|
|
28
|
+
return raw.rjust(6, '0') if raw.match?(/\A\d+\z/)
|
|
29
|
+
|
|
30
|
+
m = raw.match(%r{\A(\d{4})[ .\-/](\d{2})\z})
|
|
31
|
+
m && "#{m[1]}#{m[2]}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private_class_method :normalize
|
|
35
|
+
|
|
36
|
+
# Checks whether a CBO code exists in the official table.
|
|
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 CBO code up and returns its (bare, 6-digit) code and title.
|
|
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 CBO formatting and keeps only digits, capped to 6 digits
|
|
64
|
+
# (nothing is left-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, 6]
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
module BrazilianUtils
|
|
2
|
+
# Utilities for the CEI (Cadastro Específico do INSS), a 12-digit
|
|
3
|
+
# registration number for a work/construction site or rural employer.
|
|
4
|
+
module CEIUtils
|
|
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 CEI: 12 digits, 11 base digits and one check digit.
|
|
33
|
+
#
|
|
34
|
+
# @param value [String, Integer] Bare digits, or split into the printed
|
|
35
|
+
# groups (2, 3, 5 and 2 digits) by whitespace or the usual mask
|
|
36
|
+
# characters (`.`, `-`, `/`).
|
|
37
|
+
# @return [Boolean]
|
|
38
|
+
def self.is_valid(value)
|
|
39
|
+
return false unless value.is_a?(String) || value.is_a?(Integer)
|
|
40
|
+
|
|
41
|
+
raw = value.to_s.strip
|
|
42
|
+
return false unless raw.match?(%r{\A[\d\s.\-/]+\z})
|
|
43
|
+
|
|
44
|
+
digits = raw.gsub(%r{[\s.\-/]}, '')
|
|
45
|
+
return false unless digits.match?(/\A\d{12}\z/)
|
|
46
|
+
return false if digits.chars.uniq.length == 1
|
|
47
|
+
|
|
48
|
+
digits[11].to_i == check_digit(digits[0, 11])
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
class << self
|
|
52
|
+
alias valid? is_valid
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Formats a CEI with the mask `00.000.00000/00`, applied as far as the
|
|
56
|
+
# digits go.
|
|
57
|
+
#
|
|
58
|
+
# @param value [String, Integer]
|
|
59
|
+
# @param options [Hash] `:pad` left-pads with zeros to 12 digits first.
|
|
60
|
+
# @return [String]
|
|
61
|
+
def self.format(value, options = {})
|
|
62
|
+
digits = value.to_s.gsub(/\D/, '')
|
|
63
|
+
digits = digits.rjust(12, '0') if options[:pad] || options['pad']
|
|
64
|
+
return '' if digits.empty?
|
|
65
|
+
|
|
66
|
+
apply_mask(digits, [2, 3, 5, 2], ['.', '.', '/'])
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Removes CEI formatting and keeps only digits, capped to 12 digits.
|
|
70
|
+
#
|
|
71
|
+
# @param value [String, Integer]
|
|
72
|
+
# @return [String]
|
|
73
|
+
def self.parse(value)
|
|
74
|
+
return '' unless value.is_a?(String) || value.is_a?(Integer)
|
|
75
|
+
|
|
76
|
+
value.to_s.gsub(/\D/, '')[0, 12]
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -57,9 +57,12 @@ module BrazilianUtils
|
|
|
57
57
|
:cep,
|
|
58
58
|
:logradouro,
|
|
59
59
|
:complemento,
|
|
60
|
+
:unidade,
|
|
60
61
|
:bairro,
|
|
61
62
|
:localidade,
|
|
62
63
|
:uf,
|
|
64
|
+
:estado,
|
|
65
|
+
:regiao,
|
|
63
66
|
:ibge,
|
|
64
67
|
:gia,
|
|
65
68
|
:ddd,
|
|
@@ -141,6 +144,20 @@ module BrazilianUtils
|
|
|
141
144
|
8.times.map { rand(10) }.join
|
|
142
145
|
end
|
|
143
146
|
|
|
147
|
+
# Removes CEP formatting and keeps only digits, capped to 8 digits.
|
|
148
|
+
#
|
|
149
|
+
# @param value [String, Integer] A CEP, with or without formatting.
|
|
150
|
+
# @return [String] The parsed digits.
|
|
151
|
+
#
|
|
152
|
+
# @example
|
|
153
|
+
# parse("01001-000") #=> "01001000"
|
|
154
|
+
# parse("01001000123") #=> "01001000"
|
|
155
|
+
def self.parse(value)
|
|
156
|
+
return '' unless value.is_a?(String) || value.is_a?(Integer)
|
|
157
|
+
|
|
158
|
+
value.to_s.gsub(/\D/, '')[0, 8]
|
|
159
|
+
end
|
|
160
|
+
|
|
144
161
|
# API OPERATIONS
|
|
145
162
|
################
|
|
146
163
|
|
|
@@ -203,9 +220,12 @@ module BrazilianUtils
|
|
|
203
220
|
cep: data['cep'],
|
|
204
221
|
logradouro: data['logradouro'],
|
|
205
222
|
complemento: data['complemento'],
|
|
223
|
+
unidade: data['unidade'],
|
|
206
224
|
bairro: data['bairro'],
|
|
207
225
|
localidade: data['localidade'],
|
|
208
226
|
uf: data['uf'],
|
|
227
|
+
estado: data['estado'],
|
|
228
|
+
regiao: data['regiao'],
|
|
209
229
|
ibge: data['ibge'],
|
|
210
230
|
gia: data['gia'],
|
|
211
231
|
ddd: data['ddd'],
|
|
@@ -245,9 +265,19 @@ module BrazilianUtils
|
|
|
245
265
|
# #=> CEPNotFound: SP - Example - Example
|
|
246
266
|
#
|
|
247
267
|
# @see https://viacep.com.br/
|
|
248
|
-
def self.get_cep_information_from_address(federal_unit, city, street, raise_exceptions: false)
|
|
268
|
+
def self.get_cep_information_from_address(federal_unit, city = nil, street = nil, raise_exceptions: false)
|
|
249
269
|
base_api_url = 'https://viacep.com.br/ws/%s/%s/%s/json/'
|
|
250
270
|
|
|
271
|
+
# Accept the contract's single-Hash-argument form, e.g.
|
|
272
|
+
# get_cep_information_from_address(state: "SP", city: "São Paulo", street: "Paulista")
|
|
273
|
+
if federal_unit.is_a?(Hash)
|
|
274
|
+
params = federal_unit
|
|
275
|
+
federal_unit = params[:state] || params[:uf] || params['state'] || params['uf']
|
|
276
|
+
city = params[:city] || params['city']
|
|
277
|
+
street = params[:street] || params['street']
|
|
278
|
+
raise_exceptions = params[:raise_exceptions] || params['raise_exceptions'] || raise_exceptions
|
|
279
|
+
end
|
|
280
|
+
|
|
251
281
|
# Validate UF
|
|
252
282
|
uf_code = federal_unit.to_s.upcase
|
|
253
283
|
uf_name = UF.name_from_code(uf_code)
|
|
@@ -294,9 +324,12 @@ module BrazilianUtils
|
|
|
294
324
|
cep: address_data['cep'],
|
|
295
325
|
logradouro: address_data['logradouro'],
|
|
296
326
|
complemento: address_data['complemento'],
|
|
327
|
+
unidade: address_data['unidade'],
|
|
297
328
|
bairro: address_data['bairro'],
|
|
298
329
|
localidade: address_data['localidade'],
|
|
299
330
|
uf: address_data['uf'],
|
|
331
|
+
estado: address_data['estado'],
|
|
332
|
+
regiao: address_data['regiao'],
|
|
300
333
|
ibge: address_data['ibge'],
|
|
301
334
|
gia: address_data['gia'],
|
|
302
335
|
ddd: address_data['ddd'],
|