br-utils 0.1.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 +7 -0
- data/.gitignore +120 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +348 -0
- data/Rakefile +6 -0
- data/examples/boleto_usage_example.rb +79 -0
- data/examples/cep_usage_example.rb +148 -0
- data/examples/cnh_usage_example.rb +120 -0
- data/examples/cnpj_usage_example.rb +227 -0
- data/examples/cpf_usage_example.rb +237 -0
- data/examples/currency_usage_example.rb +266 -0
- data/examples/date_usage_example.rb +259 -0
- data/examples/email_usage_example.rb +321 -0
- data/examples/legal_nature_usage_example.rb +437 -0
- data/examples/legal_process_usage_example.rb +444 -0
- data/examples/license_plate_usage_example.rb +440 -0
- data/examples/phone_usage_example.rb +595 -0
- data/examples/pis_usage_example.rb +588 -0
- data/examples/renavam_usage_example.rb +499 -0
- data/examples/voter_id_usage_example.rb +573 -0
- data/lib/brazilian-utils/boleto-utils.rb +176 -0
- data/lib/brazilian-utils/cep-utils.rb +330 -0
- data/lib/brazilian-utils/cnh-utils.rb +88 -0
- data/lib/brazilian-utils/cnpj-utils.rb +202 -0
- data/lib/brazilian-utils/cpf-utils.rb +192 -0
- data/lib/brazilian-utils/currency-utils.rb +226 -0
- data/lib/brazilian-utils/data/legal_process_ids.json +38 -0
- data/lib/brazilian-utils/date-utils.rb +244 -0
- data/lib/brazilian-utils/email-utils.rb +54 -0
- data/lib/brazilian-utils/legal-nature-utils.rb +235 -0
- data/lib/brazilian-utils/legal-process-utils.rb +240 -0
- data/lib/brazilian-utils/license-plate-utils.rb +279 -0
- data/lib/brazilian-utils/phone-utils.rb +272 -0
- data/lib/brazilian-utils/pis-utils.rb +151 -0
- data/lib/brazilian-utils/renavam-utils.rb +113 -0
- data/lib/brazilian-utils/voter-id-utils.rb +165 -0
- metadata +123 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BrazilianUtils
|
|
4
|
+
module BoletoUtils
|
|
5
|
+
# Every Digitable Line from Boleto has exactly 47 characters
|
|
6
|
+
DIGITABLE_LINE_LENGTH = 47
|
|
7
|
+
|
|
8
|
+
# Positions to convert digitable line to boleto
|
|
9
|
+
DIGITABLE_LINE_TO_BOLETO_CONVERT_POSITIONS = [
|
|
10
|
+
{ start: 0, end: 4 },
|
|
11
|
+
{ start: 32, end: 47 },
|
|
12
|
+
{ start: 4, end: 9 },
|
|
13
|
+
{ start: 10, end: 20 },
|
|
14
|
+
{ start: 21, end: 31 }
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
# Partials to verify with mod10
|
|
18
|
+
PARTIALS_TO_VERIFY_MOD10 = [
|
|
19
|
+
{ start: 0, end: 9, digit_index: 9 },
|
|
20
|
+
{ start: 10, end: 20, digit_index: 20 },
|
|
21
|
+
{ start: 21, end: 31, digit_index: 31 }
|
|
22
|
+
].freeze
|
|
23
|
+
|
|
24
|
+
# Mod10 weights
|
|
25
|
+
MOD10_WEIGHTS = [2, 1].freeze
|
|
26
|
+
|
|
27
|
+
# Check digit mod11 position
|
|
28
|
+
CHECK_DIGIT_MOD11_POSITION = 4
|
|
29
|
+
|
|
30
|
+
# Mod11 weights configuration
|
|
31
|
+
MOD11_WEIGHTS = {
|
|
32
|
+
initial: 2,
|
|
33
|
+
end: 9,
|
|
34
|
+
increment: 1
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
class << self
|
|
38
|
+
# Validates if a given Digitable Line is valid.
|
|
39
|
+
#
|
|
40
|
+
# @param digitable_line [String] The boleto digitable line to validate
|
|
41
|
+
# @return [Boolean] true if valid, false otherwise
|
|
42
|
+
def is_valid(digitable_line)
|
|
43
|
+
# Extract only numbers from the input
|
|
44
|
+
digitable_line_numbers = extract_only_numbers(digitable_line)
|
|
45
|
+
|
|
46
|
+
return false unless valid_length?(digitable_line_numbers)
|
|
47
|
+
return false unless validate_digitable_line_partials(digitable_line_numbers)
|
|
48
|
+
|
|
49
|
+
validate_mod11_check_digit(digitable_line_numbers)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Alias for is_valid
|
|
53
|
+
alias valid? is_valid
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
# Extract only numeric characters from a string.
|
|
58
|
+
#
|
|
59
|
+
# @param str [String] The input string
|
|
60
|
+
# @return [String] String containing only numeric characters
|
|
61
|
+
def extract_only_numbers(str)
|
|
62
|
+
return '' if str.nil?
|
|
63
|
+
|
|
64
|
+
str.gsub(/\D/, '')
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Validates the string length.
|
|
68
|
+
#
|
|
69
|
+
# @param digitable_line [String] The digitable line to check
|
|
70
|
+
# @return [Boolean] true if length is exactly 47, false otherwise
|
|
71
|
+
def valid_length?(digitable_line)
|
|
72
|
+
digitable_line.length == DIGITABLE_LINE_LENGTH
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Validates the digitable line partials using mod10.
|
|
76
|
+
#
|
|
77
|
+
# @param digitable_line [String] The digitable line to validate
|
|
78
|
+
# @return [Boolean] true if all partials are valid, false otherwise
|
|
79
|
+
def validate_digitable_line_partials(digitable_line)
|
|
80
|
+
PARTIALS_TO_VERIFY_MOD10.all? do |partial|
|
|
81
|
+
partial_str = digitable_line[partial[:start]...partial[:end]]
|
|
82
|
+
mod10 = get_mod10(partial_str)
|
|
83
|
+
digit = digitable_line[partial[:digit_index]].to_i
|
|
84
|
+
digit == mod10
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Calculate mod10 for a given partial string.
|
|
89
|
+
#
|
|
90
|
+
# @param partial [String] The partial string to calculate mod10 for
|
|
91
|
+
# @return [Integer] The mod10 check digit
|
|
92
|
+
def get_mod10(partial)
|
|
93
|
+
sum = 0
|
|
94
|
+
partial_reversed = partial.reverse
|
|
95
|
+
|
|
96
|
+
partial_reversed.each_char.with_index do |char, index|
|
|
97
|
+
partial_value = char.to_i
|
|
98
|
+
weight = MOD10_WEIGHTS[index % 2]
|
|
99
|
+
multiplier = partial_value * weight
|
|
100
|
+
|
|
101
|
+
if multiplier > 9
|
|
102
|
+
sum += 1 + (multiplier % 10)
|
|
103
|
+
else
|
|
104
|
+
sum += multiplier
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
mod10 = sum % 10
|
|
109
|
+
if mod10 > 0
|
|
110
|
+
10 - mod10
|
|
111
|
+
else
|
|
112
|
+
0
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Validates the mod11 check digit.
|
|
117
|
+
#
|
|
118
|
+
# @param digitable_line [String] The digitable line to validate
|
|
119
|
+
# @return [Boolean] true if mod11 check digit is valid, false otherwise
|
|
120
|
+
def validate_mod11_check_digit(digitable_line)
|
|
121
|
+
parsed_digitable_line = parse_digitable_line(digitable_line)
|
|
122
|
+
|
|
123
|
+
# Extract the value before and after the check digit position
|
|
124
|
+
value = parsed_digitable_line[0...CHECK_DIGIT_MOD11_POSITION] +
|
|
125
|
+
parsed_digitable_line[(CHECK_DIGIT_MOD11_POSITION + 1)..-1]
|
|
126
|
+
|
|
127
|
+
mod11 = get_mod11(value)
|
|
128
|
+
mod11_value = parsed_digitable_line[CHECK_DIGIT_MOD11_POSITION].to_i
|
|
129
|
+
|
|
130
|
+
mod11_value == mod11
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Parse the digitable line by extracting specific positions.
|
|
134
|
+
#
|
|
135
|
+
# @param digitable_line [String] The digitable line to parse
|
|
136
|
+
# @return [String] The parsed digitable line
|
|
137
|
+
def parse_digitable_line(digitable_line)
|
|
138
|
+
result = ''
|
|
139
|
+
DIGITABLE_LINE_TO_BOLETO_CONVERT_POSITIONS.each do |position|
|
|
140
|
+
result += digitable_line[position[:start]...position[:end]]
|
|
141
|
+
end
|
|
142
|
+
result
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Calculate mod11 for a given value string.
|
|
146
|
+
#
|
|
147
|
+
# @param value [String] The value string to calculate mod11 for
|
|
148
|
+
# @return [Integer] The mod11 check digit
|
|
149
|
+
def get_mod11(value)
|
|
150
|
+
weight = MOD11_WEIGHTS[:initial]
|
|
151
|
+
sum = 0
|
|
152
|
+
value_reversed = value.reverse
|
|
153
|
+
|
|
154
|
+
value_reversed.each_char do |char|
|
|
155
|
+
value_value = char.to_i
|
|
156
|
+
multiplier = value_value * weight
|
|
157
|
+
|
|
158
|
+
if weight < MOD11_WEIGHTS[:end]
|
|
159
|
+
weight += MOD11_WEIGHTS[:increment]
|
|
160
|
+
else
|
|
161
|
+
weight = MOD11_WEIGHTS[:initial]
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
sum += multiplier
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
mod11 = sum % 11
|
|
168
|
+
if mod11 != 0 && mod11 != 1
|
|
169
|
+
11 - mod11
|
|
170
|
+
else
|
|
171
|
+
1
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'net/http'
|
|
3
|
+
require 'uri'
|
|
4
|
+
|
|
5
|
+
module BrazilianUtils
|
|
6
|
+
# Custom exceptions for CEP operations
|
|
7
|
+
class InvalidCEP < StandardError; end
|
|
8
|
+
class CEPNotFound < StandardError; end
|
|
9
|
+
|
|
10
|
+
# Brazilian states enum
|
|
11
|
+
module UF
|
|
12
|
+
STATES = {
|
|
13
|
+
'AC' => 'Acre',
|
|
14
|
+
'AL' => 'Alagoas',
|
|
15
|
+
'AP' => 'Amapá',
|
|
16
|
+
'AM' => 'Amazonas',
|
|
17
|
+
'BA' => 'Bahia',
|
|
18
|
+
'CE' => 'Ceará',
|
|
19
|
+
'DF' => 'Distrito Federal',
|
|
20
|
+
'ES' => 'Espírito Santo',
|
|
21
|
+
'GO' => 'Goiás',
|
|
22
|
+
'MA' => 'Maranhão',
|
|
23
|
+
'MT' => 'Mato Grosso',
|
|
24
|
+
'MS' => 'Mato Grosso do Sul',
|
|
25
|
+
'MG' => 'Minas Gerais',
|
|
26
|
+
'PA' => 'Pará',
|
|
27
|
+
'PB' => 'Paraíba',
|
|
28
|
+
'PR' => 'Paraná',
|
|
29
|
+
'PE' => 'Pernambuco',
|
|
30
|
+
'PI' => 'Piauí',
|
|
31
|
+
'RJ' => 'Rio de Janeiro',
|
|
32
|
+
'RN' => 'Rio Grande do Norte',
|
|
33
|
+
'RS' => 'Rio Grande do Sul',
|
|
34
|
+
'RO' => 'Rondônia',
|
|
35
|
+
'RR' => 'Roraima',
|
|
36
|
+
'SC' => 'Santa Catarina',
|
|
37
|
+
'SP' => 'São Paulo',
|
|
38
|
+
'SE' => 'Sergipe',
|
|
39
|
+
'TO' => 'Tocantins'
|
|
40
|
+
}.freeze
|
|
41
|
+
|
|
42
|
+
def self.valid?(uf)
|
|
43
|
+
STATES.key?(uf.to_s.upcase)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.name_from_code(code)
|
|
47
|
+
STATES[code.to_s.upcase]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.code_from_name(name)
|
|
51
|
+
STATES.key(name)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Address structure
|
|
56
|
+
Address = Struct.new(
|
|
57
|
+
:cep,
|
|
58
|
+
:logradouro,
|
|
59
|
+
:complemento,
|
|
60
|
+
:bairro,
|
|
61
|
+
:localidade,
|
|
62
|
+
:uf,
|
|
63
|
+
:ibge,
|
|
64
|
+
:gia,
|
|
65
|
+
:ddd,
|
|
66
|
+
:siafi,
|
|
67
|
+
keyword_init: true
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
module CEPUtils
|
|
71
|
+
# FORMATTING
|
|
72
|
+
############
|
|
73
|
+
|
|
74
|
+
# Removes specific symbols from a given CEP (Postal Code).
|
|
75
|
+
#
|
|
76
|
+
# This function takes a CEP (Postal Code) as input and removes all occurrences
|
|
77
|
+
# of the '.' and '-' characters from it.
|
|
78
|
+
#
|
|
79
|
+
# @param dirty [String] The input CEP (Postal Code) containing symbols to be removed.
|
|
80
|
+
# @return [String] A new string with the specified symbols removed.
|
|
81
|
+
#
|
|
82
|
+
# @example
|
|
83
|
+
# remove_symbols("123-45.678.9") #=> "123456789"
|
|
84
|
+
# remove_symbols("abc.xyz") #=> "abcxyz"
|
|
85
|
+
def self.remove_symbols(dirty)
|
|
86
|
+
return '' unless dirty
|
|
87
|
+
|
|
88
|
+
dirty.to_s.delete('.-')
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Formats a Brazilian CEP (Postal Code) into a standard format.
|
|
92
|
+
#
|
|
93
|
+
# This function takes a CEP (Postal Code) as input and, if it is a valid
|
|
94
|
+
# 8-digit CEP, formats it into the standard "12345-678" format.
|
|
95
|
+
#
|
|
96
|
+
# @param cep [String] The input CEP (Postal Code) to be formatted.
|
|
97
|
+
# @return [String, nil] The formatted CEP in the "12345-678" format if it's valid,
|
|
98
|
+
# nil if it's not valid.
|
|
99
|
+
#
|
|
100
|
+
# @example
|
|
101
|
+
# format_cep("12345678") #=> "12345-678"
|
|
102
|
+
# format_cep("12345") #=> nil
|
|
103
|
+
def self.format_cep(cep)
|
|
104
|
+
return nil unless valid?(cep)
|
|
105
|
+
|
|
106
|
+
"#{cep[0..4]}-#{cep[5..7]}"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# OPERATIONS
|
|
110
|
+
############
|
|
111
|
+
|
|
112
|
+
# Checks if a CEP (Postal Code) is valid.
|
|
113
|
+
#
|
|
114
|
+
# To be considered valid, the input must be a string containing exactly 8
|
|
115
|
+
# digits.
|
|
116
|
+
# This function does not verify if the CEP is a real postal code; it only
|
|
117
|
+
# validates the format of the string.
|
|
118
|
+
#
|
|
119
|
+
# @param cep [String] The string containing the CEP to be checked.
|
|
120
|
+
# @return [Boolean] true if the CEP is valid (8 digits), false otherwise.
|
|
121
|
+
#
|
|
122
|
+
# @example
|
|
123
|
+
# valid?("12345678") #=> true
|
|
124
|
+
# valid?("12345") #=> false
|
|
125
|
+
# valid?("abcdefgh") #=> false
|
|
126
|
+
#
|
|
127
|
+
# @see https://en.wikipedia.org/wiki/Código_de_Endereçamento_Postal
|
|
128
|
+
def self.valid?(cep)
|
|
129
|
+
return false unless cep.is_a?(String)
|
|
130
|
+
|
|
131
|
+
cep.length == 8 && cep.match?(/^\d{8}$/)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Generates a random 8-digit CEP (Postal Code) number as a string.
|
|
135
|
+
#
|
|
136
|
+
# @return [String] A randomly generated 8-digit number.
|
|
137
|
+
#
|
|
138
|
+
# @example
|
|
139
|
+
# generate() #=> "12345678"
|
|
140
|
+
def self.generate
|
|
141
|
+
8.times.map { rand(10) }.join
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# API OPERATIONS
|
|
145
|
+
################
|
|
146
|
+
|
|
147
|
+
# Fetches address information from a given CEP (Postal Code) using the ViaCEP API.
|
|
148
|
+
#
|
|
149
|
+
# @param cep [String] The CEP (Postal Code) to be used in the search.
|
|
150
|
+
# @param raise_exceptions [Boolean] Whether to raise exceptions when the CEP
|
|
151
|
+
# is invalid or not found. Defaults to false.
|
|
152
|
+
#
|
|
153
|
+
# @raise [InvalidCEP] When the input CEP is invalid.
|
|
154
|
+
# @raise [CEPNotFound] When the input CEP is not found.
|
|
155
|
+
#
|
|
156
|
+
# @return [Address, nil] An Address object containing the address information
|
|
157
|
+
# if the CEP is found, nil otherwise.
|
|
158
|
+
#
|
|
159
|
+
# @example
|
|
160
|
+
# get_address_from_cep("01310100")
|
|
161
|
+
# #=> #<Address cep="01310-100", logradouro="Avenida Paulista", ...>
|
|
162
|
+
#
|
|
163
|
+
# get_address_from_cep("abcdefg") #=> nil
|
|
164
|
+
#
|
|
165
|
+
# get_address_from_cep("abcdefg", true)
|
|
166
|
+
# #=> InvalidCEP: CEP 'abcdefg' is invalid.
|
|
167
|
+
#
|
|
168
|
+
# get_address_from_cep("00000000", true)
|
|
169
|
+
# #=> CEPNotFound: 00000000
|
|
170
|
+
#
|
|
171
|
+
# @see https://viacep.com.br/
|
|
172
|
+
def self.get_address_from_cep(cep, raise_exceptions: false)
|
|
173
|
+
base_api_url = 'https://viacep.com.br/ws/%s/json/'
|
|
174
|
+
|
|
175
|
+
clean_cep = remove_symbols(cep)
|
|
176
|
+
cep_is_valid = valid?(clean_cep)
|
|
177
|
+
|
|
178
|
+
unless cep_is_valid
|
|
179
|
+
raise InvalidCEP, "CEP '#{cep}' is invalid." if raise_exceptions
|
|
180
|
+
|
|
181
|
+
return nil
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
begin
|
|
185
|
+
uri = URI.parse(format(base_api_url, clean_cep))
|
|
186
|
+
response = Net::HTTP.get_response(uri)
|
|
187
|
+
|
|
188
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
189
|
+
raise CEPNotFound, cep if raise_exceptions
|
|
190
|
+
|
|
191
|
+
return nil
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
data = JSON.parse(response.body)
|
|
195
|
+
|
|
196
|
+
if data['erro']
|
|
197
|
+
raise CEPNotFound, cep if raise_exceptions
|
|
198
|
+
|
|
199
|
+
return nil
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
Address.new(
|
|
203
|
+
cep: data['cep'],
|
|
204
|
+
logradouro: data['logradouro'],
|
|
205
|
+
complemento: data['complemento'],
|
|
206
|
+
bairro: data['bairro'],
|
|
207
|
+
localidade: data['localidade'],
|
|
208
|
+
uf: data['uf'],
|
|
209
|
+
ibge: data['ibge'],
|
|
210
|
+
gia: data['gia'],
|
|
211
|
+
ddd: data['ddd'],
|
|
212
|
+
siafi: data['siafi']
|
|
213
|
+
)
|
|
214
|
+
rescue StandardError => e
|
|
215
|
+
raise CEPNotFound, cep if raise_exceptions
|
|
216
|
+
|
|
217
|
+
nil
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Fetches CEP (Postal Code) options from a given address using the ViaCEP API.
|
|
222
|
+
#
|
|
223
|
+
# @param federal_unit [String] The two-letter abbreviation of the Brazilian state.
|
|
224
|
+
# @param city [String] The name of the city.
|
|
225
|
+
# @param street [String] The name (or substring) of the street.
|
|
226
|
+
# @param raise_exceptions [Boolean] Whether to raise exceptions when the address
|
|
227
|
+
# is invalid or not found. Defaults to false.
|
|
228
|
+
#
|
|
229
|
+
# @raise [ArgumentError] When the input UF is invalid.
|
|
230
|
+
# @raise [CEPNotFound] When the input address is not found.
|
|
231
|
+
#
|
|
232
|
+
# @return [Array<Address>, nil] An array of Address objects containing the address
|
|
233
|
+
# information if the address is found, nil otherwise.
|
|
234
|
+
#
|
|
235
|
+
# @example
|
|
236
|
+
# get_cep_information_from_address("SP", "São Paulo", "Avenida Paulista")
|
|
237
|
+
# #=> [#<Address cep="01310-100", logradouro="Avenida Paulista", ...>, ...]
|
|
238
|
+
#
|
|
239
|
+
# get_cep_information_from_address("A", "Example", "Rua Example") #=> nil
|
|
240
|
+
#
|
|
241
|
+
# get_cep_information_from_address("XX", "Example", "Example", true)
|
|
242
|
+
# #=> ArgumentError: Invalid UF: XX
|
|
243
|
+
#
|
|
244
|
+
# get_cep_information_from_address("SP", "Example", "Example", true)
|
|
245
|
+
# #=> CEPNotFound: SP - Example - Example
|
|
246
|
+
#
|
|
247
|
+
# @see https://viacep.com.br/
|
|
248
|
+
def self.get_cep_information_from_address(federal_unit, city, street, raise_exceptions: false)
|
|
249
|
+
base_api_url = 'https://viacep.com.br/ws/%s/%s/%s/json/'
|
|
250
|
+
|
|
251
|
+
# Validate UF
|
|
252
|
+
uf_code = federal_unit.to_s.upcase
|
|
253
|
+
uf_name = UF.name_from_code(uf_code)
|
|
254
|
+
|
|
255
|
+
unless uf_name || UF.code_from_name(federal_unit)
|
|
256
|
+
if raise_exceptions
|
|
257
|
+
raise ArgumentError, "Invalid UF: #{federal_unit}"
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
return nil
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Use state name if code was provided
|
|
264
|
+
uf_to_use = uf_name ? federal_unit : UF.code_from_name(federal_unit)
|
|
265
|
+
|
|
266
|
+
# Normalize and encode city and street (remove accents and encode spaces)
|
|
267
|
+
parsed_city = normalize_string(city)
|
|
268
|
+
parsed_street = normalize_string(street)
|
|
269
|
+
|
|
270
|
+
begin
|
|
271
|
+
uri = URI.parse(format(base_api_url, uf_to_use, parsed_city, parsed_street))
|
|
272
|
+
response = Net::HTTP.get_response(uri)
|
|
273
|
+
|
|
274
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
275
|
+
if raise_exceptions
|
|
276
|
+
raise CEPNotFound, "#{federal_unit} - #{city} - #{street}"
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
return nil
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
data = JSON.parse(response.body)
|
|
283
|
+
|
|
284
|
+
if data.empty?
|
|
285
|
+
if raise_exceptions
|
|
286
|
+
raise CEPNotFound, "#{federal_unit} - #{city} - #{street}"
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
return nil
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
data.map do |address_data|
|
|
293
|
+
Address.new(
|
|
294
|
+
cep: address_data['cep'],
|
|
295
|
+
logradouro: address_data['logradouro'],
|
|
296
|
+
complemento: address_data['complemento'],
|
|
297
|
+
bairro: address_data['bairro'],
|
|
298
|
+
localidade: address_data['localidade'],
|
|
299
|
+
uf: address_data['uf'],
|
|
300
|
+
ibge: address_data['ibge'],
|
|
301
|
+
gia: address_data['gia'],
|
|
302
|
+
ddd: address_data['ddd'],
|
|
303
|
+
siafi: address_data['siafi']
|
|
304
|
+
)
|
|
305
|
+
end
|
|
306
|
+
rescue JSON::ParserError, StandardError => e
|
|
307
|
+
if raise_exceptions
|
|
308
|
+
raise CEPNotFound, "#{federal_unit} - #{city} - #{street}"
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
nil
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
private
|
|
316
|
+
|
|
317
|
+
# Normalizes a string by removing accents and encoding spaces for URL
|
|
318
|
+
def self.normalize_string(str)
|
|
319
|
+
require 'unicode_normalize/normalize'
|
|
320
|
+
|
|
321
|
+
str.to_s
|
|
322
|
+
.unicode_normalize(:nfd)
|
|
323
|
+
.encode('ASCII', undef: :replace, replace: '')
|
|
324
|
+
.gsub(' ', '%20')
|
|
325
|
+
rescue StandardError
|
|
326
|
+
# Fallback if unicode_normalize is not available
|
|
327
|
+
str.to_s.gsub(' ', '%20')
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
module BrazilianUtils
|
|
2
|
+
module CNHUtils
|
|
3
|
+
# Validates the registration number for the Brazilian CNH (Carteira Nacional de Habilitação)
|
|
4
|
+
# that was created in 2022.
|
|
5
|
+
#
|
|
6
|
+
# Previous versions of the CNH are not supported in this version.
|
|
7
|
+
# This function checks if the given CNH is valid based on the format and allowed characters,
|
|
8
|
+
# verifying the verification digits.
|
|
9
|
+
#
|
|
10
|
+
# @param cnh [String] CNH string (symbols will be ignored).
|
|
11
|
+
# @return [Boolean] true if CNH has a valid format, false otherwise.
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# valid?("12345678901") #=> false
|
|
15
|
+
# valid?("A2C45678901") #=> false
|
|
16
|
+
# valid?("98765432100") #=> true
|
|
17
|
+
# valid?("987654321-00") #=> true
|
|
18
|
+
def self.valid?(cnh)
|
|
19
|
+
return false unless cnh
|
|
20
|
+
|
|
21
|
+
# Clean the input and check for numbers only
|
|
22
|
+
clean_cnh = cnh.to_s.gsub(/\D/, '')
|
|
23
|
+
|
|
24
|
+
return false if clean_cnh.empty?
|
|
25
|
+
return false if clean_cnh.length != 11
|
|
26
|
+
|
|
27
|
+
# Reject sequences as "00000000000", "11111111111", etc.
|
|
28
|
+
return false if clean_cnh == clean_cnh[0] * 11
|
|
29
|
+
|
|
30
|
+
# Cast digits to array of integers
|
|
31
|
+
digits = clean_cnh.chars.map(&:to_i)
|
|
32
|
+
first_verificator = digits[9]
|
|
33
|
+
second_verificator = digits[10]
|
|
34
|
+
|
|
35
|
+
# Check the 10th digit
|
|
36
|
+
return false unless check_first_verificator(digits, first_verificator)
|
|
37
|
+
|
|
38
|
+
# Check the 11th digit
|
|
39
|
+
check_second_verificator(digits, second_verificator, first_verificator)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Generates the first verification digit and uses it to verify the 10th digit of the CNH
|
|
43
|
+
#
|
|
44
|
+
# @param digits [Array<Integer>] Array of CNH digits
|
|
45
|
+
# @param first_verificator [Integer] The first verification digit (10th digit)
|
|
46
|
+
# @return [Boolean] true if the first verificator is valid
|
|
47
|
+
#
|
|
48
|
+
# @private
|
|
49
|
+
def self.check_first_verificator(digits, first_verificator)
|
|
50
|
+
sum = 0
|
|
51
|
+
9.times do |i|
|
|
52
|
+
sum += digits[i] * (9 - i)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
sum = sum % 11
|
|
56
|
+
result = sum > 9 ? 0 : sum
|
|
57
|
+
|
|
58
|
+
result == first_verificator
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Generates the second verification digit and uses it to verify the 11th digit of the CNH
|
|
62
|
+
#
|
|
63
|
+
# @param digits [Array<Integer>] Array of CNH digits
|
|
64
|
+
# @param second_verificator [Integer] The second verification digit (11th digit)
|
|
65
|
+
# @param first_verificator [Integer] The first verification digit (10th digit)
|
|
66
|
+
# @return [Boolean] true if the second verificator is valid
|
|
67
|
+
#
|
|
68
|
+
# @private
|
|
69
|
+
def self.check_second_verificator(digits, second_verificator, first_verificator)
|
|
70
|
+
sum = 0
|
|
71
|
+
9.times do |i|
|
|
72
|
+
sum += digits[i] * (i + 1)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
result = sum % 11
|
|
76
|
+
|
|
77
|
+
if first_verificator > 9
|
|
78
|
+
result = (result - 2).negative? ? result + 9 : result - 2
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
result = 0 if result > 9
|
|
82
|
+
|
|
83
|
+
result == second_verificator
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private_class_method :check_first_verificator, :check_second_verificator
|
|
87
|
+
end
|
|
88
|
+
end
|