br-utils 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +27 -0
- data/lib/brazilian-utils/cep-utils.rb +9 -0
- data/lib/brazilian-utils/cnh-utils.rb +15 -15
- data/lib/brazilian-utils/cnpj-utils.rb +1 -1
- data/lib/brazilian-utils/cpf-utils.rb +1 -1
- data/lib/brazilian-utils/currency-utils.rb +38 -36
- data/lib/brazilian-utils/date-utils.rb +12 -0
- data/lib/brazilian-utils/email-utils.rb +20 -9
- data/lib/brazilian-utils/legal-nature-utils.rb +2 -2
- data/lib/brazilian-utils/legal-process-utils.rb +4 -2
- data/lib/brazilian-utils/license-plate-utils.rb +5 -1
- data/lib/brazilian-utils/phone-utils.rb +11 -3
- data/lib/brazilian-utils/voter-id-utils.rb +34 -19
- metadata +16 -2
- data/.travis.yml +0 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7410285ed2d94f3143069921599888b0c3c4055be41f25454e32e4e8f61a38f3
|
|
4
|
+
data.tar.gz: 4e6fb1f71012f46622c7a3e4dc78bf26eaa7ac864d7122f1c1ece3bc912bb94e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2f3644c35d9e2332c02efefe4736ca7b0faab1c342edd5390ff706ba03b1773f7f52680e233010643de5a4077b6b55c5e246228a1485094427587a32cbc595ef
|
|
7
|
+
data.tar.gz: 7d6813dd96f7b6f0db6953c014ecb9609a33e95666ccee310deab668767c7fe92c677c7b6b669661a629fcd056316b670831e131864f90d942fbc178c56bc79b
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [master]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: false
|
|
14
|
+
matrix:
|
|
15
|
+
ruby: ['3.1', '3.2', '3.3', '3.4']
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Set up Ruby ${{ matrix.ruby }}
|
|
21
|
+
uses: ruby/setup-ruby@v1
|
|
22
|
+
with:
|
|
23
|
+
ruby-version: ${{ matrix.ruby }}
|
|
24
|
+
bundler-cache: true
|
|
25
|
+
|
|
26
|
+
- name: Run tests
|
|
27
|
+
run: bundle exec rake spec
|
|
@@ -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,
|
|
@@ -203,9 +206,12 @@ module BrazilianUtils
|
|
|
203
206
|
cep: data['cep'],
|
|
204
207
|
logradouro: data['logradouro'],
|
|
205
208
|
complemento: data['complemento'],
|
|
209
|
+
unidade: data['unidade'],
|
|
206
210
|
bairro: data['bairro'],
|
|
207
211
|
localidade: data['localidade'],
|
|
208
212
|
uf: data['uf'],
|
|
213
|
+
estado: data['estado'],
|
|
214
|
+
regiao: data['regiao'],
|
|
209
215
|
ibge: data['ibge'],
|
|
210
216
|
gia: data['gia'],
|
|
211
217
|
ddd: data['ddd'],
|
|
@@ -294,9 +300,12 @@ module BrazilianUtils
|
|
|
294
300
|
cep: address_data['cep'],
|
|
295
301
|
logradouro: address_data['logradouro'],
|
|
296
302
|
complemento: address_data['complemento'],
|
|
303
|
+
unidade: address_data['unidade'],
|
|
297
304
|
bairro: address_data['bairro'],
|
|
298
305
|
localidade: address_data['localidade'],
|
|
299
306
|
uf: address_data['uf'],
|
|
307
|
+
estado: address_data['estado'],
|
|
308
|
+
regiao: address_data['regiao'],
|
|
300
309
|
ibge: address_data['ibge'],
|
|
301
310
|
gia: address_data['gia'],
|
|
302
311
|
ddd: address_data['ddd'],
|
|
@@ -32,41 +32,41 @@ module BrazilianUtils
|
|
|
32
32
|
first_verificator = digits[9]
|
|
33
33
|
second_verificator = digits[10]
|
|
34
34
|
|
|
35
|
-
# Check the 10th digit
|
|
36
|
-
|
|
35
|
+
# Check the 10th digit; keep the raw remainder (0-10), since the
|
|
36
|
+
# decrement applied to the 11th digit depends on whether the *pre-cap*
|
|
37
|
+
# remainder overflowed (>9), not on the printed digit (always 0-9).
|
|
38
|
+
first_rest = calculate_first_rest(digits)
|
|
39
|
+
expected_first = first_rest > 9 ? 0 : first_rest
|
|
40
|
+
return false unless expected_first == first_verificator
|
|
37
41
|
|
|
38
42
|
# Check the 11th digit
|
|
39
|
-
check_second_verificator(digits, second_verificator,
|
|
43
|
+
check_second_verificator(digits, second_verificator, first_rest)
|
|
40
44
|
end
|
|
41
45
|
|
|
42
|
-
#
|
|
46
|
+
# Computes the raw remainder used to derive the CNH's first verification digit.
|
|
43
47
|
#
|
|
44
48
|
# @param digits [Array<Integer>] Array of CNH digits
|
|
45
|
-
# @
|
|
46
|
-
# @return [Boolean] true if the first verificator is valid
|
|
49
|
+
# @return [Integer] The remainder (0-10) before capping to a single printable digit
|
|
47
50
|
#
|
|
48
51
|
# @private
|
|
49
|
-
def self.
|
|
52
|
+
def self.calculate_first_rest(digits)
|
|
50
53
|
sum = 0
|
|
51
54
|
9.times do |i|
|
|
52
55
|
sum += digits[i] * (9 - i)
|
|
53
56
|
end
|
|
54
57
|
|
|
55
|
-
sum
|
|
56
|
-
result = sum > 9 ? 0 : sum
|
|
57
|
-
|
|
58
|
-
result == first_verificator
|
|
58
|
+
sum % 11
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
# Generates the second verification digit and uses it to verify the 11th digit of the CNH
|
|
62
62
|
#
|
|
63
63
|
# @param digits [Array<Integer>] Array of CNH digits
|
|
64
64
|
# @param second_verificator [Integer] The second verification digit (11th digit)
|
|
65
|
-
# @param
|
|
65
|
+
# @param first_rest [Integer] The raw remainder (0-10) used to compute the 10th digit
|
|
66
66
|
# @return [Boolean] true if the second verificator is valid
|
|
67
67
|
#
|
|
68
68
|
# @private
|
|
69
|
-
def self.check_second_verificator(digits, second_verificator,
|
|
69
|
+
def self.check_second_verificator(digits, second_verificator, first_rest)
|
|
70
70
|
sum = 0
|
|
71
71
|
9.times do |i|
|
|
72
72
|
sum += digits[i] * (i + 1)
|
|
@@ -74,7 +74,7 @@ module BrazilianUtils
|
|
|
74
74
|
|
|
75
75
|
result = sum % 11
|
|
76
76
|
|
|
77
|
-
if
|
|
77
|
+
if first_rest > 9
|
|
78
78
|
result = (result - 2).negative? ? result + 9 : result - 2
|
|
79
79
|
end
|
|
80
80
|
|
|
@@ -83,6 +83,6 @@ module BrazilianUtils
|
|
|
83
83
|
result == second_verificator
|
|
84
84
|
end
|
|
85
85
|
|
|
86
|
-
private_class_method :
|
|
86
|
+
private_class_method :calculate_first_rest, :check_second_verificator
|
|
87
87
|
end
|
|
88
88
|
end
|
|
@@ -170,7 +170,7 @@ module BrazilianUtils
|
|
|
170
170
|
(position - 8).downto(2) { |w| weights << w }
|
|
171
171
|
9.downto(2) { |w| weights << w }
|
|
172
172
|
|
|
173
|
-
val = cnpj.chars.zip(weights).sum do |digit, weight|
|
|
173
|
+
val = cnpj.chars.first(position - 1).zip(weights).sum do |digit, weight|
|
|
174
174
|
digit.to_i * weight
|
|
175
175
|
end % 11
|
|
176
176
|
|
|
@@ -161,7 +161,7 @@ module BrazilianUtils
|
|
|
161
161
|
#
|
|
162
162
|
# @private
|
|
163
163
|
def self.hashdigit(cpf, position)
|
|
164
|
-
val = cpf.chars.zip(position.downto(2)).sum do |digit, weight|
|
|
164
|
+
val = cpf.chars.first(position - 1).zip(position.downto(2)).sum do |digit, weight|
|
|
165
165
|
digit.to_i * weight
|
|
166
166
|
end % 11
|
|
167
167
|
|
|
@@ -42,7 +42,7 @@ module BrazilianUtils
|
|
|
42
42
|
#
|
|
43
43
|
# @example
|
|
44
44
|
# convert_real_to_text(1523.45)
|
|
45
|
-
# #=> "Mil
|
|
45
|
+
# #=> "Mil quinhentos e vinte e três reais e quarenta e cinco centavos"
|
|
46
46
|
#
|
|
47
47
|
# convert_real_to_text(1.00)
|
|
48
48
|
# #=> "Um real"
|
|
@@ -72,7 +72,10 @@ module BrazilianUtils
|
|
|
72
72
|
if reais > 0
|
|
73
73
|
reais_text = number_to_words(reais)
|
|
74
74
|
currency_text = reais == 1 ? 'real' : 'reais'
|
|
75
|
-
|
|
75
|
+
# "de" only applies when the text ends in "milhão(ões)"/"bilhão(ões)"/...
|
|
76
|
+
# on its own (e.g. "um milhão de reais"), not when it's followed by
|
|
77
|
+
# more words (e.g. "um milhão e um reais", no "de").
|
|
78
|
+
conector = reais_text.match?(/lhão$|lhões$/) ? 'de ' : ''
|
|
76
79
|
parts << "#{reais_text} #{conector}#{currency_text}"
|
|
77
80
|
end
|
|
78
81
|
|
|
@@ -104,8 +107,9 @@ module BrazilianUtils
|
|
|
104
107
|
#
|
|
105
108
|
# @private
|
|
106
109
|
def self.number_to_words(number)
|
|
110
|
+
number = number.to_i.abs
|
|
107
111
|
return 'zero' if number.zero?
|
|
108
|
-
|
|
112
|
+
|
|
109
113
|
# Scale names
|
|
110
114
|
scales = [
|
|
111
115
|
'',
|
|
@@ -115,7 +119,7 @@ module BrazilianUtils
|
|
|
115
119
|
'trilhão',
|
|
116
120
|
'quadrilhão'
|
|
117
121
|
]
|
|
118
|
-
|
|
122
|
+
|
|
119
123
|
scales_plural = [
|
|
120
124
|
'',
|
|
121
125
|
'mil',
|
|
@@ -124,49 +128,47 @@ module BrazilianUtils
|
|
|
124
128
|
'trilhões',
|
|
125
129
|
'quadrilhões'
|
|
126
130
|
]
|
|
127
|
-
|
|
128
|
-
# Break number into groups of 3 digits
|
|
131
|
+
|
|
132
|
+
# Break number into groups of 3 digits, lowest order first
|
|
133
|
+
# (groups[0] is units-hundreds, groups[1] is thousands, ...)
|
|
129
134
|
groups = []
|
|
130
135
|
temp = number
|
|
131
136
|
while temp > 0
|
|
132
137
|
groups << temp % 1000
|
|
133
138
|
temp /= 1000
|
|
134
139
|
end
|
|
135
|
-
|
|
136
|
-
|
|
140
|
+
|
|
141
|
+
parts = []
|
|
137
142
|
groups.each_with_index do |group, index|
|
|
138
143
|
next if group.zero?
|
|
139
|
-
|
|
144
|
+
|
|
140
145
|
group_text = convert_group(group)
|
|
141
146
|
scale_name = group == 1 ? scales[index] : scales_plural[index]
|
|
142
|
-
|
|
143
|
-
if scale_name.empty?
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
end
|
|
151
|
-
else
|
|
152
|
-
result << "#{group_text} #{scale_name}"
|
|
153
|
-
end
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
# Join with "e" where appropriate
|
|
157
|
-
if result.length > 1
|
|
158
|
-
last = result.pop
|
|
159
|
-
result_text = result.reverse.join(', ')
|
|
160
|
-
|
|
161
|
-
# Check if we need "e" before the last part
|
|
162
|
-
if number % 1000 < 100 && number % 1000 > 0
|
|
163
|
-
"#{result_text} e #{last}"
|
|
164
|
-
else
|
|
165
|
-
"#{result_text}, #{last}"
|
|
166
|
-
end
|
|
167
|
-
else
|
|
168
|
-
result.first || 'zero'
|
|
147
|
+
|
|
148
|
+
parts << if scale_name.empty?
|
|
149
|
+
group_text
|
|
150
|
+
elsif index == 1 && group == 1 # "mil" doesn't need "um" before it
|
|
151
|
+
scale_name
|
|
152
|
+
else
|
|
153
|
+
"#{group_text} #{scale_name}"
|
|
154
|
+
end
|
|
169
155
|
end
|
|
156
|
+
|
|
157
|
+
# parts was built lowest-order group first; the spoken form reads
|
|
158
|
+
# highest order first (e.g. "mil duzentos e trinta e quatro", not
|
|
159
|
+
# "duzentos e trinta e quatro, mil").
|
|
160
|
+
parts.reverse!
|
|
161
|
+
return parts.first if parts.length == 1
|
|
162
|
+
|
|
163
|
+
# The lowest-order non-zero group (spoken last) gets an "e" in front
|
|
164
|
+
# when it reads as a single small/round term: below 100, or an exact
|
|
165
|
+
# multiple of 100 (e.g. "mil e quatrocentos", "mil e um"); otherwise
|
|
166
|
+
# groups are simply concatenated with a space, no comma
|
|
167
|
+
# (Manual de Redação da Presidência: "mil duzentos e cinquenta reais").
|
|
168
|
+
final_group = groups.find { |g| !g.zero? }
|
|
169
|
+
last = parts.pop
|
|
170
|
+
separator = (final_group < 100 || (final_group % 100).zero?) ? ' e ' : ' '
|
|
171
|
+
"#{parts.join(' ')}#{separator}#{last}"
|
|
170
172
|
end
|
|
171
173
|
|
|
172
174
|
# Converts a group of 3 digits (0-999) to words.
|
|
@@ -38,6 +38,13 @@ module BrazilianUtils
|
|
|
38
38
|
[12, 25] => 'Natal'
|
|
39
39
|
}.freeze
|
|
40
40
|
|
|
41
|
+
# Lei 14.759/2023 made November 20th (Dia Nacional de Zumbi e da
|
|
42
|
+
# Consciência Negra) a national holiday starting in 2024; before that it
|
|
43
|
+
# was only a holiday in the states/cities that already had their own law
|
|
44
|
+
# for it (some of which are still listed in STATE_HOLIDAYS below).
|
|
45
|
+
NATIONAL_CONSCIENCIA_NEGRA_MONTH_DAY = [11, 20].freeze
|
|
46
|
+
NATIONAL_CONSCIENCIA_NEGRA_EFFECTIVE_YEAR = 2024
|
|
47
|
+
|
|
41
48
|
# State-specific holidays (fixed dates)
|
|
42
49
|
STATE_HOLIDAYS = {
|
|
43
50
|
'AC' => { [1, 23] => 'Dia do Evangélico', [6, 15] => 'Aniversário do Acre', [9, 5] => 'Dia da Amazônia', [11, 17] => 'Assinatura do Tratado de Petrópolis' },
|
|
@@ -112,6 +119,10 @@ module BrazilianUtils
|
|
|
112
119
|
# Check national holidays
|
|
113
120
|
return true if NATIONAL_HOLIDAYS.key?(month_day)
|
|
114
121
|
|
|
122
|
+
if month_day == NATIONAL_CONSCIENCIA_NEGRA_MONTH_DAY && date.year >= NATIONAL_CONSCIENCIA_NEGRA_EFFECTIVE_YEAR
|
|
123
|
+
return true
|
|
124
|
+
end
|
|
125
|
+
|
|
115
126
|
# Check state holidays if UF is provided
|
|
116
127
|
if uf
|
|
117
128
|
state_uf = uf.to_s.upcase
|
|
@@ -138,6 +149,7 @@ module BrazilianUtils
|
|
|
138
149
|
# convert_date_to_text("15/03/2024") #=> "Quinze de março de dois mil e vinte e quatro"
|
|
139
150
|
# convert_date_to_text("invalid") #=> nil
|
|
140
151
|
def self.convert_date_to_text(date)
|
|
152
|
+
return nil unless date.is_a?(String)
|
|
141
153
|
return nil unless DATE_REGEX.match?(date)
|
|
142
154
|
|
|
143
155
|
begin
|
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
module BrazilianUtils
|
|
2
2
|
module EmailUtils
|
|
3
|
-
# Email validation pattern based on RFC 5322
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# -
|
|
11
|
-
|
|
3
|
+
# Email validation pattern based on RFC 5322/RFC 1035.
|
|
4
|
+
#
|
|
5
|
+
# This is stricter than brutils/python's reference regex
|
|
6
|
+
# (`^(?![.])[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`), which lets
|
|
7
|
+
# through addresses with a leading/trailing/doubled dot in the local
|
|
8
|
+
# part, or a domain label starting/ending with a hyphen. Each dot-joined
|
|
9
|
+
# segment is validated on its own:
|
|
10
|
+
# - Local part: one or more non-empty segments (letters, digits,
|
|
11
|
+
# `._%+-`) joined by single dots - no leading/trailing/consecutive dot
|
|
12
|
+
# - Domain labels: alphanumeric, may contain internal hyphens, but must
|
|
13
|
+
# not start or end with one (RFC 1035 "preferred name syntax")
|
|
14
|
+
# - TLD: at least 2 letters, no digits or hyphens
|
|
15
|
+
EMAIL_PATTERN = %r{
|
|
16
|
+
\A
|
|
17
|
+
[a-zA-Z0-9_%+-]+ (?: \. [a-zA-Z0-9_%+-]+ )* # local part
|
|
18
|
+
@
|
|
19
|
+
(?: [a-zA-Z0-9] (?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])? \. )+ # domain labels
|
|
20
|
+
[a-zA-Z]{2,63} # TLD
|
|
21
|
+
\z
|
|
22
|
+
}x.freeze
|
|
12
23
|
|
|
13
24
|
# Checks if a string corresponds to a valid email address.
|
|
14
25
|
#
|
|
@@ -39,7 +39,7 @@ module BrazilianUtils
|
|
|
39
39
|
'2046' => 'Sociedade Anônima Aberta',
|
|
40
40
|
'2054' => 'Sociedade Anônima Fechada',
|
|
41
41
|
'2062' => 'Sociedade Empresária Limitada',
|
|
42
|
-
'
|
|
42
|
+
'2076' => 'Sociedade Empresária em Nome Coletivo',
|
|
43
43
|
'2089' => 'Sociedade Empresária em Comandita Simples',
|
|
44
44
|
'2097' => 'Sociedade Empresária em Comandita por Ações',
|
|
45
45
|
'2100' => 'Sociedade Mercantil de Capital e Indústria (extinta pelo NCC/2002)',
|
|
@@ -177,7 +177,7 @@ module BrazilianUtils
|
|
|
177
177
|
# #=> "Sociedade Empresária Limitada"
|
|
178
178
|
#
|
|
179
179
|
# all_codes.size
|
|
180
|
-
# #=>
|
|
180
|
+
# #=> 60 (total number of codes in the official table)
|
|
181
181
|
def self.list_all
|
|
182
182
|
LEGAL_NATURE.dup
|
|
183
183
|
end
|
|
@@ -177,7 +177,9 @@ module BrazilianUtils
|
|
|
177
177
|
|
|
178
178
|
# Calculates the checksum (verification digits) for a legal process ID.
|
|
179
179
|
#
|
|
180
|
-
# The checksum is calculated as:
|
|
180
|
+
# The checksum is calculated as: 98 - ((basenum * 100) % 97), padded to 2 digits.
|
|
181
|
+
# This is the ISO 7064 MOD 97-10 check digit used by CNJ Resolution 65/2008,
|
|
182
|
+
# art. 1º, § 2º.
|
|
181
183
|
#
|
|
182
184
|
# @param basenum [Integer] The base number for checksum calculation
|
|
183
185
|
# (without the verification digits)
|
|
@@ -186,7 +188,7 @@ module BrazilianUtils
|
|
|
186
188
|
#
|
|
187
189
|
# @private
|
|
188
190
|
def self.checksum(basenum)
|
|
189
|
-
result =
|
|
191
|
+
result = 98 - ((basenum * 100) % 97)
|
|
190
192
|
result.to_s.rjust(2, '0')
|
|
191
193
|
end
|
|
192
194
|
|
|
@@ -36,9 +36,11 @@ module BrazilianUtils
|
|
|
36
36
|
# convert_to_mercosul("ABC4*67")
|
|
37
37
|
# #=> nil
|
|
38
38
|
def self.convert_to_mercosul(license_plate)
|
|
39
|
-
return nil unless
|
|
39
|
+
return nil unless license_plate.is_a?(String)
|
|
40
40
|
|
|
41
41
|
clean = remove_symbols(license_plate).upcase
|
|
42
|
+
return nil unless valid_old_format?(clean)
|
|
43
|
+
|
|
42
44
|
chars = clean.chars
|
|
43
45
|
|
|
44
46
|
# Convert the 5th character (index 4) - the first digit after the letters
|
|
@@ -223,6 +225,8 @@ module BrazilianUtils
|
|
|
223
225
|
# generate('invalid')
|
|
224
226
|
# #=> nil
|
|
225
227
|
def self.generate(format = 'LLLNLNN')
|
|
228
|
+
return nil unless format.is_a?(String)
|
|
229
|
+
|
|
226
230
|
format_upper = format.upcase
|
|
227
231
|
|
|
228
232
|
return nil unless ['LLLNLNN', 'LLLNNNN'].include?(format_upper)
|
|
@@ -181,9 +181,17 @@ module BrazilianUtils
|
|
|
181
181
|
def self.remove_international_dialing_code(phone_number)
|
|
182
182
|
return '' unless phone_number.is_a?(String)
|
|
183
183
|
|
|
184
|
-
#
|
|
185
|
-
|
|
186
|
-
|
|
184
|
+
# Only touch a "clean" digit string (with an optional leading '+') that
|
|
185
|
+
# is longer than 11 digits; anything with spaces/hyphens/etc. is left
|
|
186
|
+
# alone rather than partially stripped.
|
|
187
|
+
digits_part = phone_number.sub(/\A\+/, '')
|
|
188
|
+
|
|
189
|
+
if INTERNATIONAL_CODE_PATTERN.match?(phone_number) &&
|
|
190
|
+
digits_part.match?(/\A\d+\z/) && digits_part.length > 11
|
|
191
|
+
# Anchor to the start so only the leading "+55"/"55" is stripped
|
|
192
|
+
# (a plain #sub would also drop the '+' and could hit an unrelated
|
|
193
|
+
# "55" further into the number, e.g. an RS-state "55" DDD).
|
|
194
|
+
phone_number.sub(/\A\+?55/, '')
|
|
187
195
|
else
|
|
188
196
|
phone_number
|
|
189
197
|
end
|
|
@@ -13,13 +13,11 @@ module BrazilianUtils
|
|
|
13
13
|
'AP' => '25', 'RR' => '26', 'TO' => '27', 'ZZ' => '28'
|
|
14
14
|
}.freeze
|
|
15
15
|
|
|
16
|
-
module_function
|
|
17
|
-
|
|
18
16
|
# Check if a Brazilian voter ID number is valid.
|
|
19
17
|
#
|
|
20
18
|
# @param voter_id [String] The voter ID to validate
|
|
21
19
|
# @return [Boolean] true if valid, false otherwise
|
|
22
|
-
def is_valid_voter_id(voter_id)
|
|
20
|
+
def self.is_valid_voter_id(voter_id)
|
|
23
21
|
# Ensure voter_id is a string with only digits and valid length
|
|
24
22
|
return false unless voter_id.is_a?(String)
|
|
25
23
|
return false unless voter_id.match?(/^\d+$/)
|
|
@@ -44,27 +42,37 @@ module BrazilianUtils
|
|
|
44
42
|
true
|
|
45
43
|
end
|
|
46
44
|
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
class << self
|
|
46
|
+
# Alias for is_valid_voter_id
|
|
47
|
+
alias valid_voter_id? is_valid_voter_id
|
|
48
|
+
end
|
|
49
49
|
|
|
50
50
|
# Format a voter ID for display with visual spaces.
|
|
51
51
|
#
|
|
52
52
|
# @param voter_id [String] The voter ID to format
|
|
53
53
|
# @return [String, nil] Formatted voter ID or nil if invalid
|
|
54
|
-
def format_voter_id(voter_id)
|
|
54
|
+
def self.format_voter_id(voter_id)
|
|
55
55
|
return nil unless is_valid_voter_id(voter_id)
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
# The 13-digit SP/MG variant has an extra sequential digit; grouping it
|
|
58
|
+
# as 4+4+2+2 (like the 12-digit case) silently drops the last digit.
|
|
59
|
+
if voter_id.length == 13
|
|
60
|
+
"#{voter_id[0..3]} #{voter_id[4..7]} #{voter_id[8]} #{voter_id[9..10]} #{voter_id[11..12]}"
|
|
61
|
+
else
|
|
62
|
+
"#{voter_id[0..3]} #{voter_id[4..7]} #{voter_id[8..9]} #{voter_id[10..11]}"
|
|
63
|
+
end
|
|
58
64
|
end
|
|
59
65
|
|
|
60
|
-
|
|
61
|
-
|
|
66
|
+
class << self
|
|
67
|
+
# Alias for format_voter_id
|
|
68
|
+
alias format format_voter_id
|
|
69
|
+
end
|
|
62
70
|
|
|
63
71
|
# Generate a random valid Brazilian voter ID.
|
|
64
72
|
#
|
|
65
73
|
# @param federative_union [String] The UF code (e.g., "SP", "MG", "ZZ")
|
|
66
74
|
# @return [String, nil] A valid voter ID or nil if UF is invalid
|
|
67
|
-
def generate(federative_union = 'ZZ')
|
|
75
|
+
def self.generate(federative_union = 'ZZ')
|
|
68
76
|
federative_union = federative_union.upcase
|
|
69
77
|
return nil unless UF_CODES.key?(federative_union)
|
|
70
78
|
|
|
@@ -72,7 +80,9 @@ module BrazilianUtils
|
|
|
72
80
|
return nil unless is_federative_union_valid?(uf_number)
|
|
73
81
|
|
|
74
82
|
# Generate random 8-digit sequential number
|
|
75
|
-
|
|
83
|
+
# (uses rjust, not Kernel#format, since `format` is aliased to
|
|
84
|
+
# format_voter_id on this module's singleton class below)
|
|
85
|
+
sequential_number = rand(0..99_999_999).to_s.rjust(8, '0')
|
|
76
86
|
|
|
77
87
|
# Calculate verification digits
|
|
78
88
|
vd1 = calculate_vd1(sequential_number, uf_number)
|
|
@@ -81,11 +91,12 @@ module BrazilianUtils
|
|
|
81
91
|
"#{sequential_number}#{uf_number}#{vd1}#{vd2}"
|
|
82
92
|
end
|
|
83
93
|
|
|
84
|
-
|
|
94
|
+
# PRIVATE METHODS
|
|
95
|
+
#################
|
|
85
96
|
|
|
86
97
|
# Check if the length of the voter ID is valid.
|
|
87
98
|
# Typically 12 digits, but can be 13 for SP and MG (edge case).
|
|
88
|
-
def is_length_valid?(voter_id)
|
|
99
|
+
def self.is_length_valid?(voter_id)
|
|
89
100
|
return true if voter_id.length == 12
|
|
90
101
|
|
|
91
102
|
# Edge case: SP and MG can have 13 digits
|
|
@@ -94,22 +105,22 @@ module BrazilianUtils
|
|
|
94
105
|
end
|
|
95
106
|
|
|
96
107
|
# Extract the sequential number (first 8 digits).
|
|
97
|
-
def get_sequential_number(voter_id)
|
|
108
|
+
def self.get_sequential_number(voter_id)
|
|
98
109
|
voter_id[0..7]
|
|
99
110
|
end
|
|
100
111
|
|
|
101
112
|
# Extract the federative union (2 digits before last 2 digits).
|
|
102
|
-
def get_federative_union(voter_id)
|
|
113
|
+
def self.get_federative_union(voter_id)
|
|
103
114
|
voter_id[-4..-3]
|
|
104
115
|
end
|
|
105
116
|
|
|
106
117
|
# Extract the verifying digits (last 2 digits).
|
|
107
|
-
def get_verifying_digits(voter_id)
|
|
118
|
+
def self.get_verifying_digits(voter_id)
|
|
108
119
|
voter_id[-2..-1]
|
|
109
120
|
end
|
|
110
121
|
|
|
111
122
|
# Check if the federative union is valid (between '01' and '28').
|
|
112
|
-
def is_federative_union_valid?(federative_union)
|
|
123
|
+
def self.is_federative_union_valid?(federative_union)
|
|
113
124
|
num = federative_union.to_i
|
|
114
125
|
num >= 1 && num <= 28
|
|
115
126
|
end
|
|
@@ -119,7 +130,7 @@ module BrazilianUtils
|
|
|
119
130
|
# @param sequential_number [String] First 8 digits
|
|
120
131
|
# @param federative_union [String] 2-digit UF code
|
|
121
132
|
# @return [Integer] The first verification digit
|
|
122
|
-
def calculate_vd1(sequential_number, federative_union)
|
|
133
|
+
def self.calculate_vd1(sequential_number, federative_union)
|
|
123
134
|
# Weights: 2, 3, 4, 5, 6, 7, 8, 9
|
|
124
135
|
weights = (2..9).to_a
|
|
125
136
|
|
|
@@ -144,7 +155,7 @@ module BrazilianUtils
|
|
|
144
155
|
# @param federative_union [String] 2-digit UF code
|
|
145
156
|
# @param vd1 [Integer] First verification digit
|
|
146
157
|
# @return [Integer] The second verification digit
|
|
147
|
-
def calculate_vd2(federative_union, vd1)
|
|
158
|
+
def self.calculate_vd2(federative_union, vd1)
|
|
148
159
|
# Weights: 7, 8, 9
|
|
149
160
|
sum = (federative_union[0].to_i * 7) +
|
|
150
161
|
(federative_union[1].to_i * 8) +
|
|
@@ -161,5 +172,9 @@ module BrazilianUtils
|
|
|
161
172
|
|
|
162
173
|
vd2
|
|
163
174
|
end
|
|
175
|
+
|
|
176
|
+
private_class_method :is_length_valid?, :get_sequential_number, :get_federative_union,
|
|
177
|
+
:get_verifying_digits, :is_federative_union_valid?, :calculate_vd1,
|
|
178
|
+
:calculate_vd2
|
|
164
179
|
end
|
|
165
180
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: br-utils
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex Rocha
|
|
@@ -10,6 +10,20 @@ bindir: bin
|
|
|
10
10
|
cert_chain: []
|
|
11
11
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: bigdecimal
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0'
|
|
13
27
|
- !ruby/object:Gem::Dependency
|
|
14
28
|
name: bundler
|
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -61,9 +75,9 @@ executables: []
|
|
|
61
75
|
extensions: []
|
|
62
76
|
extra_rdoc_files: []
|
|
63
77
|
files:
|
|
78
|
+
- ".github/workflows/ci.yml"
|
|
64
79
|
- ".gitignore"
|
|
65
80
|
- ".rspec"
|
|
66
|
-
- ".travis.yml"
|
|
67
81
|
- Gemfile
|
|
68
82
|
- LICENSE.txt
|
|
69
83
|
- README.md
|