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,244 @@
1
+ require 'date'
2
+
3
+ module BrazilianUtils
4
+ # Brazilian months enumeration
5
+ module Months
6
+ NAMES = {
7
+ 1 => 'janeiro',
8
+ 2 => 'fevereiro',
9
+ 3 => 'março',
10
+ 4 => 'abril',
11
+ 5 => 'maio',
12
+ 6 => 'junho',
13
+ 7 => 'julho',
14
+ 8 => 'agosto',
15
+ 9 => 'setembro',
16
+ 10 => 'outubro',
17
+ 11 => 'novembro',
18
+ 12 => 'dezembro'
19
+ }.freeze
20
+
21
+ def self.name(month_number)
22
+ NAMES[month_number]
23
+ end
24
+ end
25
+
26
+ module DateUtils
27
+ DATE_REGEX = /^\d{2}\/\d{2}\/\d{4}$/.freeze
28
+
29
+ # Brazilian national holidays (fixed dates)
30
+ NATIONAL_HOLIDAYS = {
31
+ [1, 1] => 'Ano Novo',
32
+ [4, 21] => 'Tiradentes',
33
+ [5, 1] => 'Dia do Trabalho',
34
+ [9, 7] => 'Independência do Brasil',
35
+ [10, 12] => 'Nossa Senhora Aparecida',
36
+ [11, 2] => 'Finados',
37
+ [11, 15] => 'Proclamação da República',
38
+ [12, 25] => 'Natal'
39
+ }.freeze
40
+
41
+ # State-specific holidays (fixed dates)
42
+ STATE_HOLIDAYS = {
43
+ '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' },
44
+ 'AL' => { [6, 24] => 'São João', [6, 29] => 'São Pedro', [9, 16] => 'Emancipação Política', [11, 20] => 'Morte de Zumbi dos Palmares' },
45
+ 'AM' => { [9, 5] => 'Elevação do Amazonas à categoria de província' },
46
+ 'AP' => { [3, 19] => 'Dia de São José', [9, 13] => 'Criação do Território Federal' },
47
+ 'BA' => { [7, 2] => 'Independência da Bahia' },
48
+ 'CE' => { [3, 19] => 'São José', [3, 25] => 'Data Magna do Ceará' },
49
+ 'DF' => { [4, 21] => 'Fundação de Brasília', [11, 30] => 'Dia do Evangélico' },
50
+ 'ES' => { [4, 21] => 'Nossa Senhora da Penha' },
51
+ 'GO' => { [10, 24] => 'Pedra fundamental de Goiânia' },
52
+ 'MA' => { [7, 28] => 'Adesão do Maranhão à independência do Brasil' },
53
+ 'MG' => { [4, 21] => 'Data Magna de Minas Gerais' },
54
+ 'MS' => { [10, 11] => 'Criação do estado' },
55
+ 'MT' => { [11, 20] => 'Dia da Consciência Negra' },
56
+ 'PA' => { [8, 15] => 'Adesão do Grão-Pará à independência do Brasil' },
57
+ 'PB' => { [7, 26] => 'Homenagem à memória do ex-presidente João Pessoa', [8, 5] => 'Fundação do Estado em 1585' },
58
+ 'PE' => { [3, 6] => 'Revolução Pernambucana de 1817', [6, 24] => 'São João' },
59
+ 'PI' => { [10, 19] => 'Dia do Piauí' },
60
+ 'PR' => { [12, 19] => 'Emancipação política do Paraná' },
61
+ 'RJ' => { [4, 23] => 'Dia de São Jorge', [11, 20] => 'Dia da Consciência Negra' },
62
+ 'RN' => { [6, 29] => 'Dia de São Pedro', [10, 3] => 'Mártires de Cunhaú e Uruaçu' },
63
+ 'RO' => { [1, 4] => 'Criação do estado', [6, 18] => 'Dia do Evangélico' },
64
+ 'RR' => { [10, 5] => 'Criação de Roraima' },
65
+ 'RS' => { [9, 20] => 'Revolução Farroupilha' },
66
+ 'SC' => { [8, 11] => 'Criação da capitania, separando-se de SP' },
67
+ 'SE' => { [7, 8] => 'Autonomia política de Sergipe' },
68
+ 'SP' => { [7, 9] => 'Revolução Constitucionalista de 1932' },
69
+ 'TO' => { [10, 5] => 'Criação de Tocantins' }
70
+ }.freeze
71
+
72
+ VALID_UFS = %w[
73
+ AC AL AP AM BA CE DF ES GO MA MT MS MG PA PB PR PE PI RJ RN RS RO RR SC SP SE TO
74
+ ].freeze
75
+
76
+ # Checks if the given date is a national or state holiday in Brazil.
77
+ #
78
+ # This function takes a date as a Date or DateTime object and an optional UF (Unidade Federativa),
79
+ # returning a boolean value indicating whether the date is a holiday or nil if the date or
80
+ # UF are invalid.
81
+ #
82
+ # The method does not handle municipal holidays.
83
+ #
84
+ # @param target_date [Date, DateTime, Time] The date to be checked.
85
+ # @param uf [String, nil] The state abbreviation (UF) to check for state holidays.
86
+ # If not provided, only national holidays will be considered.
87
+ #
88
+ # @return [Boolean, nil] Returns true if the date is a holiday, false if it is not,
89
+ # or nil if the date or UF are invalid.
90
+ #
91
+ # @note This implementation includes fixed national and state holidays.
92
+ # Movable holidays (like Carnival, Easter) are not included in this basic implementation.
93
+ #
94
+ # @example
95
+ # is_holiday(Date.new(2024, 1, 1)) #=> true (New Year)
96
+ # is_holiday(Date.new(2024, 1, 2)) #=> false
97
+ # is_holiday(Date.new(2024, 7, 9), 'SP') #=> true (SP state holiday)
98
+ # is_holiday(Date.new(2024, 12, 25), 'RJ') #=> true (Christmas)
99
+ def self.is_holiday(target_date, uf = nil)
100
+ return nil unless target_date.is_a?(Date) || target_date.is_a?(DateTime) || target_date.is_a?(Time)
101
+
102
+ # Convert to Date if needed
103
+ date = target_date.is_a?(Date) ? target_date : target_date.to_date
104
+
105
+ # Validate UF if provided
106
+ if uf && !VALID_UFS.include?(uf.to_s.upcase)
107
+ return nil
108
+ end
109
+
110
+ month_day = [date.month, date.day]
111
+
112
+ # Check national holidays
113
+ return true if NATIONAL_HOLIDAYS.key?(month_day)
114
+
115
+ # Check state holidays if UF is provided
116
+ if uf
117
+ state_uf = uf.to_s.upcase
118
+ state_holidays = STATE_HOLIDAYS[state_uf]
119
+ return true if state_holidays && state_holidays.key?(month_day)
120
+ end
121
+
122
+ false
123
+ end
124
+
125
+ # Converts a given date in Brazilian format (dd/mm/yyyy) to its textual representation.
126
+ #
127
+ # This function takes a date as a string in the format dd/mm/yyyy and converts it
128
+ # to a string with the date written out in Brazilian Portuguese, including the full
129
+ # month name and the year.
130
+ #
131
+ # @param date [String] The date to be converted into text. Expected format: dd/mm/yyyy.
132
+ #
133
+ # @return [String, nil] A string with the date written out in Brazilian Portuguese,
134
+ # or nil if the date is invalid.
135
+ #
136
+ # @example
137
+ # convert_date_to_text("01/01/2024") #=> "Primeiro de janeiro de dois mil e vinte e quatro"
138
+ # convert_date_to_text("15/03/2024") #=> "Quinze de março de dois mil e vinte e quatro"
139
+ # convert_date_to_text("invalid") #=> nil
140
+ def self.convert_date_to_text(date)
141
+ return nil unless DATE_REGEX.match?(date)
142
+
143
+ begin
144
+ dt = Date.strptime(date, '%d/%m/%Y')
145
+ rescue ArgumentError
146
+ return nil
147
+ end
148
+
149
+ day = dt.day
150
+ month = dt.month
151
+ year = dt.year
152
+
153
+ # Convert day to text (special case for 1st)
154
+ day_str = if day == 1
155
+ 'Primeiro'
156
+ else
157
+ # Reuse number_to_words from CurrencyUtils or implement inline
158
+ number_to_words(day).capitalize
159
+ end
160
+
161
+ month_name = Months.name(month)
162
+ year_str = number_to_words(year)
163
+
164
+ "#{day_str} de #{month_name} de #{year_str}"
165
+ end
166
+
167
+ # Converts a number to its textual representation in Brazilian Portuguese.
168
+ # This is a simplified version focused on dates (days 1-31, years).
169
+ #
170
+ # @param number [Integer] The number to convert
171
+ # @return [String] The textual representation
172
+ #
173
+ # @private
174
+ def self.number_to_words(number)
175
+ return 'zero' if number.zero?
176
+
177
+ ones = %w[zero um dois três quatro cinco seis sete oito nove]
178
+ tens = %w[dez onze doze treze quatorze quinze dezesseis dezessete dezoito dezenove]
179
+ tens_multiples = %w[_ _ vinte trinta quarenta cinquenta sessenta setenta oitenta noventa]
180
+ hundreds = %w[
181
+ _
182
+ cento
183
+ duzentos
184
+ trezentos
185
+ quatrocentos
186
+ quinhentos
187
+ seiscentos
188
+ setecentos
189
+ oitocentos
190
+ novecentos
191
+ ]
192
+
193
+ if number < 10
194
+ return ones[number]
195
+ elsif number < 20
196
+ return tens[number - 10]
197
+ elsif number < 100
198
+ tens_digit = number / 10
199
+ ones_digit = number % 10
200
+ if ones_digit.zero?
201
+ return tens_multiples[tens_digit]
202
+ else
203
+ return "#{tens_multiples[tens_digit]} e #{ones[ones_digit]}"
204
+ end
205
+ elsif number == 100
206
+ return 'cem'
207
+ elsif number < 1000
208
+ hundreds_digit = number / 100
209
+ remainder = number % 100
210
+ if remainder.zero?
211
+ return hundreds[hundreds_digit]
212
+ else
213
+ return "#{hundreds[hundreds_digit]} e #{number_to_words(remainder)}"
214
+ end
215
+ elsif number < 1_000_000
216
+ # For years like 2024
217
+ thousands = number / 1000
218
+ remainder = number % 1000
219
+
220
+ result = []
221
+
222
+ if thousands == 1
223
+ result << 'mil'
224
+ else
225
+ result << "#{number_to_words(thousands)} mil"
226
+ end
227
+
228
+ if remainder > 0
229
+ if remainder < 100
230
+ result << "e #{number_to_words(remainder)}"
231
+ else
232
+ result << number_to_words(remainder)
233
+ end
234
+ end
235
+
236
+ result.join(' ')
237
+ else
238
+ number.to_s
239
+ end
240
+ end
241
+
242
+ private_class_method :number_to_words
243
+ end
244
+ end
@@ -0,0 +1,54 @@
1
+ module BrazilianUtils
2
+ module EmailUtils
3
+ # Email validation pattern based on RFC 5322
4
+ # This pattern validates:
5
+ # - Email must not start with a dot
6
+ # - Local part (before @): alphanumeric, dots, underscores, percent, plus, minus
7
+ # - @ symbol required
8
+ # - Domain part: alphanumeric, dots, hyphens
9
+ # - Dot separator required
10
+ # - TLD: at least 2 alphabetic characters
11
+ EMAIL_PATTERN = /\A(?![.])[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\z/.freeze
12
+
13
+ # Checks if a string corresponds to a valid email address.
14
+ #
15
+ # This function validates email addresses following the specifications defined
16
+ # by RFC 5322, which is the widely accepted standard for email address formats.
17
+ #
18
+ # @param email [String] The input string to be checked
19
+ #
20
+ # @return [Boolean] Returns true if email is a valid email address, false otherwise
21
+ #
22
+ # @example Valid emails
23
+ # is_valid("brutils@brutils.com") #=> true
24
+ # is_valid("user.name@example.com") #=> true
25
+ # is_valid("user+tag@example.co.uk") #=> true
26
+ # is_valid("user_123@test-domain.com") #=> true
27
+ #
28
+ # @example Invalid emails
29
+ # is_valid("invalid-email@brutils") #=> false (no TLD)
30
+ # is_valid(".user@example.com") #=> false (starts with dot)
31
+ # is_valid("user@") #=> false (no domain)
32
+ # is_valid("@example.com") #=> false (no local part)
33
+ # is_valid("user name@example.com") #=> false (space not allowed)
34
+ # is_valid(nil) #=> false (not a string)
35
+ #
36
+ # @note The validation rules generally follow RFC 5322 specifications:
37
+ # - Local part cannot start with a dot
38
+ # - Local part can contain: letters, numbers, dots, underscores, percent, plus, minus
39
+ # - Must have @ symbol
40
+ # - Domain can contain: letters, numbers, dots, hyphens
41
+ # - Must have at least one dot in domain
42
+ # - TLD must be at least 2 characters and only letters
43
+ def self.is_valid(email)
44
+ return false unless email.is_a?(String)
45
+
46
+ EMAIL_PATTERN.match?(email)
47
+ end
48
+
49
+ # Alias for is_valid to provide alternative naming convention
50
+ class << self
51
+ alias valid? is_valid
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,235 @@
1
+ module BrazilianUtils
2
+ # Utilities for consulting and validating the official *Natureza Jurídica* (Legal Nature)
3
+ # codes defined by the Receita Federal do Brasil (RFB).
4
+ #
5
+ # The codes and descriptions in this module are sourced from the official
6
+ # **Tabela de Natureza Jurídica** (RFB), as provided in the document used
7
+ # by the Cadastro Nacional (e.g., FCN).
8
+ #
9
+ # This module offers simple lookups and validation helpers based on the official table.
10
+ # It does not infer the current legal/registration status of any entity.
11
+ #
12
+ # Source: https://www.gov.br/empresas-e-negocios/pt-br/drei/links-e-downloads/arquivos/TABELADENATUREZAJURDICA.pdf
13
+ module LegalNatureUtils
14
+ # Official Legal Nature codes from Receita Federal do Brasil
15
+ # Format: 4-digit code => Description in Portuguese
16
+ LEGAL_NATURE = {
17
+ # 1. ADMINISTRAÇÃO PÚBLICA
18
+ '1015' => 'Órgão Público do Poder Executivo Federal',
19
+ '1023' => 'Órgão Público do Poder Executivo Estadual ou do Distrito Federal',
20
+ '1031' => 'Órgão Público do Poder Executivo Municipal',
21
+ '1040' => 'Órgão Público do Poder Legislativo Federal',
22
+ '1058' => 'Órgão Público do Poder Legislativo Estadual ou do Distrito Federal',
23
+ '1066' => 'Órgão Público do Poder Legislativo Municipal',
24
+ '1074' => 'Órgão Público do Poder Judiciário Federal',
25
+ '1082' => 'Órgão Público do Poder Judiciário Estadual',
26
+ '1104' => 'Autarquia Federal',
27
+ '1112' => 'Autarquia Estadual ou do Distrito Federal',
28
+ '1120' => 'Autarquia Municipal',
29
+ '1139' => 'Fundação Federal',
30
+ '1147' => 'Fundação Estadual ou do Distrito Federal',
31
+ '1155' => 'Fundação Municipal',
32
+ '1163' => 'Órgão Público Autônomo da União',
33
+ '1171' => 'Órgão Público Autônomo Estadual ou do Distrito Federal',
34
+ '1180' => 'Órgão Público Autônomo Municipal',
35
+
36
+ # 2. ENTIDADES EMPRESARIAIS
37
+ '2011' => 'Empresa Pública',
38
+ '2038' => 'Sociedade de Economia Mista',
39
+ '2046' => 'Sociedade Anônima Aberta',
40
+ '2054' => 'Sociedade Anônima Fechada',
41
+ '2062' => 'Sociedade Empresária Limitada',
42
+ '2070' => 'Sociedade Empresária em Nome Coletivo',
43
+ '2089' => 'Sociedade Empresária em Comandita Simples',
44
+ '2097' => 'Sociedade Empresária em Comandita por Ações',
45
+ '2100' => 'Sociedade Mercantil de Capital e Indústria (extinta pelo NCC/2002)',
46
+ '2127' => 'Sociedade Empresária em Conta de Participação',
47
+ '2135' => 'Empresário (Individual)',
48
+ '2143' => 'Cooperativa',
49
+ '2151' => 'Consórcio de Sociedades',
50
+ '2160' => 'Grupo de Sociedades',
51
+ '2178' => 'Estabelecimento, no Brasil, de Sociedade Estrangeira',
52
+ '2194' => 'Estabelecimento, no Brasil, de Empresa Binacional Argentino-Brasileira',
53
+ '2208' => 'Entidade Binacional Itaipu',
54
+ '2216' => 'Empresa Domiciliada no Exterior',
55
+ '2224' => 'Clube/Fundo de Investimento',
56
+ '2232' => 'Sociedade Simples Pura',
57
+ '2240' => 'Sociedade Simples Limitada',
58
+ '2259' => 'Sociedade em Nome Coletivo',
59
+ '2267' => 'Sociedade em Comandita Simples',
60
+ '2275' => 'Sociedade Simples em Conta de Participação',
61
+ '2305' => 'Empresa Individual de Responsabilidade Limitada',
62
+
63
+ # 3. ENTIDADES SEM FINS LUCRATIVOS
64
+ '3034' => 'Serviço Notarial e Registral (Cartório)',
65
+ '3042' => 'Organização Social',
66
+ '3050' => 'Organização da Sociedade Civil de Interesse Público (Oscip)',
67
+ '3069' => 'Outras Formas de Fundações Mantidas com Recursos Privados',
68
+ '3077' => 'Serviço Social Autônomo',
69
+ '3085' => 'Condomínio Edilícios',
70
+ '3093' => 'Unidade Executora (Programa Dinheiro Direto na Escola)',
71
+ '3107' => 'Comissão de Conciliação Prévia',
72
+ '3115' => 'Entidade de Mediação e Arbitragem',
73
+ '3123' => 'Partido Político',
74
+ '3131' => 'Entidade Sindical',
75
+ '3204' => 'Estabelecimento, no Brasil, de Fundação ou Associação Estrangeiras',
76
+ '3212' => 'Fundação ou Associação Domiciliada no Exterior',
77
+ '3999' => 'Outras Formas de Associação',
78
+
79
+ # 4. PESSOAS FÍSICAS
80
+ '4014' => 'Empresa Individual Imobiliária',
81
+ '4022' => 'Segurado Especial',
82
+ '4081' => 'Contribuinte individual',
83
+
84
+ # 5. ORGANIZAÇÕES INTERNACIONAIS E OUTRAS INSTITUIÇÕES EXTRATERRITORIAIS
85
+ '5002' => 'Organização Internacional e Outras Instituições Extraterritoriais'
86
+ }.freeze
87
+
88
+ # Normalizes a legal nature code to 4-digit format.
89
+ # Accepts formats like "2062", "206-2", or any string with exactly 4 digits.
90
+ #
91
+ # @param code [String] The code to normalize
92
+ # @return [String, nil] The normalized 4-digit code, or nil if invalid
93
+ #
94
+ # @private
95
+ def self.normalize(code)
96
+ return nil unless code.is_a?(String)
97
+
98
+ # Extract only digits from the input
99
+ digits = code.strip.gsub(/\D/, '')
100
+
101
+ # Return the digits only if we have exactly 4
102
+ digits.length == 4 ? digits : nil
103
+ end
104
+
105
+ private_class_method :normalize
106
+
107
+ # Checks if a string corresponds to a valid *Natureza Jurídica* code.
108
+ #
109
+ # Validation is based solely on the presence of the code in the official RFB table.
110
+ # It does not verify the current legal status or registration of the entity.
111
+ #
112
+ # @param code [String] The code to be validated. Accepts either "NNNN" or "NNN-N" format.
113
+ #
114
+ # @return [Boolean] Returns true if the normalized code exists in the official table,
115
+ # false otherwise.
116
+ #
117
+ # @example Valid codes
118
+ # is_valid("2062") #=> true (Sociedade Empresária Limitada)
119
+ # is_valid("206-2") #=> true (same, with hyphen)
120
+ # is_valid("1015") #=> true (Órgão Público Federal)
121
+ # is_valid("101-5") #=> true (same, with hyphen)
122
+ #
123
+ # @example Invalid codes
124
+ # is_valid("9999") #=> false (not in official table)
125
+ # is_valid("0000") #=> false (not in official table)
126
+ # is_valid("123") #=> false (wrong length)
127
+ # is_valid("abcd") #=> false (not digits)
128
+ # is_valid(nil) #=> false (not a string)
129
+ def self.is_valid(code)
130
+ normalized = normalize(code)
131
+ return false unless normalized
132
+
133
+ LEGAL_NATURE.key?(normalized)
134
+ end
135
+
136
+ # Alias for is_valid to provide Ruby-style naming
137
+ class << self
138
+ alias valid? is_valid
139
+ end
140
+
141
+ # Retrieves the description of a *Natureza Jurídica* code.
142
+ #
143
+ # @param code [String] The code to look up. Accepts either "NNNN" or "NNN-N" format.
144
+ #
145
+ # @return [String, nil] The full description if the code is valid, otherwise nil.
146
+ #
147
+ # @example Valid lookups
148
+ # get_description("2062")
149
+ # #=> "Sociedade Empresária Limitada"
150
+ #
151
+ # get_description("101-5")
152
+ # #=> "Órgão Público do Poder Executivo Federal"
153
+ #
154
+ # get_description("2305")
155
+ # #=> "Empresa Individual de Responsabilidade Limitada"
156
+ #
157
+ # @example Invalid lookups
158
+ # get_description("0000")
159
+ # #=> nil
160
+ #
161
+ # get_description("invalid")
162
+ # #=> nil
163
+ def self.get_description(code)
164
+ normalized = normalize(code)
165
+ return nil unless normalized
166
+
167
+ LEGAL_NATURE[normalized]
168
+ end
169
+
170
+ # Returns a copy of the full *Natureza Jurídica* table.
171
+ #
172
+ # @return [Hash<String, String>] Mapping from 4-digit codes to descriptions
173
+ #
174
+ # @example
175
+ # all_codes = list_all
176
+ # all_codes["2062"]
177
+ # #=> "Sociedade Empresária Limitada"
178
+ #
179
+ # all_codes.size
180
+ # #=> 64 (total number of codes in the official table)
181
+ def self.list_all
182
+ LEGAL_NATURE.dup
183
+ end
184
+
185
+ # Returns all codes within a specific category.
186
+ #
187
+ # Categories:
188
+ # - 1: Administração Pública (Public Administration)
189
+ # - 2: Entidades Empresariais (Business Entities)
190
+ # - 3: Entidades Sem Fins Lucrativos (Non-Profit Entities)
191
+ # - 4: Pessoas Físicas (Individuals)
192
+ # - 5: Organizações Internacionais (International Organizations)
193
+ #
194
+ # @param category [Integer, String] The category number (1-5)
195
+ #
196
+ # @return [Hash<String, String>] Codes and descriptions for the specified category
197
+ #
198
+ # @example
199
+ # business_entities = list_by_category(2)
200
+ # business_entities.keys
201
+ # #=> ["2011", "2038", "2046", ...]
202
+ #
203
+ # non_profits = list_by_category(3)
204
+ # non_profits["3123"]
205
+ # #=> "Partido Político"
206
+ def self.list_by_category(category)
207
+ category_str = category.to_s
208
+ return {} unless ['1', '2', '3', '4', '5'].include?(category_str)
209
+
210
+ LEGAL_NATURE.select { |code, _| code.start_with?(category_str) }
211
+ end
212
+
213
+ # Returns the category number for a given code.
214
+ #
215
+ # @param code [String] The code to check. Accepts either "NNNN" or "NNN-N" format.
216
+ #
217
+ # @return [Integer, nil] The category number (1-5), or nil if invalid
218
+ #
219
+ # @example
220
+ # get_category("2062")
221
+ # #=> 2 (Entidades Empresariais)
222
+ #
223
+ # get_category("101-5")
224
+ # #=> 1 (Administração Pública)
225
+ #
226
+ # get_category("9999")
227
+ # #=> nil (invalid code)
228
+ def self.get_category(code)
229
+ normalized = normalize(code)
230
+ return nil unless normalized && LEGAL_NATURE.key?(normalized)
231
+
232
+ normalized[0].to_i
233
+ end
234
+ end
235
+ end