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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +120 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +5 -0
  5. data/Gemfile +3 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +348 -0
  8. data/Rakefile +6 -0
  9. data/examples/boleto_usage_example.rb +79 -0
  10. data/examples/cep_usage_example.rb +148 -0
  11. data/examples/cnh_usage_example.rb +120 -0
  12. data/examples/cnpj_usage_example.rb +227 -0
  13. data/examples/cpf_usage_example.rb +237 -0
  14. data/examples/currency_usage_example.rb +266 -0
  15. data/examples/date_usage_example.rb +259 -0
  16. data/examples/email_usage_example.rb +321 -0
  17. data/examples/legal_nature_usage_example.rb +437 -0
  18. data/examples/legal_process_usage_example.rb +444 -0
  19. data/examples/license_plate_usage_example.rb +440 -0
  20. data/examples/phone_usage_example.rb +595 -0
  21. data/examples/pis_usage_example.rb +588 -0
  22. data/examples/renavam_usage_example.rb +499 -0
  23. data/examples/voter_id_usage_example.rb +573 -0
  24. data/lib/brazilian-utils/boleto-utils.rb +176 -0
  25. data/lib/brazilian-utils/cep-utils.rb +330 -0
  26. data/lib/brazilian-utils/cnh-utils.rb +88 -0
  27. data/lib/brazilian-utils/cnpj-utils.rb +202 -0
  28. data/lib/brazilian-utils/cpf-utils.rb +192 -0
  29. data/lib/brazilian-utils/currency-utils.rb +226 -0
  30. data/lib/brazilian-utils/data/legal_process_ids.json +38 -0
  31. data/lib/brazilian-utils/date-utils.rb +244 -0
  32. data/lib/brazilian-utils/email-utils.rb +54 -0
  33. data/lib/brazilian-utils/legal-nature-utils.rb +235 -0
  34. data/lib/brazilian-utils/legal-process-utils.rb +240 -0
  35. data/lib/brazilian-utils/license-plate-utils.rb +279 -0
  36. data/lib/brazilian-utils/phone-utils.rb +272 -0
  37. data/lib/brazilian-utils/pis-utils.rb +151 -0
  38. data/lib/brazilian-utils/renavam-utils.rb +113 -0
  39. data/lib/brazilian-utils/voter-id-utils.rb +165 -0
  40. metadata +123 -0
