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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +27 -0
  3. data/README.md +446 -22
  4. data/lib/brazilian-utils/area-code-utils.rb +60 -0
  5. data/lib/brazilian-utils/bank-account-utils.rb +82 -0
  6. data/lib/brazilian-utils/bank-utils.rb +61 -0
  7. data/lib/brazilian-utils/boleto-utils.rb +139 -0
  8. data/lib/brazilian-utils/caepf-utils.rb +91 -0
  9. data/lib/brazilian-utils/cbo-utils.rb +74 -0
  10. data/lib/brazilian-utils/cei-utils.rb +79 -0
  11. data/lib/brazilian-utils/cep-utils.rb +34 -1
  12. data/lib/brazilian-utils/certidao-utils.rb +171 -0
  13. data/lib/brazilian-utils/cfop-utils.rb +74 -0
  14. data/lib/brazilian-utils/cnae-utils.rb +104 -0
  15. data/lib/brazilian-utils/cnh-utils.rb +68 -15
  16. data/lib/brazilian-utils/cno-utils.rb +76 -0
  17. data/lib/brazilian-utils/cnpj-utils.rb +126 -13
  18. data/lib/brazilian-utils/cns-utils.rb +110 -0
  19. data/lib/brazilian-utils/cpf-utils.rb +14 -1
  20. data/lib/brazilian-utils/credit-card-utils.rb +47 -0
  21. data/lib/brazilian-utils/csosn-utils.rb +55 -0
  22. data/lib/brazilian-utils/cst-utils.rb +103 -0
  23. data/lib/brazilian-utils/currency-utils.rb +347 -226
  24. data/lib/brazilian-utils/data/area_codes.json +676 -0
  25. data/lib/brazilian-utils/data/banks.json +357 -0
  26. data/lib/brazilian-utils/data/cbo.json +1 -0
  27. data/lib/brazilian-utils/data/cfop.json +1 -0
  28. data/lib/brazilian-utils/data/cnae.json +1 -0
  29. data/lib/brazilian-utils/data/csosn.json +42 -0
  30. data/lib/brazilian-utils/data/cst.json +294 -0
  31. data/lib/brazilian-utils/data/legal_nature.json +900 -0
  32. data/lib/brazilian-utils/data/municipalities.json +1 -0
  33. data/lib/brazilian-utils/data/ncm.json +1 -0
  34. data/lib/brazilian-utils/data/states.json +218 -0
  35. data/lib/brazilian-utils/date-utils.rb +504 -244
  36. data/lib/brazilian-utils/email-utils.rb +20 -9
  37. data/lib/brazilian-utils/iban-utils.rb +120 -0
  38. data/lib/brazilian-utils/ie-utils.rb +84 -0
  39. data/lib/brazilian-utils/legal-nature-utils.rb +238 -235
  40. data/lib/brazilian-utils/legal-process-utils.rb +35 -5
  41. data/lib/brazilian-utils/license-plate-utils.rb +24 -4
  42. data/lib/brazilian-utils/municipality-utils.rb +58 -0
  43. data/lib/brazilian-utils/ncm-utils.rb +91 -0
  44. data/lib/brazilian-utils/nfe-key-utils.rb +158 -0
  45. data/lib/brazilian-utils/number-utils.rb +123 -0
  46. data/lib/brazilian-utils/passport-utils.rb +65 -0
  47. data/lib/brazilian-utils/phone-utils.rb +491 -272
  48. data/lib/brazilian-utils/pis-utils.rb +16 -1
  49. data/lib/brazilian-utils/pix-key-utils.rb +70 -0
  50. data/lib/brazilian-utils/pix-payload-utils.rb +253 -0
  51. data/lib/brazilian-utils/registro-profissional-utils.rb +112 -0
  52. data/lib/brazilian-utils/renavam-utils.rb +14 -0
  53. data/lib/brazilian-utils/state-utils.rb +105 -0
  54. data/lib/brazilian-utils/text-utils.rb +105 -0
  55. data/lib/brazilian-utils/vin-utils.rb +46 -0
  56. data/lib/brazilian-utils/voter-id-utils.rb +70 -19
  57. metadata +54 -2
  58. data/.travis.yml +0 -5
