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,272 @@
|
|
|
1
|
+
module BrazilianUtils
|
|
2
|
+
# Utilities for formatting, validating, and generating Brazilian phone numbers.
|
|
3
|
+
#
|
|
4
|
+
# Brazilian phone numbers come in two types:
|
|
5
|
+
# - Mobile (Celular): 11 digits - DDD (2 digits) + 9 + 8 digits, e.g., "11994029275"
|
|
6
|
+
# - Landline (Fixo): 10 digits - DDD (2 digits) + [2-5] + 7 digits, e.g., "1635014415"
|
|
7
|
+
#
|
|
8
|
+
# DDD (Discagem Direta à Distância) is the area code, ranging from 11 to 99.
|
|
9
|
+
# Mobile numbers always have 9 as the 3rd digit (after DDD).
|
|
10
|
+
# Landline numbers have 2, 3, 4, or 5 as the 3rd digit (after DDD).
|
|
11
|
+
module PhoneUtils
|
|
12
|
+
# Pattern for mobile phone numbers (11 digits: DDD + 9 + 8 digits)
|
|
13
|
+
MOBILE_PATTERN = /^[1-9][1-9][9]\d{8}$/.freeze
|
|
14
|
+
|
|
15
|
+
# Pattern for landline phone numbers (10 digits: DDD + [2-5] + 7 digits)
|
|
16
|
+
LANDLINE_PATTERN = /^[1-9][1-9][2-5]\d{7}$/.freeze
|
|
17
|
+
|
|
18
|
+
# Pattern for international dialing code (+55 or 55)
|
|
19
|
+
INTERNATIONAL_CODE_PATTERN = /\+?55/.freeze
|
|
20
|
+
|
|
21
|
+
# Formats a Brazilian phone number into the standard pattern.
|
|
22
|
+
#
|
|
23
|
+
# Formats as (DD)NNNNN-NNNN for both mobile and landline numbers.
|
|
24
|
+
#
|
|
25
|
+
# @param phone [String] A string representing the phone number (digits only)
|
|
26
|
+
#
|
|
27
|
+
# @return [String, nil] The formatted phone number or nil if invalid
|
|
28
|
+
#
|
|
29
|
+
# @example
|
|
30
|
+
# format_phone("11994029275")
|
|
31
|
+
# #=> "(11)99402-9275"
|
|
32
|
+
#
|
|
33
|
+
# format_phone("1635014415")
|
|
34
|
+
# #=> "(16)3501-4415"
|
|
35
|
+
#
|
|
36
|
+
# format_phone("333333")
|
|
37
|
+
# #=> nil
|
|
38
|
+
def self.format_phone(phone)
|
|
39
|
+
return nil unless is_valid(phone)
|
|
40
|
+
|
|
41
|
+
ddd = phone[0..1]
|
|
42
|
+
phone_number = phone[2..-1]
|
|
43
|
+
|
|
44
|
+
"(#{ddd})#{phone_number[0..-5]}-#{phone_number[-4..-1]}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Alias for format_phone
|
|
48
|
+
class << self
|
|
49
|
+
alias format format_phone
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Returns if a Brazilian phone number is valid.
|
|
53
|
+
#
|
|
54
|
+
# It does not verify if the number actually exists.
|
|
55
|
+
#
|
|
56
|
+
# @param phone_number [String] The phone number to validate (digits only, no country code)
|
|
57
|
+
# @param type [Symbol, String, nil] :mobile, :landline, "mobile", or "landline".
|
|
58
|
+
# If not specified, checks for either type.
|
|
59
|
+
#
|
|
60
|
+
# @return [Boolean] True if the phone number is valid, false otherwise
|
|
61
|
+
#
|
|
62
|
+
# @example
|
|
63
|
+
# is_valid("11994029275")
|
|
64
|
+
# #=> true (mobile)
|
|
65
|
+
#
|
|
66
|
+
# is_valid("1635014415")
|
|
67
|
+
# #=> true (landline)
|
|
68
|
+
#
|
|
69
|
+
# is_valid("11994029275", :mobile)
|
|
70
|
+
# #=> true
|
|
71
|
+
#
|
|
72
|
+
# is_valid("1635014415", :mobile)
|
|
73
|
+
# #=> false
|
|
74
|
+
#
|
|
75
|
+
# is_valid("1635014415", :landline)
|
|
76
|
+
# #=> true
|
|
77
|
+
#
|
|
78
|
+
# is_valid("123456")
|
|
79
|
+
# #=> false
|
|
80
|
+
def self.is_valid(phone_number, type = nil)
|
|
81
|
+
return false unless phone_number.is_a?(String)
|
|
82
|
+
|
|
83
|
+
type_str = type.to_s if type
|
|
84
|
+
|
|
85
|
+
case type_str
|
|
86
|
+
when 'mobile'
|
|
87
|
+
valid_mobile?(phone_number)
|
|
88
|
+
when 'landline'
|
|
89
|
+
valid_landline?(phone_number)
|
|
90
|
+
else
|
|
91
|
+
valid_mobile?(phone_number) || valid_landline?(phone_number)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Alias for is_valid
|
|
96
|
+
class << self
|
|
97
|
+
alias valid? is_valid
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Removes common symbols from a Brazilian phone number string.
|
|
101
|
+
#
|
|
102
|
+
# Removes: (, ), -, +, and spaces
|
|
103
|
+
#
|
|
104
|
+
# @param phone_number [String] The phone number to remove symbols from
|
|
105
|
+
#
|
|
106
|
+
# @return [String] A new string with the specified symbols removed
|
|
107
|
+
#
|
|
108
|
+
# @example
|
|
109
|
+
# remove_symbols_phone("(11)99402-9275")
|
|
110
|
+
# #=> "11994029275"
|
|
111
|
+
#
|
|
112
|
+
# remove_symbols_phone("+55 11 99402-9275")
|
|
113
|
+
# #=> "5511994029275"
|
|
114
|
+
#
|
|
115
|
+
# remove_symbols_phone("(16) 3501-4415")
|
|
116
|
+
# #=> "1635014415"
|
|
117
|
+
def self.remove_symbols_phone(phone_number)
|
|
118
|
+
return '' unless phone_number.is_a?(String)
|
|
119
|
+
|
|
120
|
+
phone_number.gsub(/[\(\)\-\+\s]/, '')
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Alias for remove_symbols_phone
|
|
124
|
+
class << self
|
|
125
|
+
alias remove_symbols remove_symbols_phone
|
|
126
|
+
alias sieve remove_symbols_phone
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Generates a valid and random phone number.
|
|
130
|
+
#
|
|
131
|
+
# @param type [Symbol, String, nil] :mobile, :landline, "mobile", or "landline".
|
|
132
|
+
# If not specified, generates either type randomly.
|
|
133
|
+
#
|
|
134
|
+
# @return [String] A randomly generated valid phone number
|
|
135
|
+
#
|
|
136
|
+
# @example
|
|
137
|
+
# generate
|
|
138
|
+
# #=> "2234451215" (random type)
|
|
139
|
+
#
|
|
140
|
+
# generate(:mobile)
|
|
141
|
+
# #=> "11999115895"
|
|
142
|
+
#
|
|
143
|
+
# generate(:landline)
|
|
144
|
+
# #=> "1635317900"
|
|
145
|
+
#
|
|
146
|
+
# generate("mobile")
|
|
147
|
+
# #=> "21987654321"
|
|
148
|
+
def self.generate(type = nil)
|
|
149
|
+
type_str = type.to_s if type
|
|
150
|
+
|
|
151
|
+
case type_str
|
|
152
|
+
when 'mobile'
|
|
153
|
+
generate_mobile_phone
|
|
154
|
+
when 'landline'
|
|
155
|
+
generate_landline_phone
|
|
156
|
+
else
|
|
157
|
+
[method(:generate_mobile_phone), method(:generate_landline_phone)].sample.call
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Removes the international dialing code (+55 or 55) from a phone number.
|
|
162
|
+
#
|
|
163
|
+
# Only removes the code if the resulting number has more than 11 digits.
|
|
164
|
+
#
|
|
165
|
+
# @param phone_number [String] The phone number with or without international code
|
|
166
|
+
#
|
|
167
|
+
# @return [String] The phone number without international code, or the same number if no code present
|
|
168
|
+
#
|
|
169
|
+
# @example
|
|
170
|
+
# remove_international_dialing_code("5511994029275")
|
|
171
|
+
# #=> "11994029275"
|
|
172
|
+
#
|
|
173
|
+
# remove_international_dialing_code("+5511994029275")
|
|
174
|
+
# #=> "11994029275"
|
|
175
|
+
#
|
|
176
|
+
# remove_international_dialing_code("1635014415")
|
|
177
|
+
# #=> "1635014415" (no international code)
|
|
178
|
+
#
|
|
179
|
+
# remove_international_dialing_code("+55 11 99402-9275")
|
|
180
|
+
# #=> "+55 11 99402-9275" (has spaces, length check fails)
|
|
181
|
+
def self.remove_international_dialing_code(phone_number)
|
|
182
|
+
return '' unless phone_number.is_a?(String)
|
|
183
|
+
|
|
184
|
+
# Check if pattern matches and length (without spaces) is > 11
|
|
185
|
+
if INTERNATIONAL_CODE_PATTERN.match?(phone_number) && phone_number.gsub(' ', '').length > 11
|
|
186
|
+
phone_number.sub('55', '')
|
|
187
|
+
else
|
|
188
|
+
phone_number
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Returns if a Brazilian mobile number is valid.
|
|
193
|
+
#
|
|
194
|
+
# Mobile pattern: DDD (2 digits 1-9) + 9 + 8 digits (total 11 digits)
|
|
195
|
+
#
|
|
196
|
+
# @param phone_number [String] The mobile number to validate
|
|
197
|
+
#
|
|
198
|
+
# @return [Boolean] True if valid mobile, false otherwise
|
|
199
|
+
#
|
|
200
|
+
# @private
|
|
201
|
+
def self.valid_mobile?(phone_number)
|
|
202
|
+
return false unless phone_number.is_a?(String)
|
|
203
|
+
|
|
204
|
+
MOBILE_PATTERN.match?(phone_number.strip)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
private_class_method :valid_mobile?
|
|
208
|
+
|
|
209
|
+
# Returns if a Brazilian landline number is valid.
|
|
210
|
+
#
|
|
211
|
+
# Landline pattern: DDD (2 digits 1-9) + [2-5] + 7 digits (total 10 digits)
|
|
212
|
+
#
|
|
213
|
+
# @param phone_number [String] The landline number to validate
|
|
214
|
+
#
|
|
215
|
+
# @return [Boolean] True if valid landline, false otherwise
|
|
216
|
+
#
|
|
217
|
+
# @private
|
|
218
|
+
def self.valid_landline?(phone_number)
|
|
219
|
+
return false unless phone_number.is_a?(String)
|
|
220
|
+
|
|
221
|
+
LANDLINE_PATTERN.match?(phone_number.strip)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
private_class_method :valid_landline?
|
|
225
|
+
|
|
226
|
+
# Generates a valid DDD (area code) number.
|
|
227
|
+
#
|
|
228
|
+
# DDD consists of 2 digits, both ranging from 1-9.
|
|
229
|
+
#
|
|
230
|
+
# @return [String] A 2-digit DDD number
|
|
231
|
+
#
|
|
232
|
+
# @private
|
|
233
|
+
def self.generate_ddd_number
|
|
234
|
+
2.times.map { rand(1..9) }.join
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
private_class_method :generate_ddd_number
|
|
238
|
+
|
|
239
|
+
# Generates a valid and random mobile phone number.
|
|
240
|
+
#
|
|
241
|
+
# Format: DDD + 9 + 8 random digits (total 11 digits)
|
|
242
|
+
#
|
|
243
|
+
# @return [String] A valid mobile phone number
|
|
244
|
+
#
|
|
245
|
+
# @private
|
|
246
|
+
def self.generate_mobile_phone
|
|
247
|
+
ddd = generate_ddd_number
|
|
248
|
+
client_number = 8.times.map { rand(0..9) }.join
|
|
249
|
+
|
|
250
|
+
"#{ddd}9#{client_number}"
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
private_class_method :generate_mobile_phone
|
|
254
|
+
|
|
255
|
+
# Generates a valid and random landline phone number.
|
|
256
|
+
#
|
|
257
|
+
# Format: DDD + [2-5] + 7 random digits (total 10 digits)
|
|
258
|
+
#
|
|
259
|
+
# @return [String] A valid landline phone number
|
|
260
|
+
#
|
|
261
|
+
# @private
|
|
262
|
+
def self.generate_landline_phone
|
|
263
|
+
ddd = generate_ddd_number
|
|
264
|
+
first_digit = rand(2..5)
|
|
265
|
+
remaining_digits = rand(0..9999999).to_s.rjust(7, '0')
|
|
266
|
+
|
|
267
|
+
"#{ddd}#{first_digit}#{remaining_digits}"
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
private_class_method :generate_landline_phone
|
|
271
|
+
end
|
|
272
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
module BrazilianUtils
|
|
2
|
+
# Utilities for formatting, validating, and generating Brazilian PIS numbers.
|
|
3
|
+
#
|
|
4
|
+
# PIS (Programa de Integração Social) is an 11-digit identification number
|
|
5
|
+
# for Brazilian workers, similar to a social security number.
|
|
6
|
+
#
|
|
7
|
+
# Format: XXX.XXXXX.XX-X (e.g., "123.45678.90-9")
|
|
8
|
+
module PISUtils
|
|
9
|
+
# Weights used for checksum calculation
|
|
10
|
+
WEIGHTS = [3, 2, 9, 8, 7, 6, 5, 4, 3, 2].freeze
|
|
11
|
+
|
|
12
|
+
# Removes formatting symbols from a PIS string.
|
|
13
|
+
#
|
|
14
|
+
# This function takes a PIS string with formatting symbols (dots and hyphens)
|
|
15
|
+
# and returns a cleaned version with only digits.
|
|
16
|
+
#
|
|
17
|
+
# @param pis [String] A PIS string that may contain formatting symbols
|
|
18
|
+
#
|
|
19
|
+
# @return [String] A cleaned PIS string with no formatting symbols
|
|
20
|
+
#
|
|
21
|
+
# @example
|
|
22
|
+
# remove_symbols("123.45678.90-9")
|
|
23
|
+
# #=> "12345678909"
|
|
24
|
+
#
|
|
25
|
+
# remove_symbols("987.65432.10-0")
|
|
26
|
+
# #=> "98765432100"
|
|
27
|
+
#
|
|
28
|
+
# remove_symbols("12345678909")
|
|
29
|
+
# #=> "12345678909"
|
|
30
|
+
def self.remove_symbols(pis)
|
|
31
|
+
return '' unless pis.is_a?(String)
|
|
32
|
+
|
|
33
|
+
pis.gsub(/[.\-]/, '')
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Alias for remove_symbols
|
|
37
|
+
class << self
|
|
38
|
+
alias sieve remove_symbols
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Formats a valid PIS string with standard visual aid symbols.
|
|
42
|
+
#
|
|
43
|
+
# This function takes a valid numbers-only PIS string as input
|
|
44
|
+
# and adds standard formatting visual aid symbols for display.
|
|
45
|
+
#
|
|
46
|
+
# Format: XXX.XXXXX.XX-X
|
|
47
|
+
#
|
|
48
|
+
# @param pis [String] A valid numbers-only PIS string (11 digits)
|
|
49
|
+
#
|
|
50
|
+
# @return [String, nil] A formatted PIS string or nil if invalid
|
|
51
|
+
#
|
|
52
|
+
# @example
|
|
53
|
+
# format_pis("12345678909")
|
|
54
|
+
# #=> "123.45678.90-9"
|
|
55
|
+
#
|
|
56
|
+
# format_pis("98765432100")
|
|
57
|
+
# #=> "987.65432.10-0"
|
|
58
|
+
#
|
|
59
|
+
# format_pis("123456789")
|
|
60
|
+
# #=> nil (invalid)
|
|
61
|
+
def self.format_pis(pis)
|
|
62
|
+
return nil unless is_valid(pis)
|
|
63
|
+
|
|
64
|
+
"#{pis[0..2]}.#{pis[3..7]}.#{pis[8..9]}-#{pis[10]}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Alias for format_pis
|
|
68
|
+
class << self
|
|
69
|
+
alias format format_pis
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Returns whether the verifying checksum digit of the given PIS matches its base number.
|
|
73
|
+
#
|
|
74
|
+
# This function validates that:
|
|
75
|
+
# - The input is a string
|
|
76
|
+
# - The length is exactly 11 digits
|
|
77
|
+
# - All characters are digits
|
|
78
|
+
# - The checksum digit (last digit) is correct
|
|
79
|
+
#
|
|
80
|
+
# @param pis [String] PIS number as a string (11 digits)
|
|
81
|
+
#
|
|
82
|
+
# @return [Boolean] True if PIS is valid, false otherwise
|
|
83
|
+
#
|
|
84
|
+
# @example
|
|
85
|
+
# is_valid("12345678909")
|
|
86
|
+
# #=> true
|
|
87
|
+
#
|
|
88
|
+
# is_valid("123.45678.90-9")
|
|
89
|
+
# #=> false (contains symbols, use remove_symbols first)
|
|
90
|
+
#
|
|
91
|
+
# is_valid("12345678900")
|
|
92
|
+
# #=> false (invalid checksum)
|
|
93
|
+
#
|
|
94
|
+
# is_valid("123456789")
|
|
95
|
+
# #=> false (wrong length)
|
|
96
|
+
def self.is_valid(pis)
|
|
97
|
+
return false unless pis.is_a?(String)
|
|
98
|
+
return false unless pis.length == 11
|
|
99
|
+
return false unless pis.match?(/^\d+$/)
|
|
100
|
+
|
|
101
|
+
pis[-1] == checksum(pis[0..9]).to_s
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Alias for is_valid
|
|
105
|
+
class << self
|
|
106
|
+
alias valid? is_valid
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Generates a random valid Brazilian PIS number.
|
|
110
|
+
#
|
|
111
|
+
# This function generates a random PIS number with the following characteristics:
|
|
112
|
+
# - It has 11 digits
|
|
113
|
+
# - It passes the weight calculation check
|
|
114
|
+
#
|
|
115
|
+
# @return [String] A randomly generated valid PIS number as a string
|
|
116
|
+
#
|
|
117
|
+
# @example
|
|
118
|
+
# generate
|
|
119
|
+
# #=> "12345678909" (example, actual value is random)
|
|
120
|
+
#
|
|
121
|
+
# generate
|
|
122
|
+
# #=> "98765432100" (example)
|
|
123
|
+
def self.generate
|
|
124
|
+
base = rand(0..9_999_999_999).to_s.rjust(10, '0')
|
|
125
|
+
base + checksum(base).to_s
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Calculates the checksum digit of the given base PIS string.
|
|
129
|
+
#
|
|
130
|
+
# The checksum algorithm:
|
|
131
|
+
# 1. Multiply each of the first 10 digits by corresponding weight
|
|
132
|
+
# 2. Sum all products
|
|
133
|
+
# 3. Calculate: 11 - (sum % 11)
|
|
134
|
+
# 4. If result is 10 or 11, use 0 instead
|
|
135
|
+
#
|
|
136
|
+
# @param base_pis [String] The first 10 digits of a PIS number
|
|
137
|
+
#
|
|
138
|
+
# @return [Integer] The checksum digit (0-9)
|
|
139
|
+
#
|
|
140
|
+
# @private
|
|
141
|
+
def self.checksum(base_pis)
|
|
142
|
+
pis_digits = base_pis.chars.map(&:to_i)
|
|
143
|
+
pis_sum = pis_digits.zip(WEIGHTS).sum { |digit, weight| digit * weight }
|
|
144
|
+
check_digit = 11 - (pis_sum % 11)
|
|
145
|
+
|
|
146
|
+
[10, 11].include?(check_digit) ? 0 : check_digit
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
private_class_method :checksum
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
module BrazilianUtils
|
|
2
|
+
# Utilities for validating Brazilian RENAVAM numbers.
|
|
3
|
+
#
|
|
4
|
+
# RENAVAM (Registro Nacional de Veículos Automotores) is an 11-digit
|
|
5
|
+
# identification number for motor vehicles in Brazil.
|
|
6
|
+
#
|
|
7
|
+
# The last digit is a verification digit calculated from the first 10 digits.
|
|
8
|
+
module RENAVAMUtils
|
|
9
|
+
# Weights used for verification digit calculation
|
|
10
|
+
# Applied to the first 10 digits in reverse order
|
|
11
|
+
DV_WEIGHTS = [2, 3, 4, 5, 6, 7, 8, 9, 2, 3].freeze
|
|
12
|
+
|
|
13
|
+
# Validates the Brazilian vehicle registration number (RENAVAM).
|
|
14
|
+
#
|
|
15
|
+
# This function takes a RENAVAM string and checks if it is valid.
|
|
16
|
+
# A valid RENAVAM consists of exactly 11 digits, with the last digit as
|
|
17
|
+
# a verification digit calculated from the previous 10 digits.
|
|
18
|
+
#
|
|
19
|
+
# @param renavam [String] The RENAVAM string to be validated
|
|
20
|
+
#
|
|
21
|
+
# @return [Boolean] True if the RENAVAM is valid, false otherwise
|
|
22
|
+
#
|
|
23
|
+
# @example
|
|
24
|
+
# is_valid_renavam("86769597308")
|
|
25
|
+
# #=> true
|
|
26
|
+
#
|
|
27
|
+
# is_valid_renavam("12345678901")
|
|
28
|
+
# #=> false
|
|
29
|
+
#
|
|
30
|
+
# is_valid_renavam("1234567890a")
|
|
31
|
+
# #=> false (contains letter)
|
|
32
|
+
#
|
|
33
|
+
# is_valid_renavam("12345678 901")
|
|
34
|
+
# #=> false (contains space)
|
|
35
|
+
#
|
|
36
|
+
# is_valid_renavam("12345678")
|
|
37
|
+
# #=> false (wrong length)
|
|
38
|
+
#
|
|
39
|
+
# is_valid_renavam("")
|
|
40
|
+
# #=> false
|
|
41
|
+
def self.is_valid_renavam(renavam)
|
|
42
|
+
return false unless validate_renavam_format(renavam)
|
|
43
|
+
|
|
44
|
+
calculate_renavam_dv(renavam) == renavam[-1].to_i
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Alias for is_valid_renavam
|
|
48
|
+
class << self
|
|
49
|
+
alias is_valid is_valid_renavam
|
|
50
|
+
alias valid? is_valid_renavam
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Validates the format of a RENAVAM string.
|
|
54
|
+
#
|
|
55
|
+
# Checks:
|
|
56
|
+
# - Must be a string
|
|
57
|
+
# - Must be exactly 11 digits
|
|
58
|
+
# - Cannot be all the same digit (e.g., "11111111111")
|
|
59
|
+
#
|
|
60
|
+
# @param renavam [String] The RENAVAM string to validate
|
|
61
|
+
#
|
|
62
|
+
# @return [Boolean] True if format is valid, false otherwise
|
|
63
|
+
#
|
|
64
|
+
# @private
|
|
65
|
+
def self.validate_renavam_format(renavam)
|
|
66
|
+
return false unless renavam.is_a?(String)
|
|
67
|
+
return false unless renavam.length == 11
|
|
68
|
+
return false unless renavam.match?(/^\d+$/)
|
|
69
|
+
return false if renavam.chars.uniq.length == 1 # All same digit
|
|
70
|
+
|
|
71
|
+
true
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private_class_method :validate_renavam_format
|
|
75
|
+
|
|
76
|
+
# Sums the weighted digits of a RENAVAM.
|
|
77
|
+
#
|
|
78
|
+
# Takes the first 10 digits, reverses them, multiplies each by the
|
|
79
|
+
# corresponding weight, and returns the sum.
|
|
80
|
+
#
|
|
81
|
+
# @param renavam [String] The RENAVAM string
|
|
82
|
+
#
|
|
83
|
+
# @return [Integer] The sum of weighted digits
|
|
84
|
+
#
|
|
85
|
+
# @private
|
|
86
|
+
def self.sum_weighted_digits(renavam)
|
|
87
|
+
base_digits = renavam[0..9].reverse.chars.map(&:to_i)
|
|
88
|
+
base_digits.zip(DV_WEIGHTS).sum { |digit, weight| digit * weight }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private_class_method :sum_weighted_digits
|
|
92
|
+
|
|
93
|
+
# Calculates the verification digit for a RENAVAM.
|
|
94
|
+
#
|
|
95
|
+
# Algorithm:
|
|
96
|
+
# 1. Sum the weighted digits (first 10 digits reversed, multiplied by weights)
|
|
97
|
+
# 2. Calculate: 11 - (sum % 11)
|
|
98
|
+
# 3. If result >= 10, return 0, otherwise return the result
|
|
99
|
+
#
|
|
100
|
+
# @param renavam [String] The RENAVAM string
|
|
101
|
+
#
|
|
102
|
+
# @return [Integer] The verification digit (0-9)
|
|
103
|
+
#
|
|
104
|
+
# @private
|
|
105
|
+
def self.calculate_renavam_dv(renavam)
|
|
106
|
+
weighted_sum = sum_weighted_digits(renavam)
|
|
107
|
+
dv = 11 - (weighted_sum % 11)
|
|
108
|
+
dv >= 10 ? 0 : dv
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private_class_method :calculate_renavam_dv
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BrazilianUtils
|
|
4
|
+
module VoterIdUtils
|
|
5
|
+
# UF codes mapping to federative union numbers
|
|
6
|
+
UF_CODES = {
|
|
7
|
+
'SP' => '01', 'MG' => '02', 'RJ' => '03', 'RS' => '04',
|
|
8
|
+
'BA' => '05', 'PR' => '06', 'CE' => '07', 'PE' => '08',
|
|
9
|
+
'SC' => '09', 'GO' => '10', 'MA' => '11', 'PB' => '12',
|
|
10
|
+
'PA' => '13', 'ES' => '14', 'PI' => '15', 'RN' => '16',
|
|
11
|
+
'AL' => '17', 'MT' => '18', 'MS' => '19', 'DF' => '20',
|
|
12
|
+
'SE' => '21', 'AM' => '22', 'RO' => '23', 'AC' => '24',
|
|
13
|
+
'AP' => '25', 'RR' => '26', 'TO' => '27', 'ZZ' => '28'
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# Check if a Brazilian voter ID number is valid.
|
|
19
|
+
#
|
|
20
|
+
# @param voter_id [String] The voter ID to validate
|
|
21
|
+
# @return [Boolean] true if valid, false otherwise
|
|
22
|
+
def is_valid_voter_id(voter_id)
|
|
23
|
+
# Ensure voter_id is a string with only digits and valid length
|
|
24
|
+
return false unless voter_id.is_a?(String)
|
|
25
|
+
return false unless voter_id.match?(/^\d+$/)
|
|
26
|
+
return false unless is_length_valid?(voter_id)
|
|
27
|
+
|
|
28
|
+
# Extract parts (using negative indexing for federative union and verifying digits)
|
|
29
|
+
sequential_number = get_sequential_number(voter_id)
|
|
30
|
+
federative_union = get_federative_union(voter_id)
|
|
31
|
+
verifying_digits = get_verifying_digits(voter_id)
|
|
32
|
+
|
|
33
|
+
# Validate federative union
|
|
34
|
+
return false unless is_federative_union_valid?(federative_union)
|
|
35
|
+
|
|
36
|
+
# Validate first verifying digit
|
|
37
|
+
vd1 = calculate_vd1(sequential_number, federative_union)
|
|
38
|
+
return false if vd1 != verifying_digits[0].to_i
|
|
39
|
+
|
|
40
|
+
# Validate second verifying digit
|
|
41
|
+
vd2 = calculate_vd2(federative_union, vd1)
|
|
42
|
+
return false if vd2 != verifying_digits[1].to_i
|
|
43
|
+
|
|
44
|
+
true
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Alias for is_valid_voter_id
|
|
48
|
+
alias valid_voter_id? is_valid_voter_id
|
|
49
|
+
|
|
50
|
+
# Format a voter ID for display with visual spaces.
|
|
51
|
+
#
|
|
52
|
+
# @param voter_id [String] The voter ID to format
|
|
53
|
+
# @return [String, nil] Formatted voter ID or nil if invalid
|
|
54
|
+
def format_voter_id(voter_id)
|
|
55
|
+
return nil unless is_valid_voter_id(voter_id)
|
|
56
|
+
|
|
57
|
+
"#{voter_id[0..3]} #{voter_id[4..7]} #{voter_id[8..9]} #{voter_id[10..11]}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Alias for format_voter_id
|
|
61
|
+
alias format format_voter_id
|
|
62
|
+
|
|
63
|
+
# Generate a random valid Brazilian voter ID.
|
|
64
|
+
#
|
|
65
|
+
# @param federative_union [String] The UF code (e.g., "SP", "MG", "ZZ")
|
|
66
|
+
# @return [String, nil] A valid voter ID or nil if UF is invalid
|
|
67
|
+
def generate(federative_union = 'ZZ')
|
|
68
|
+
federative_union = federative_union.upcase
|
|
69
|
+
return nil unless UF_CODES.key?(federative_union)
|
|
70
|
+
|
|
71
|
+
uf_number = UF_CODES[federative_union]
|
|
72
|
+
return nil unless is_federative_union_valid?(uf_number)
|
|
73
|
+
|
|
74
|
+
# Generate random 8-digit sequential number
|
|
75
|
+
sequential_number = format('%08d', rand(0..99_999_999))
|
|
76
|
+
|
|
77
|
+
# Calculate verification digits
|
|
78
|
+
vd1 = calculate_vd1(sequential_number, uf_number)
|
|
79
|
+
vd2 = calculate_vd2(uf_number, vd1)
|
|
80
|
+
|
|
81
|
+
"#{sequential_number}#{uf_number}#{vd1}#{vd2}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
# Check if the length of the voter ID is valid.
|
|
87
|
+
# Typically 12 digits, but can be 13 for SP and MG (edge case).
|
|
88
|
+
def is_length_valid?(voter_id)
|
|
89
|
+
return true if voter_id.length == 12
|
|
90
|
+
|
|
91
|
+
# Edge case: SP and MG can have 13 digits
|
|
92
|
+
federative_union = get_federative_union(voter_id)
|
|
93
|
+
voter_id.length == 13 && ['01', '02'].include?(federative_union)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Extract the sequential number (first 8 digits).
|
|
97
|
+
def get_sequential_number(voter_id)
|
|
98
|
+
voter_id[0..7]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Extract the federative union (2 digits before last 2 digits).
|
|
102
|
+
def get_federative_union(voter_id)
|
|
103
|
+
voter_id[-4..-3]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Extract the verifying digits (last 2 digits).
|
|
107
|
+
def get_verifying_digits(voter_id)
|
|
108
|
+
voter_id[-2..-1]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Check if the federative union is valid (between '01' and '28').
|
|
112
|
+
def is_federative_union_valid?(federative_union)
|
|
113
|
+
num = federative_union.to_i
|
|
114
|
+
num >= 1 && num <= 28
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Calculate the first verifying digit.
|
|
118
|
+
#
|
|
119
|
+
# @param sequential_number [String] First 8 digits
|
|
120
|
+
# @param federative_union [String] 2-digit UF code
|
|
121
|
+
# @return [Integer] The first verification digit
|
|
122
|
+
def calculate_vd1(sequential_number, federative_union)
|
|
123
|
+
# Weights: 2, 3, 4, 5, 6, 7, 8, 9
|
|
124
|
+
weights = (2..9).to_a
|
|
125
|
+
|
|
126
|
+
sum = sequential_number.chars.each_with_index.sum do |digit, index|
|
|
127
|
+
digit.to_i * weights[index]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
rest = sum % 11
|
|
131
|
+
vd1 = rest
|
|
132
|
+
|
|
133
|
+
# Edge case: rest == 0 and federative_union is SP ('01') or MG ('02')
|
|
134
|
+
vd1 = 1 if rest == 0 && ['01', '02'].include?(federative_union)
|
|
135
|
+
|
|
136
|
+
# Edge case: rest == 10
|
|
137
|
+
vd1 = 0 if rest == 10
|
|
138
|
+
|
|
139
|
+
vd1
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Calculate the second verifying digit.
|
|
143
|
+
#
|
|
144
|
+
# @param federative_union [String] 2-digit UF code
|
|
145
|
+
# @param vd1 [Integer] First verification digit
|
|
146
|
+
# @return [Integer] The second verification digit
|
|
147
|
+
def calculate_vd2(federative_union, vd1)
|
|
148
|
+
# Weights: 7, 8, 9
|
|
149
|
+
sum = (federative_union[0].to_i * 7) +
|
|
150
|
+
(federative_union[1].to_i * 8) +
|
|
151
|
+
(vd1 * 9)
|
|
152
|
+
|
|
153
|
+
rest = sum % 11
|
|
154
|
+
vd2 = rest
|
|
155
|
+
|
|
156
|
+
# Edge case: rest == 0 and federative_union is SP ('01') or MG ('02')
|
|
157
|
+
vd2 = 1 if rest == 0 && ['01', '02'].include?(federative_union)
|
|
158
|
+
|
|
159
|
+
# Edge case: rest == 10
|
|
160
|
+
vd2 = 0 if rest == 10
|
|
161
|
+
|
|
162
|
+
vd2
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|