@@ -0,0 +1,240 @@
1
+ require 'json'
2
+
3
+ module BrazilianUtils
4
+ # Utilities for formatting, validating, and generating Brazilian Legal Process IDs.
5
+ #
6
+ # A legal process ID (Número de Processo Judicial) is a 20-digit code that identifies
7
+ # a legal case in the Brazilian judiciary system. The format is:
8
+ # NNNNNNN-DD.AAAA.J.TR.OOOO
9
+ #
10
+ # Where:
11
+ # - NNNNNNN: Sequential number (7 digits)
12
+ # - DD: Verification digits (2 digits) - checksum
13
+ # - AAAA: Year the process was filed (4 digits)
14
+ # - J: Judicial segment (1 digit) - Orgão
15
+ # - TR: Court (2 digits) - Tribunal
16
+ # - OOOO: Court of origin (4 digits) - Foro
17
+ #
18
+ # This module does not verify if a legal process ID corresponds to a real case;
19
+ # it only validates the format and structure of the ID.
20
+ module LegalProcessUtils
21
+ # Path to the JSON file containing valid tribunal and foro IDs
22
+ DATA_FILE = File.join(File.dirname(__FILE__), 'data', 'legal_process_ids.json')
23
+
24
+ # Removes specific symbols (dots and hyphens) from a legal process ID.
25
+ #
26
+ # This function takes a legal process ID as input and removes all occurrences
27
+ # of the '.' and '-' characters from it.
28
+ #
29
+ # @param legal_process [String] A legal process ID containing symbols to be removed
30
+ #
31
+ # @return [String] The legal process ID string with the specified symbols removed
32
+ #
33
+ # @example
34
+ # remove_symbols("123.45-678.901.234-56.7890")
35
+ # #=> "12345678901234567890"
36
+ #
37
+ # remove_symbols("9876543-21.0987.6.54.3210")
38
+ # #=> "98765432109876543210"
39
+ #
40
+ # remove_symbols("1234567890123456789012345")
41
+ # #=> "1234567890123456789012345"
42
+ def self.remove_symbols(legal_process)
43
+ return '' unless legal_process.is_a?(String)
44
+
45
+ legal_process.gsub('.', '').gsub('-', '')
46
+ end
47
+
48
+ # Formats a legal process ID into the standard format.
49
+ #
50
+ # Takes a 20-digit string and formats it as: NNNNNNN-DD.AAAA.J.TR.OOOO
51
+ #
52
+ # @param legal_process_id [String] A 20-digit string representing the legal process ID
53
+ #
54
+ # @return [String, nil] The formatted legal process ID, or nil if the input is invalid
55
+ #
56
+ # @example
57
+ # format_legal_process("12345678901234567890")
58
+ # #=> "1234567-89.0123.4.56.7890"
59
+ #
60
+ # format_legal_process("98765432109876543210")
61
+ # #=> "9876543-21.0987.6.54.3210"
62
+ #
63
+ # format_legal_process("123")
64
+ # #=> nil
65
+ def self.format_legal_process(legal_process_id)
66
+ return nil unless legal_process_id.is_a?(String)
67
+ return nil unless legal_process_id =~ /^\d{20}$/
68
+
69
+ # Extract fields: NNNNNNN DD AAAA J TR OOOO
70
+ nnnnnnn = legal_process_id[0, 7]
71
+ dd = legal_process_id[7, 2]
72
+ aaaa = legal_process_id[9, 4]
73
+ j = legal_process_id[13, 1]
74
+ tr = legal_process_id[14, 2]
75
+ oooo = legal_process_id[16, 4]
76
+
77
+ "#{nnnnnnn}-#{dd}.#{aaaa}.#{j}.#{tr}.#{oooo}"
78
+ end
79
+
80
+ # Checks if a legal process ID is valid.
81
+ #
82
+ # This function validates:
83
+ # 1. The format (20 digits)
84
+ # 2. The checksum (DD verification digits)
85
+ # 3. The tribunal (TR) and foro (OOOO) combination against the official table
86
+ #
87
+ # This function does not verify if the legal process ID corresponds to a real case;
88
+ # it only validates the format and structure of the ID.
89
+ #
90
+ # @param legal_process_id [String] A digit-only or formatted string representing
91
+ # the legal process ID
92
+ #
93
+ # @return [Boolean] Returns true if the legal process ID is valid, false otherwise
94
+ #
95
+ # @example
96
+ # is_valid("68476506020233030000")
97
+ # #=> true
98
+ #
99
+ # is_valid("5180823-36.2023.3.03.0000")
100
+ # #=> true
101
+ #
102
+ # is_valid("123")
103
+ # #=> false
104
+ def self.is_valid(legal_process_id)
105
+ return false unless legal_process_id.is_a?(String)
106
+
107
+ clean_id = remove_symbols(legal_process_id)
108
+ return false unless clean_id =~ /^\d{20}$/
109
+
110
+ # Extract fields
111
+ nnnnnnn = clean_id[0, 7]
112
+ dd = clean_id[7, 2]
113
+ aaaa = clean_id[9, 4]
114
+ j = clean_id[13, 1]
115
+ tr = clean_id[14, 2]
116
+ oooo = clean_id[16, 4]
117
+
118
+ # Validate checksum
119
+ base_for_checksum = nnnnnnn + aaaa + j + tr + oooo
120
+ expected_dd = checksum(base_for_checksum.to_i)
121
+ return false unless dd == expected_dd
122
+
123
+ # Validate tribunal and foro against JSON data
124
+ validate_tribunal_and_foro(j.to_i, tr.to_i, oooo.to_i)
125
+ end
126
+
127
+ # Alias for is_valid to provide Ruby-style naming
128
+ class << self
129
+ alias valid? is_valid
130
+ end
131
+
132
+ # Generates a random legal process ID.
133
+ #
134
+ # @param year [Integer] The year for the legal process ID (default is current year).
135
+ # The year should not be in the past.
136
+ # @param orgao [Integer] The judicial segment code (1-9) for the legal process ID
137
+ # (default is random)
138
+ #
139
+ # @return [String, nil] A randomly generated legal process ID (20 digits),
140
+ # or nil if arguments are invalid
141
+ #
142
+ # @example
143
+ # generate(2023, 5)
144
+ # #=> "51659517020235080562" (example, actual value is random)
145
+ #
146
+ # generate()
147
+ # #=> "88031888120233030000" (uses current year and random orgao)
148
+ #
149
+ # generate(2022, 10)
150
+ # #=> nil (year in the past, orgao out of range)
151
+ def self.generate(year = Time.now.year, orgao = rand(1..9))
152
+ return nil if year < Time.now.year
153
+ return nil unless (1..9).include?(orgao)
154
+
155
+ data = load_legal_process_data
156
+ return nil unless data
157
+
158
+ orgao_data = data["orgao_#{orgao}"]
159
+ return nil unless orgao_data
160
+
161
+ # Get random tribunal and foro
162
+ tribunals = orgao_data['id_tribunal']
163
+ foros = orgao_data['id_foro']
164
+
165
+ tr = tribunals[rand(tribunals.length)].to_s.rjust(2, '0')
166
+ oooo = foros[rand(foros.length)].to_s.rjust(4, '0')
167
+
168
+ # Generate random sequential number
169
+ nnnnnnn = rand(0..9999999).to_s.rjust(7, '0')
170
+
171
+ # Calculate checksum
172
+ base_for_checksum = nnnnnnn + year.to_s + orgao.to_s + tr + oooo
173
+ dd = checksum(base_for_checksum.to_i)
174
+
175
+ "#{nnnnnnn}#{dd}#{year}#{orgao}#{tr}#{oooo}"
176
+ end
177
+
178
+ # Calculates the checksum (verification digits) for a legal process ID.
179
+ #
180
+ # The checksum is calculated as: 97 - ((basenum * 100) % 97), padded to 2 digits.
181
+ #
182
+ # @param basenum [Integer] The base number for checksum calculation
183
+ # (without the verification digits)
184
+ #
185
+ # @return [String] The checksum value as a 2-digit string
186
+ #
187
+ # @private
188
+ def self.checksum(basenum)
189
+ result = 97 - ((basenum * 100) % 97)
190
+ result.to_s.rjust(2, '0')
191
+ end
192
+
193
+ private_class_method :checksum
194
+
195
+ # Validates if a tribunal and foro combination is valid for a given orgao.
196
+ #
197
+ # @param orgao [Integer] The judicial segment (1-9)
198
+ # @param tribunal [Integer] The court number
199
+ # @param foro [Integer] The court of origin number
200
+ #
201
+ # @return [Boolean] True if the combination is valid, false otherwise
202
+ #
203
+ # @private
204
+ def self.validate_tribunal_and_foro(orgao, tribunal, foro)
205
+ return false unless (1..9).include?(orgao)
206
+
207
+ data = load_legal_process_data
208
+ return false unless data
209
+
210
+ orgao_data = data["orgao_#{orgao}"]
211
+ return false unless orgao_data
212
+
213
+ tribunals = orgao_data['id_tribunal']
214
+ foros = orgao_data['id_foro']
215
+
216
+ tribunals.include?(tribunal) && foros.include?(foro)
217
+ end
218
+
219
+ private_class_method :validate_tribunal_and_foro
220
+
221
+ # Loads the legal process data from the JSON file.
222
+ #
223
+ # @return [Hash, nil] The parsed JSON data, or nil if the file cannot be loaded
224
+ #
225
+ # @private
226
+ def self.load_legal_process_data
227
+ return @legal_process_data if @legal_process_data
228
+
229
+ if File.exist?(DATA_FILE)
230
+ @legal_process_data = JSON.parse(File.read(DATA_FILE))
231
+ else
232
+ nil
233
+ end
234
+ rescue JSON::ParserError, Errno::ENOENT
235
+ nil
236
+ end
237
+
238
+ private_class_method :load_legal_process_data
239
+ end
240
+ end
@@ -0,0 +1,279 @@
1
+ module BrazilianUtils
2
+ # Utilities for formatting, validating, and generating Brazilian license plates.
3
+ #
4
+ # Brazilian license plates come in two formats:
5
+ # - Old format: LLLNNNN (3 letters + 4 numbers) e.g., "ABC-1234"
6
+ # - Mercosul format: LLLNLNN (3 letters + 1 number + 1 letter + 2 numbers) e.g., "ABC1D23"
7
+ #
8
+ # The Mercosul format was introduced in 2018 as part of a standardization
9
+ # effort across Mercosul countries.
10
+ module LicensePlateUtils
11
+ # Pattern for old format license plates (LLLNNNN)
12
+ OLD_FORMAT_PATTERN = /^[A-Za-z]{3}[0-9]{4}$/.freeze
13
+
14
+ # Pattern for Mercosul format license plates (LLLNLNN)
15
+ MERCOSUL_PATTERN = /^[A-Z]{3}\d[A-Z]\d{2}$/.freeze
16
+
17
+ # Converts an old pattern license plate (LLLNNNN) to Mercosul format (LLLNLNN).
18
+ #
19
+ # The conversion replaces the first digit (position 4) with its corresponding letter
20
+ # (0→A, 1→B, 2→C, ..., 9→J).
21
+ #
22
+ # @param license_plate [String] A string representing the old pattern license plate
23
+ #
24
+ # @return [String, nil] The converted Mercosul license plate (LLLNLNN) or nil if invalid
25
+ #
26
+ # @example
27
+ # convert_to_mercosul("ABC1234")
28
+ # #=> "ABC1C34"
29
+ #
30
+ # convert_to_mercosul("ABC4567")
31
+ # #=> "ABC4F67"
32
+ #
33
+ # convert_to_mercosul("ABC-1234")
34
+ # #=> "ABC1C34"
35
+ #
36
+ # convert_to_mercosul("ABC4*67")
37
+ # #=> nil
38
+ def self.convert_to_mercosul(license_plate)
39
+ return nil unless valid_old_format?(license_plate)
40
+
41
+ clean = remove_symbols(license_plate).upcase
42
+ chars = clean.chars
43
+
44
+ # Convert the 5th character (index 4) - the first digit after the letters
45
+ # 0→A, 1→B, 2→C, etc.
46
+ chars[4] = ('A'.ord + chars[4].to_i).chr
47
+
48
+ chars.join
49
+ end
50
+
51
+ # Formats a license plate into the correct pattern.
52
+ #
53
+ # This function receives a license plate in any pattern (LLLNNNN or LLLNLNN)
54
+ # and returns a formatted version:
55
+ # - Old format: adds dash (ABC-1234)
56
+ # - Mercosul format: uppercase without dash (ABC1D34)
57
+ #
58
+ # @param license_plate [String] A license plate string
59
+ #
60
+ # @return [String, nil] The formatted license plate string or nil if invalid
61
+ #
62
+ # @example
63
+ # format_license_plate("ABC1234")
64
+ # #=> "ABC-1234" (old format with dash)
65
+ #
66
+ # format_license_plate("abc1e34")
67
+ # #=> "ABC1E34" (Mercosul format, uppercase)
68
+ #
69
+ # format_license_plate("ABC-1234")
70
+ # #=> "ABC-1234" (already formatted old format)
71
+ #
72
+ # format_license_plate("ABC123")
73
+ # #=> nil (invalid)
74
+ def self.format_license_plate(license_plate)
75
+ return nil unless license_plate.is_a?(String)
76
+
77
+ clean = remove_symbols(license_plate).upcase
78
+
79
+ if valid_old_format?(clean)
80
+ # Old format: add dash after 3rd character
81
+ "#{clean[0..2]}-#{clean[3..-1]}"
82
+ elsif valid_mercosul?(clean)
83
+ # Mercosul format: just uppercase, no dash
84
+ clean
85
+ else
86
+ nil
87
+ end
88
+ end
89
+
90
+ # Alias for format_license_plate
91
+ class << self
92
+ alias format format_license_plate
93
+ end
94
+
95
+ # Returns if a Brazilian license plate number is valid.
96
+ #
97
+ # It does not verify if the plate actually exists, only validates the format.
98
+ #
99
+ # @param license_plate [String] The license plate number to be validated
100
+ # @param type [Symbol, String, nil] :old_format, :mercosul, "old_format", or "mercosul".
101
+ # If not specified, checks for either format.
102
+ #
103
+ # @return [Boolean] True if the plate number is valid, false otherwise
104
+ #
105
+ # @example
106
+ # is_valid("ABC1234")
107
+ # #=> true (old format)
108
+ #
109
+ # is_valid("ABC1D34")
110
+ # #=> true (Mercosul format)
111
+ #
112
+ # is_valid("ABC-1234")
113
+ # #=> true (old format with dash)
114
+ #
115
+ # is_valid("ABC1234", :old_format)
116
+ # #=> true
117
+ #
118
+ # is_valid("ABC1D34", :old_format)
119
+ # #=> false
120
+ #
121
+ # is_valid("ABC1D34", :mercosul)
122
+ # #=> true
123
+ #
124
+ # is_valid("ABCD123")
125
+ # #=> false
126
+ def self.is_valid(license_plate, type = nil)
127
+ return false unless license_plate.is_a?(String)
128
+
129
+ clean = remove_symbols(license_plate)
130
+
131
+ type_str = type.to_s if type
132
+
133
+ case type_str
134
+ when 'old_format'
135
+ valid_old_format?(clean)
136
+ when 'mercosul'
137
+ valid_mercosul?(clean)
138
+ else
139
+ valid_old_format?(clean) || valid_mercosul?(clean)
140
+ end
141
+ end
142
+
143
+ # Alias for is_valid
144
+ class << self
145
+ alias valid? is_valid
146
+ end
147
+
148
+ # Removes the dash (-) symbol from a license plate string.
149
+ #
150
+ # @param license_plate_number [String] A license plate number containing symbols
151
+ #
152
+ # @return [String] The license plate number with dashes removed
153
+ #
154
+ # @example
155
+ # remove_symbols("ABC-1234")
156
+ # #=> "ABC1234"
157
+ #
158
+ # remove_symbols("abc123")
159
+ # #=> "abc123"
160
+ #
161
+ # remove_symbols("ABCD123")
162
+ # #=> "ABCD123"
163
+ def self.remove_symbols(license_plate_number)
164
+ return '' unless license_plate_number.is_a?(String)
165
+
166
+ license_plate_number.gsub('-', '')
167
+ end
168
+
169
+ # Returns the format of a license plate.
170
+ #
171
+ # Returns 'LLLNNNN' for the old pattern and 'LLLNLNN' for the Mercosul one.
172
+ #
173
+ # @param license_plate [String] A license plate string without symbols
174
+ #
175
+ # @return [String, nil] The format of the license plate (LLLNNNN, LLLNLNN) or nil if invalid
176
+ #
177
+ # @example
178
+ # get_format("ABC1234")
179
+ # #=> "LLLNNNN"
180
+ #
181
+ # get_format("abc1d23")
182
+ # #=> "LLLNLNN"
183
+ #
184
+ # get_format("ABC-1234")
185
+ # #=> "LLLNNNN" (dash is removed automatically)
186
+ #
187
+ # get_format("ABCD123")
188
+ # #=> nil
189
+ def self.get_format(license_plate)
190
+ return nil unless license_plate.is_a?(String)
191
+
192
+ clean = remove_symbols(license_plate)
193
+
194
+ if valid_old_format?(clean)
195
+ 'LLLNNNN'
196
+ elsif valid_mercosul?(clean)
197
+ 'LLLNLNN'
198
+ else
199
+ nil
200
+ end
201
+ end
202
+
203
+ # Generates a valid license plate in the given format.
204
+ #
205
+ # In case no format is provided, it will return a license plate in the Mercosul format.
206
+ #
207
+ # @param format [String] The desired format for the license plate.
208
+ # 'LLLNNNN' for the old pattern or 'LLLNLNN' for the Mercosul one.
209
+ # Default is 'LLLNLNN'
210
+ #
211
+ # @return [String, nil] A randomly generated license plate number or nil if format is invalid
212
+ #
213
+ # @example
214
+ # generate
215
+ # #=> "ABC1D23" (Mercosul format by default)
216
+ #
217
+ # generate('LLLNLNN')
218
+ # #=> "XYZ2E45" (Mercosul format)
219
+ #
220
+ # generate('LLLNNNN')
221
+ # #=> "ABC1234" (old format)
222
+ #
223
+ # generate('invalid')
224
+ # #=> nil
225
+ def self.generate(format = 'LLLNLNN')
226
+ format_upper = format.upcase
227
+
228
+ return nil unless ['LLLNLNN', 'LLLNNNN'].include?(format_upper)
229
+
230
+ generated = ''
231
+
232
+ format_upper.each_char do |char|
233
+ if char == 'L'
234
+ # Generate random uppercase letter
235
+ generated += ('A'..'Z').to_a.sample
236
+ else # char == 'N'
237
+ # Generate random digit
238
+ generated += rand(0..9).to_s
239
+ end
240
+ end
241
+
242
+ generated
243
+ end
244
+
245
+ # Checks whether a string matches the old format of Brazilian license plate.
246
+ #
247
+ # Pattern: LLLNNNN (3 letters + 4 numbers)
248
+ #
249
+ # @param license_plate [String] The license plate to validate
250
+ #
251
+ # @return [Boolean] True if the plate matches old format, false otherwise
252
+ #
253
+ # @private
254
+ def self.valid_old_format?(license_plate)
255
+ return false unless license_plate.is_a?(String)
256
+
257
+ OLD_FORMAT_PATTERN.match?(license_plate.strip)
258
+ end
259
+
260
+ private_class_method :valid_old_format?
261
+
262
+ # Checks whether a string matches the Mercosul format of Brazilian license plate.
263
+ #
264
+ # Pattern: LLLNLNN (3 letters + 1 number + 1 letter + 2 numbers)
265
+ #
266
+ # @param license_plate [String] The license plate to validate
267
+ #
268
+ # @return [Boolean] True if the plate matches Mercosul format, false otherwise
269
+ #
270
+ # @private
271
+ def self.valid_mercosul?(license_plate)
272
+ return false unless license_plate.is_a?(String)
273
+
274
+ MERCOSUL_PATTERN.match?(license_plate.upcase.strip)
275
+ end
276
+
277
+ private_class_method :valid_mercosul?
278
+ end
279
+ end