@@ -1,272 +1,491 @@
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
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
+ # Códigos Não Geográficos (Anatel) that take 7 digits.
22
+ SERVICE_CNG_PREFIXES = %w[0300 0303 0500 0800 0900].freeze
23
+
24
+ # 3-digit public-utility numbers designated by Anatel (não exaustivo).
25
+ SERVICE_SHORT_CODES = %w[
26
+ 100 101 102 104 105 106 107 108 110 111 116 118 119 120 121 122 123
27
+ 125 126 127 128 129 130 131 132 133 135 136 137 138 140 141 144 145
28
+ 146 147 148 150 151 152 153 154 155 156 158 159 160 161 162 163 164
29
+ 171 172 173 174 175 176 177 178 179 180 181 185 188 189 190 191 192
30
+ 193 194 195 196 197 198 199
31
+ ].freeze
32
+
33
+ # Removes a leading country code (`+55`, `0055` or a bare `55`) from an
34
+ # already digits-only string, but only when doing so leaves 10 or 11
35
+ # digits (so a DDD of `55`, e.g. Rio Grande do Sul, is not mistaken for
36
+ # the country code).
37
+ #
38
+ # @param digits [String] A digits-only phone number.
39
+ # @return [String] The digits, with the country code removed if applicable.
40
+ #
41
+ # @private
42
+ def self.strip_country_code(digits)
43
+ if digits.start_with?('0055') && [10, 11].include?(digits.length - 4)
44
+ digits[4..-1]
45
+ elsif digits.start_with?('55') && [10, 11].include?(digits.length - 2)
46
+ digits[2..-1]
47
+ else
48
+ digits
49
+ end
50
+ end
51
+
52
+ private_class_method :strip_country_code
53
+
54
+ # Removes phone formatting and keeps only digits, capped to 11 digits.
55
+ #
56
+ # A country code (`+55`, `0055` or a bare `55`) is stripped only when 10
57
+ # or 11 digits are left, so an area code of `55` is not mistaken for it.
58
+ #
59
+ # @param value [String, Integer] The value to parse.
60
+ # @return [String] The parsed digits.
61
+ #
62
+ # @example
63
+ # parse("(11) 98888-7777") #=> "11988887777"
64
+ # parse("+55 11 98888-7777") #=> "11988887777"
65
+ # parse("55988887777") #=> "55988887777" (55 read as DDD)
66
+ def self.parse(value)
67
+ return '' unless value.is_a?(String) || value.is_a?(Integer)
68
+
69
+ digits = value.to_s.gsub(/\D/, '')
70
+ return '' if digits.empty?
71
+
72
+ strip_country_code(digits)[0, 11]
73
+ end
74
+
75
+ # Formats a Brazilian phone number.
76
+ #
77
+ # Without `options`, formats as the subscriber number only (`sn` mask,
78
+ # no DDD): e.g. `"988887777"` becomes `"98888-7777"`. Other masks:
79
+ # `:ddd` (`(11) 99402-9275`), `:e164` (`+5511994029275`),
80
+ # `:international` (`+55 11 99402-9275`) and `:service`
81
+ # (`0800 123 4567`).
82
+ #
83
+ # @param phone [String, Integer] A phone number, with or without formatting.
84
+ # @param options [Hash] `:mask` picks the mask (default `:sn`).
85
+ #
86
+ # @return [String] The formatted phone number, or an empty string when
87
+ # there is nothing to format.
88
+ #
89
+ # @example
90
+ # format_phone("988887777") #=> "98888-7777"
91
+ # format_phone("1130000000") #=> "11300-0000"
92
+ # format_phone("11994029275", mask: :ddd) #=> "(11) 99402-9275"
93
+ def self.format_phone(phone, options = {})
94
+ return '' unless phone.is_a?(String) || phone.is_a?(Integer)
95
+
96
+ digits = phone.to_s.gsub(/\D/, '')
97
+ return '' if digits.empty?
98
+
99
+ mask = (options[:mask] || options['mask'] || :sn).to_s
100
+
101
+ case mask
102
+ when 'ddd'
103
+ format_ddd_mask(digits)
104
+ when 'e164'
105
+ format_e164_mask(digits)
106
+ when 'international'
107
+ format_international_mask(digits)
108
+ when 'service'
109
+ format_service_mask(digits)
110
+ else
111
+ format_subscriber_number_mask(digits)
112
+ end
113
+ end
114
+
115
+ # Alias for format_phone
116
+ class << self
117
+ alias format format_phone
118
+ end
119
+
120
+ # @private
121
+ def self.format_subscriber_number_mask(digits)
122
+ d = digits[0, [digits.length, 9].min]
123
+ return d if d.length <= 5
124
+
125
+ "#{d[0, 5]}-#{d[5..-1]}"
126
+ end
127
+
128
+ private_class_method :format_subscriber_number_mask
129
+
130
+ # @private
131
+ def self.format_ddd_mask(digits)
132
+ d = strip_country_code(digits)
133
+ return '' unless d.length == 10 || d.length == 11
134
+
135
+ ddd = d[0, 2]
136
+ subscriber = d[2..-1]
137
+ "(#{ddd}) #{subscriber[0..-5]}-#{subscriber[-4..-1]}"
138
+ end
139
+
140
+ private_class_method :format_ddd_mask
141
+
142
+ # @private
143
+ def self.format_e164_mask(digits)
144
+ d = strip_country_code(digits)
145
+ return '' unless d.length == 10 || d.length == 11
146
+
147
+ "+55#{d}"
148
+ end
149
+
150
+ private_class_method :format_e164_mask
151
+
152
+ # @private
153
+ def self.format_international_mask(digits)
154
+ d = strip_country_code(digits)
155
+ return '' unless d.length == 10 || d.length == 11
156
+
157
+ ddd = d[0, 2]
158
+ subscriber = d[2..-1]
159
+ "+55 #{ddd} #{subscriber[0..-5]}-#{subscriber[-4..-1]}"
160
+ end
161
+
162
+ private_class_method :format_international_mask
163
+
164
+ # @private
165
+ def self.format_service_mask(digits)
166
+ if digits.length == 11 && SERVICE_CNG_PREFIXES.include?(digits[0, 4])
167
+ "#{digits[0, 4]} #{digits[4, 3]} #{digits[7, 4]}"
168
+ elsif digits.length == 8
169
+ "#{digits[0, 4]}-#{digits[4, 4]}"
170
+ else
171
+ digits
172
+ end
173
+ end
174
+
175
+ private_class_method :format_service_mask
176
+
177
+ # Returns if a Brazilian phone number is valid (mobile or landline).
178
+ #
179
+ # A country code (`+55`, `0055` or a bare `55`) is accepted and removed
180
+ # first, as in {parse}.
181
+ #
182
+ # @param phone_number [String] The phone number to validate.
183
+ # @param type [Symbol, String, Hash, nil] :mobile, :landline, "mobile",
184
+ # "landline", or a Hash of options (`:type`, `:mobile_version`).
185
+ # If not specified, checks for either type.
186
+ #
187
+ # @return [Boolean] True if the phone number is valid, false otherwise
188
+ #
189
+ # @example
190
+ # is_valid("11994029275") #=> true (mobile)
191
+ # is_valid("1635014415") #=> true (landline)
192
+ # is_valid("+5511994029275") #=> true (country code stripped first)
193
+ def self.is_valid(phone_number, type = nil)
194
+ return false unless phone_number.is_a?(String)
195
+
196
+ options = type.is_a?(Hash) ? type : { type: type }
197
+ type_str = options[:type] ? options[:type].to_s : nil
198
+ mobile_version = options[:mobile_version] || 1
199
+
200
+ digits = phone_number.to_s.gsub(/\D/, '')
201
+ return false if digits.empty?
202
+
203
+ value = strip_country_code(digits)
204
+
205
+ case type_str
206
+ when 'mobile'
207
+ mobile_number_matches?(value, mobile_version)
208
+ when 'landline'
209
+ landline_number_matches?(value)
210
+ when 'service'
211
+ service_number_matches?(value)
212
+ else
213
+ mobile_number_matches?(value, mobile_version) || landline_number_matches?(value)
214
+ end
215
+ end
216
+
217
+ # Alias for is_valid
218
+ class << self
219
+ alias valid? is_valid
220
+ end
221
+
222
+ # Validates if a phone number is a valid Brazilian mobile phone (DDD +
223
+ # 9 digits). A country code is accepted and removed first, as in {parse}.
224
+ #
225
+ # @param value [String] The phone number to validate.
226
+ # @param options [Hash] `:version` 1 (default, subscriber digit 6-9) or
227
+ # 2 (Resolução Anatel nº 749/2022: subscriber digit 7-9, no 700 series).
228
+ # @return [Boolean]
229
+ def self.is_valid_mobile(value, options = {})
230
+ return false unless value.is_a?(String)
231
+
232
+ digits = value.to_s.gsub(/\D/, '')
233
+ return false if digits.empty?
234
+
235
+ version = options[:version] || options['version'] || 1
236
+ mobile_number_matches?(strip_country_code(digits), version)
237
+ end
238
+
239
+ class << self
240
+ alias valid_mobile? is_valid_mobile
241
+ end
242
+
243
+ # Validates if a phone number is a valid Brazilian landline phone (DDD +
244
+ # 8 digits). A country code is accepted and removed first, as in {parse}.
245
+ #
246
+ # @param value [String]
247
+ # @return [Boolean]
248
+ def self.is_valid_landline(value)
249
+ return false unless value.is_a?(String)
250
+
251
+ digits = value.to_s.gsub(/\D/, '')
252
+ return false if digits.empty?
253
+
254
+ landline_number_matches?(strip_country_code(digits))
255
+ end
256
+
257
+ class << self
258
+ alias valid_landline? is_valid_landline
259
+ end
260
+
261
+ # Validates if a phone number is a valid Brazilian service number
262
+ # (Código Não Geográfico or a 3-digit public-utility code).
263
+ #
264
+ # @param value [String]
265
+ # @return [Boolean]
266
+ def self.is_valid_service(value)
267
+ return false unless value.is_a?(String)
268
+
269
+ digits = value.to_s.gsub(/\D/, '')
270
+ return false if digits.empty?
271
+
272
+ service_number_matches?(digits)
273
+ end
274
+
275
+ class << self
276
+ alias valid_service? is_valid_service
277
+ end
278
+
279
+ # Removes common symbols from a Brazilian phone number string.
280
+ #
281
+ # Removes: (, ), -, +, and spaces
282
+ #
283
+ # @param phone_number [String] The phone number to remove symbols from
284
+ #
285
+ # @return [String] A new string with the specified symbols removed
286
+ #
287
+ # @example
288
+ # remove_symbols_phone("(11)99402-9275")
289
+ # #=> "11994029275"
290
+ #
291
+ # remove_symbols_phone("+55 11 99402-9275")
292
+ # #=> "5511994029275"
293
+ #
294
+ # remove_symbols_phone("(16) 3501-4415")
295
+ # #=> "1635014415"
296
+ def self.remove_symbols_phone(phone_number)
297
+ return '' unless phone_number.is_a?(String)
298
+
299
+ phone_number.gsub(/[\(\)\-\+\s]/, '')
300
+ end
301
+
302
+ # Alias for remove_symbols_phone
303
+ class << self
304
+ alias remove_symbols remove_symbols_phone
305
+ alias sieve remove_symbols_phone
306
+ end
307
+
308
+ # Generates a valid and random phone number.
309
+ #
310
+ # @param type [Symbol, String, nil] :mobile, :landline, "mobile", or "landline".
311
+ # If not specified, generates either type randomly.
312
+ #
313
+ # @return [String] A randomly generated valid phone number
314
+ #
315
+ # @example
316
+ # generate
317
+ # #=> "2234451215" (random type)
318
+ #
319
+ # generate(:mobile)
320
+ # #=> "11999115895"
321
+ #
322
+ # generate(:landline)
323
+ # #=> "1635317900"
324
+ #
325
+ # generate("mobile")
326
+ # #=> "21987654321"
327
+ def self.generate(type = nil)
328
+ type_str = type.to_s if type
329
+
330
+ case type_str
331
+ when 'mobile'
332
+ generate_mobile_phone
333
+ when 'landline'
334
+ generate_landline_phone
335
+ else
336
+ [method(:generate_mobile_phone), method(:generate_landline_phone)].sample.call
337
+ end
338
+ end
339
+
340
+ # Removes the international dialing code (+55 or 55) from a phone number.
341
+ #
342
+ # Only removes the code if the resulting number has more than 11 digits.
343
+ #
344
+ # @param phone_number [String] The phone number with or without international code
345
+ #
346
+ # @return [String] The phone number without international code, or the same number if no code present
347
+ #
348
+ # @example
349
+ # remove_international_dialing_code("5511994029275")
350
+ # #=> "11994029275"
351
+ #
352
+ # remove_international_dialing_code("+5511994029275")
353
+ # #=> "11994029275"
354
+ #
355
+ # remove_international_dialing_code("1635014415")
356
+ # #=> "1635014415" (no international code)
357
+ #
358
+ # remove_international_dialing_code("+55 11 99402-9275")
359
+ # #=> "+55 11 99402-9275" (has spaces, length check fails)
360
+ def self.remove_international_dialing_code(phone_number)
361
+ return '' unless phone_number.is_a?(String)
362
+
363
+ # Only touch a "clean" digit string (with an optional leading '+') that
364
+ # is longer than 11 digits; anything with spaces/hyphens/etc. is left
365
+ # alone rather than partially stripped.
366
+ digits_part = phone_number.sub(/\A\+/, '')
367
+
368
+ if INTERNATIONAL_CODE_PATTERN.match?(phone_number) &&
369
+ digits_part.match?(/\A\d+\z/) && digits_part.length > 11
370
+ # Anchor to the start so only the leading "+55"/"55" is stripped
371
+ # (a plain #sub would also drop the '+' and could hit an unrelated
372
+ # "55" further into the number, e.g. an RS-state "55" DDD).
373
+ phone_number.sub(/\A\+?55/, '')
374
+ else
375
+ phone_number
376
+ end
377
+ end
378
+
379
+ # Returns if a Brazilian mobile number is valid.
380
+ #
381
+ # Mobile pattern: DDD (2 digits 1-9) + 9 + 8 digits (total 11 digits)
382
+ #
383
+ # @param phone_number [String] The mobile number to validate
384
+ # @param version [Integer] 1 (default) or 2, see {is_valid_mobile}.
385
+ #
386
+ # @return [Boolean] True if valid mobile, false otherwise
387
+ #
388
+ # @private
389
+ def self.mobile_number_matches?(phone_number, version = 1)
390
+ return false unless phone_number.is_a?(String)
391
+ return false unless MOBILE_PATTERN.match?(phone_number.strip)
392
+
393
+ subscriber_first_digit = phone_number.strip[3]
394
+
395
+ case version.to_i
396
+ when 2
397
+ return false unless %w[7 8 9].include?(subscriber_first_digit)
398
+ return false if phone_number.strip[3, 3] == '700'
399
+
400
+ true
401
+ else
402
+ true
403
+ end
404
+ end
405
+
406
+ private_class_method :mobile_number_matches?
407
+
408
+ # Returns if a Brazilian landline number is valid.
409
+ #
410
+ # Landline pattern: DDD (2 digits 1-9) + [2-5] + 7 digits (total 10 digits)
411
+ #
412
+ # @param phone_number [String] The landline number to validate
413
+ #
414
+ # @return [Boolean] True if valid landline, false otherwise
415
+ #
416
+ # @private
417
+ def self.landline_number_matches?(phone_number)
418
+ return false unless phone_number.is_a?(String)
419
+
420
+ LANDLINE_PATTERN.match?(phone_number.strip)
421
+ end
422
+
423
+ private_class_method :landline_number_matches?
424
+
425
+ # Returns if a value is a valid Brazilian service number.
426
+ #
427
+ # @param value [String] Digits-only phone number.
428
+ # @return [Boolean]
429
+ #
430
+ # @private
431
+ def self.service_number_matches?(value)
432
+ return false unless value.is_a?(String)
433
+
434
+ v = value.strip
435
+
436
+ return true if v.length == 11 && SERVICE_CNG_PREFIXES.include?(v[0, 4])
437
+ return true if v.length == 8 && %w[300 400].include?(v[0, 3])
438
+ return true if v.length == 3 && SERVICE_SHORT_CODES.include?(v)
439
+
440
+ false
441
+ end
442
+
443
+ private_class_method :service_number_matches?
444
+
445
+ # Generates a valid DDD (area code) number.
446
+ #
447
+ # DDD consists of 2 digits, both ranging from 1-9.
448
+ #
449
+ # @return [String] A 2-digit DDD number
450
+ #
451
+ # @private
452
+ def self.generate_ddd_number
453
+ 2.times.map { rand(1..9) }.join
454
+ end
455
+
456
+ private_class_method :generate_ddd_number
457
+
458
+ # Generates a valid and random mobile phone number.
459
+ #
460
+ # Format: DDD + 9 + 8 random digits (total 11 digits)
461
+ #
462
+ # @return [String] A valid mobile phone number
463
+ #
464
+ # @private
465
+ def self.generate_mobile_phone
466
+ ddd = generate_ddd_number
467
+ client_number = 8.times.map { rand(0..9) }.join
468
+
469
+ "#{ddd}9#{client_number}"
470
+ end
471
+
472
+ private_class_method :generate_mobile_phone
473
+
474
+ # Generates a valid and random landline phone number.
475
+ #
476
+ # Format: DDD + [2-5] + 7 random digits (total 10 digits)
477
+ #
478
+ # @return [String] A valid landline phone number
479
+ #
480
+ # @private
481
+ def self.generate_landline_phone
482
+ ddd = generate_ddd_number
483
+ first_digit = rand(2..5)
484
+ remaining_digits = rand(0..9999999).to_s.rjust(7, '0')
485
+
486
+ "#{ddd}#{first_digit}#{remaining_digits}"
487
+ end
488
+
489
+ private_class_method :generate_landline_phone
490
+ end
491
+ end