br-utils 0.1.1 → 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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +446 -22
  3. data/lib/brazilian-utils/area-code-utils.rb +60 -0
  4. data/lib/brazilian-utils/bank-account-utils.rb +82 -0
  5. data/lib/brazilian-utils/bank-utils.rb +61 -0
  6. data/lib/brazilian-utils/boleto-utils.rb +139 -0
  7. data/lib/brazilian-utils/caepf-utils.rb +91 -0
  8. data/lib/brazilian-utils/cbo-utils.rb +74 -0
  9. data/lib/brazilian-utils/cei-utils.rb +79 -0
  10. data/lib/brazilian-utils/cep-utils.rb +25 -1
  11. data/lib/brazilian-utils/certidao-utils.rb +171 -0
  12. data/lib/brazilian-utils/cfop-utils.rb +74 -0
  13. data/lib/brazilian-utils/cnae-utils.rb +104 -0
  14. data/lib/brazilian-utils/cnh-utils.rb +53 -0
  15. data/lib/brazilian-utils/cno-utils.rb +76 -0
  16. data/lib/brazilian-utils/cnpj-utils.rb +125 -12
  17. data/lib/brazilian-utils/cns-utils.rb +110 -0
  18. data/lib/brazilian-utils/cpf-utils.rb +13 -0
  19. data/lib/brazilian-utils/credit-card-utils.rb +47 -0
  20. data/lib/brazilian-utils/csosn-utils.rb +55 -0
  21. data/lib/brazilian-utils/cst-utils.rb +103 -0
  22. data/lib/brazilian-utils/currency-utils.rb +347 -228
  23. data/lib/brazilian-utils/data/area_codes.json +676 -0
  24. data/lib/brazilian-utils/data/banks.json +357 -0
  25. data/lib/brazilian-utils/data/cbo.json +1 -0
  26. data/lib/brazilian-utils/data/cfop.json +1 -0
  27. data/lib/brazilian-utils/data/cnae.json +1 -0
  28. data/lib/brazilian-utils/data/csosn.json +42 -0
  29. data/lib/brazilian-utils/data/cst.json +294 -0
  30. data/lib/brazilian-utils/data/legal_nature.json +900 -0
  31. data/lib/brazilian-utils/data/municipalities.json +1 -0
  32. data/lib/brazilian-utils/data/ncm.json +1 -0
  33. data/lib/brazilian-utils/data/states.json +218 -0
  34. data/lib/brazilian-utils/date-utils.rb +504 -256
  35. data/lib/brazilian-utils/iban-utils.rb +120 -0
  36. data/lib/brazilian-utils/ie-utils.rb +84 -0
  37. data/lib/brazilian-utils/legal-nature-utils.rb +238 -235
  38. data/lib/brazilian-utils/legal-process-utils.rb +31 -3
  39. data/lib/brazilian-utils/license-plate-utils.rb +21 -5
  40. data/lib/brazilian-utils/municipality-utils.rb +58 -0
  41. data/lib/brazilian-utils/ncm-utils.rb +91 -0
  42. data/lib/brazilian-utils/nfe-key-utils.rb +158 -0
  43. data/lib/brazilian-utils/number-utils.rb +123 -0
  44. data/lib/brazilian-utils/passport-utils.rb +65 -0
  45. data/lib/brazilian-utils/phone-utils.rb +491 -280
  46. data/lib/brazilian-utils/pis-utils.rb +16 -1
  47. data/lib/brazilian-utils/pix-key-utils.rb +70 -0
  48. data/lib/brazilian-utils/pix-payload-utils.rb +253 -0
  49. data/lib/brazilian-utils/registro-profissional-utils.rb +112 -0
  50. data/lib/brazilian-utils/renavam-utils.rb +14 -0
  51. data/lib/brazilian-utils/state-utils.rb +105 -0
  52. data/lib/brazilian-utils/text-utils.rb +105 -0
  53. data/lib/brazilian-utils/vin-utils.rb +46 -0
  54. data/lib/brazilian-utils/voter-id-utils.rb +36 -0
  55. metadata +39 -1
@@ -1,228 +1,347 @@
1
- require 'bigdecimal'
2
-
3
- module BrazilianUtils
4
- module CurrencyUtils
5
- # Formats a numeric value as Brazilian currency (R$).
6
- #
7
- # @param value [Float, Integer, String, BigDecimal] The numeric value to format.
8
- # @return [String, nil] Formatted currency string (e.g., "R$ 1.234,56") or nil if invalid.
9
- #
10
- # @example
11
- # format_currency(1234.56) #=> "R$ 1.234,56"
12
- # format_currency(0) #=> "R$ 0,00"
13
- # format_currency(-9876.54) #=> "R$ -9.876,54"
14
- # format_currency("invalid") #=> nil
15
- def self.format_currency(value)
16
- decimal_value = BigDecimal(value.to_s)
17
-
18
- # Format with 2 decimal places and thousands separator
19
- formatted = format('%.2f', decimal_value)
20
-
21
- # Split into integer and decimal parts
22
- integer_part, decimal_part = formatted.split('.')
23
-
24
- # Add thousands separator to integer part
25
- integer_part = integer_part.chars.reverse.each_slice(3).map(&:join).join('.').reverse
26
-
27
- # Combine with Brazilian format
28
- "R$ #{integer_part},#{decimal_part}"
29
- rescue ArgumentError, TypeError
30
- nil
31
- end
32
-
33
- # Converts a monetary value in Brazilian Reais to textual representation.
34
- #
35
- # @param amount [BigDecimal, Float, Integer, String] Monetary value to convert.
36
- # @return [String, nil] Textual representation in Brazilian Portuguese, or nil if invalid.
37
- #
38
- # @note
39
- # - Values are rounded down to 2 decimal places
40
- # - Maximum supported value is 1 quadrillion reais
41
- # - Negative values are prefixed with "Menos"
42
- #
43
- # @example
44
- # convert_real_to_text(1523.45)
45
- # #=> "Mil quinhentos e vinte e três reais e quarenta e cinco centavos"
46
- #
47
- # convert_real_to_text(1.00)
48
- # #=> "Um real"
49
- #
50
- # convert_real_to_text(0.50)
51
- # #=> "Cinquenta centavos"
52
- #
53
- # convert_real_to_text(0.00)
54
- # #=> "Zero reais"
55
- def self.convert_real_to_text(amount)
56
- # Convert to BigDecimal and round down to 2 decimal places
57
- decimal_amount = BigDecimal(amount.to_s)
58
- decimal_amount = decimal_amount.truncate(2)
59
-
60
- # Check for invalid values
61
- return nil if decimal_amount.nan? || decimal_amount.infinite?
62
- return nil if decimal_amount.abs > BigDecimal('1000000000000000.00') # 1 quadrillion
63
-
64
- negative = decimal_amount < 0
65
- decimal_amount = decimal_amount.abs
66
-
67
- reais = decimal_amount.to_i
68
- centavos = ((decimal_amount - reais) * 100).to_i
69
-
70
- parts = []
71
-
72
- if reais > 0
73
- reais_text = number_to_words(reais)
74
- currency_text = reais == 1 ? 'real' : 'reais'
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 ' : ''
79
- parts << "#{reais_text} #{conector}#{currency_text}"
80
- end
81
-
82
- if centavos > 0
83
- centavos_text = "#{number_to_words(centavos)} #{centavos == 1 ? 'centavo' : 'centavos'}"
84
- if reais > 0
85
- parts << "e #{centavos_text}"
86
- else
87
- parts << centavos_text
88
- end
89
- end
90
-
91
- if reais == 0 && centavos == 0
92
- parts << 'Zero reais'
93
- end
94
-
95
- result = parts.join(' ')
96
- result = "Menos #{result}" if negative
97
-
98
- result.capitalize
99
- rescue ArgumentError, TypeError
100
- nil
101
- end
102
-
103
- # Converts a number to its textual representation in Brazilian Portuguese.
104
- #
105
- # @param number [Integer] The number to convert (0 to 999,999,999,999,999,999)
106
- # @return [String] The textual representation
107
- #
108
- # @private
109
- def self.number_to_words(number)
110
- number = number.to_i.abs
111
- return 'zero' if number.zero?
112
-
113
- # Scale names
114
- scales = [
115
- '',
116
- 'mil',
117
- 'milhão',
118
- 'bilhão',
119
- 'trilhão',
120
- 'quadrilhão'
121
- ]
122
-
123
- scales_plural = [
124
- '',
125
- 'mil',
126
- 'milhões',
127
- 'bilhões',
128
- 'trilhões',
129
- 'quadrilhões'
130
- ]
131
-
132
- # Break number into groups of 3 digits, lowest order first
133
- # (groups[0] is units-hundreds, groups[1] is thousands, ...)
134
- groups = []
135
- temp = number
136
- while temp > 0
137
- groups << temp % 1000
138
- temp /= 1000
139
- end
140
-
141
- parts = []
142
- groups.each_with_index do |group, index|
143
- next if group.zero?
144
-
145
- group_text = convert_group(group)
146
- scale_name = group == 1 ? scales[index] : scales_plural[index]
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
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}"
172
- end
173
-
174
- # Converts a group of 3 digits (0-999) to words.
175
- #
176
- # @param number [Integer] Number between 0 and 999
177
- # @return [String] The textual representation
178
- #
179
- # @private
180
- def self.convert_group(number)
181
- ones = %w[zero um dois três quatro cinco seis sete oito nove]
182
- tens = %w[dez onze doze treze quatorze quinze dezesseis dezessete dezoito dezenove]
183
- tens_multiples = %w[_ _ vinte trinta quarenta cinquenta sessenta setenta oitenta noventa]
184
- hundreds = %w[
185
- _
186
- cento
187
- duzentos
188
- trezentos
189
- quatrocentos
190
- quinhentos
191
- seiscentos
192
- setecentos
193
- oitocentos
194
- novecentos
195
- ]
196
-
197
- return ones[number] if number < 10
198
-
199
- if number < 20
200
- return tens[number - 10]
201
- end
202
-
203
- if number < 100
204
- tens_digit = number / 10
205
- ones_digit = number % 10
206
- if ones_digit.zero?
207
- return tens_multiples[tens_digit]
208
- else
209
- return "#{tens_multiples[tens_digit]} e #{ones[ones_digit]}"
210
- end
211
- end
212
-
213
- # 100-999
214
- hundreds_digit = number / 100
215
- remainder = number % 100
216
-
217
- if number == 100
218
- 'cem'
219
- elsif remainder.zero?
220
- hundreds[hundreds_digit]
221
- else
222
- "#{hundreds[hundreds_digit]} e #{convert_group(remainder)}"
223
- end
224
- end
225
-
226
- private_class_method :number_to_words, :convert_group
227
- end
228
- end
1
+ require 'bigdecimal'
2
+
3
+ module BrazilianUtils
4
+ module CurrencyUtils
5
+ # Splits a BRL-ish amount string into its integer and decimal parts.
6
+ #
7
+ # The last `,` or `.` followed by 1 to `decimal_max` digits and then the
8
+ # end of the string is treated as the decimal separator; every other `,`
9
+ # or `.` found before it is treated as a thousands separator and removed.
10
+ #
11
+ # @param str [String] The (already symbol-stripped) amount string.
12
+ # @param decimal_max [Integer] Maximum length of a trailing decimal run.
13
+ # @return [Array(String, String, Boolean, Boolean)] integer part (digits
14
+ # only), decimal part (digits only, '' when none), whether a decimal
15
+ # separator was found, and whether the value is negative.
16
+ #
17
+ # @private
18
+ def self.split_amount(str, decimal_max)
19
+ cleaned = str.to_s.gsub(/[^\d.,\-]/, '')
20
+ negative = cleaned.start_with?('-')
21
+ cleaned = cleaned.sub(/\A-/, '')
22
+
23
+ return ['0', '', false, negative] if cleaned.empty?
24
+
25
+ last_sep_idx = cleaned.rindex(/[.,]/)
26
+ integer_part = cleaned
27
+ decimal_part = ''
28
+ has_decimal = false
29
+
30
+ if last_sep_idx
31
+ after = cleaned[(last_sep_idx + 1)..-1]
32
+ if after.length.between?(1, decimal_max) && after.match?(/\A\d+\z/)
33
+ has_decimal = true
34
+ decimal_part = after
35
+ integer_part = cleaned[0...last_sep_idx]
36
+ end
37
+ end
38
+
39
+ integer_part = integer_part.gsub(/[.,]/, '')
40
+ integer_part = '0' if integer_part.empty?
41
+
42
+ [integer_part, decimal_part, has_decimal, negative]
43
+ end
44
+
45
+ private_class_method :split_amount
46
+
47
+ # Clamps a requested decimal precision to the 0..20 range, defaulting to 2.
48
+ #
49
+ # @private
50
+ def self.clamp_precision(precision)
51
+ value = precision.nil? ? 2 : precision.to_i
52
+ value.clamp(0, 20)
53
+ end
54
+
55
+ private_class_method :clamp_precision
56
+
57
+ # Parses a Brazilian currency string (e.g. `"R$ 1.234,56"`) into a Float.
58
+ #
59
+ # A value with no thousands/decimal separator at all is read as cents
60
+ # (divided by 10 to the power of the precision); an empty string is 0.
61
+ #
62
+ # @param value [String] The value to parse.
63
+ # @param options [Hash] `:precision` sets the number of decimal places
64
+ # assumed for a separator-less value (default 2).
65
+ # @return [Float] The parsed amount.
66
+ #
67
+ # @example
68
+ # parse_currency("R$ 1.234,56") #=> 1234.56
69
+ # parse_currency("1234") #=> 12.34
70
+ # parse_currency("") #=> 0
71
+ def self.parse_currency(value, options = {})
72
+ precision = clamp_precision(options[:precision] || options['precision'])
73
+ str = value.to_s
74
+ return 0 if str.strip.empty?
75
+
76
+ integer_part, decimal_part, has_decimal, negative = split_amount(str, [precision, 2].max)
77
+
78
+ amount = if has_decimal
79
+ "#{integer_part}.#{decimal_part}".to_f
80
+ else
81
+ integer_part.to_i / (10.0**precision)
82
+ end
83
+
84
+ amount = -amount if negative
85
+ amount
86
+ end
87
+
88
+ class << self
89
+ alias parse parse_currency
90
+ end
91
+
92
+ # Formats a numeric value (or a currency-like string) as Brazilian
93
+ # currency, e.g. `1234.56` becomes `"1.234,56"`.
94
+ #
95
+ # No currency symbol is added by default; pass `options[:symbol] = true`
96
+ # to prefix the result with `"R$ "`.
97
+ #
98
+ # @param value [Float, Integer, String, BigDecimal] The value to format.
99
+ # @param options [Hash] `:precision` (default 2, clamped to 0..20) sets
100
+ # the number of decimal places; `:symbol` (default false) prefixes
101
+ # `"R$ "`.
102
+ # @return [String, nil] Formatted currency string, or nil if invalid.
103
+ #
104
+ # @example
105
+ # format_currency(1234.56) #=> "1.234,56"
106
+ # format_currency(1234.56, symbol: true) #=> "R$ 1.234,56"
107
+ # format_currency("1.234,56") #=> "1.234,56"
108
+ # format_currency("invalid") #=> nil
109
+ def self.format_currency(value, options = {})
110
+ precision = clamp_precision(options[:precision] || options['precision'])
111
+ symbol = options[:symbol] || options['symbol']
112
+
113
+ amount =
114
+ case value
115
+ when Numeric
116
+ value.to_f
117
+ when String
118
+ return nil if value.strip.empty?
119
+ return nil unless value.match?(/\d/)
120
+
121
+ integer_part, decimal_part, has_decimal, negative = split_amount(value, [precision, 2].max)
122
+ n = has_decimal ? "#{integer_part}.#{decimal_part}".to_f : integer_part.to_f
123
+ negative ? -n : n
124
+ else
125
+ return nil
126
+ end
127
+
128
+ return nil unless amount.finite?
129
+
130
+ formatted = sprintf("%.#{precision}f", amount)
131
+ sign = ''
132
+ if formatted.start_with?('-')
133
+ sign = '-'
134
+ formatted = formatted[1..-1]
135
+ end
136
+
137
+ integer_str, decimal_str = formatted.split('.')
138
+ integer_str = integer_str.chars.reverse.each_slice(3).map(&:join).join('.').reverse
139
+
140
+ result = "#{sign}#{integer_str}"
141
+ result += ",#{decimal_str}" if decimal_str
142
+ symbol ? "R$ #{result}" : result
143
+ rescue ArgumentError, TypeError, FloatDomainError
144
+ nil
145
+ end
146
+
147
+ class << self
148
+ alias format format_currency
149
+ end
150
+
151
+ # Converts a monetary value in Brazilian Reais to textual representation.
152
+ #
153
+ # @param amount [BigDecimal, Float, Integer, String] Monetary value to convert.
154
+ # @return [String, nil] Textual representation in Brazilian Portuguese
155
+ # (all lower case), or nil if invalid.
156
+ #
157
+ # @note
158
+ # - Values are truncated (not rounded) to 2 decimal places
159
+ # - Maximum supported value is 1 quadrillion reais
160
+ # - Negative values are prefixed with "menos"
161
+ #
162
+ # @example
163
+ # convert_real_to_text(1523.45)
164
+ # #=> "mil quinhentos e vinte e três reais e quarenta e cinco centavos"
165
+ #
166
+ # convert_real_to_text(1.00)
167
+ # #=> "um real"
168
+ #
169
+ # convert_real_to_text(0.50)
170
+ # #=> "cinquenta centavos"
171
+ #
172
+ # convert_real_to_text(0.00)
173
+ # #=> "zero reais"
174
+ def self.convert_real_to_text(amount)
175
+ # Convert to BigDecimal and round down to 2 decimal places
176
+ decimal_amount = BigDecimal(amount.to_s)
177
+ decimal_amount = decimal_amount.truncate(2)
178
+
179
+ # Check for invalid values
180
+ return nil if decimal_amount.nan? || decimal_amount.infinite?
181
+ return nil if decimal_amount.abs > BigDecimal('1000000000000000.00') # 1 quadrillion
182
+
183
+ negative = decimal_amount < 0
184
+ decimal_amount = decimal_amount.abs
185
+
186
+ reais = decimal_amount.to_i
187
+ centavos = ((decimal_amount - reais) * 100).to_i
188
+
189
+ parts = []
190
+
191
+ if reais > 0
192
+ reais_text = number_to_words(reais)
193
+ currency_text = reais == 1 ? 'real' : 'reais'
194
+ # "de" only applies when the text ends in "milhão(ões)"/"bilhão(ões)"/...
195
+ # on its own (e.g. "um milhão de reais"), not when it's followed by
196
+ # more words (e.g. "um milhão e um reais", no "de").
197
+ conector = reais_text.match?(/lhão$|lhões$/) ? 'de ' : ''
198
+ parts << "#{reais_text} #{conector}#{currency_text}"
199
+ end
200
+
201
+ if centavos > 0
202
+ centavos_text = "#{number_to_words(centavos)} #{centavos == 1 ? 'centavo' : 'centavos'}"
203
+ if reais > 0
204
+ parts << "e #{centavos_text}"
205
+ else
206
+ parts << centavos_text
207
+ end
208
+ end
209
+
210
+ if reais == 0 && centavos == 0
211
+ parts << 'zero reais'
212
+ end
213
+
214
+ result = parts.join(' ')
215
+ result = "menos #{result}" if negative
216
+
217
+ result
218
+ rescue ArgumentError, TypeError
219
+ nil
220
+ end
221
+
222
+ # Converts a number to its textual representation in Brazilian Portuguese.
223
+ #
224
+ # @param number [Integer] The number to convert (0 to 999,999,999,999,999,999)
225
+ # @return [String] The textual representation
226
+ #
227
+ # @private
228
+ def self.number_to_words(number)
229
+ number = number.to_i.abs
230
+ return 'zero' if number.zero?
231
+
232
+ # Scale names
233
+ scales = [
234
+ '',
235
+ 'mil',
236
+ 'milhão',
237
+ 'bilhão',
238
+ 'trilhão',
239
+ 'quadrilhão'
240
+ ]
241
+
242
+ scales_plural = [
243
+ '',
244
+ 'mil',
245
+ 'milhões',
246
+ 'bilhões',
247
+ 'trilhões',
248
+ 'quadrilhões'
249
+ ]
250
+
251
+ # Break number into groups of 3 digits, lowest order first
252
+ # (groups[0] is units-hundreds, groups[1] is thousands, ...)
253
+ groups = []
254
+ temp = number
255
+ while temp > 0
256
+ groups << temp % 1000
257
+ temp /= 1000
258
+ end
259
+
260
+ parts = []
261
+ groups.each_with_index do |group, index|
262
+ next if group.zero?
263
+
264
+ group_text = convert_group(group)
265
+ scale_name = group == 1 ? scales[index] : scales_plural[index]
266
+
267
+ parts << if scale_name.empty?
268
+ group_text
269
+ elsif index == 1 && group == 1 # "mil" doesn't need "um" before it
270
+ scale_name
271
+ else
272
+ "#{group_text} #{scale_name}"
273
+ end
274
+ end
275
+
276
+ # parts was built lowest-order group first; the spoken form reads
277
+ # highest order first (e.g. "mil duzentos e trinta e quatro", not
278
+ # "duzentos e trinta e quatro, mil").
279
+ parts.reverse!
280
+ return parts.first if parts.length == 1
281
+
282
+ # The lowest-order non-zero group (spoken last) gets an "e" in front
283
+ # when it reads as a single small/round term: below 100, or an exact
284
+ # multiple of 100 (e.g. "mil e quatrocentos", "mil e um"); otherwise
285
+ # groups are simply concatenated with a space, no comma
286
+ # (Manual de Redação da Presidência: "mil duzentos e cinquenta reais").
287
+ final_group = groups.find { |g| !g.zero? }
288
+ last = parts.pop
289
+ separator = (final_group < 100 || (final_group % 100).zero?) ? ' e ' : ' '
290
+ "#{parts.join(' ')}#{separator}#{last}"
291
+ end
292
+
293
+ # Converts a group of 3 digits (0-999) to words.
294
+ #
295
+ # @param number [Integer] Number between 0 and 999
296
+ # @return [String] The textual representation
297
+ #
298
+ # @private
299
+ def self.convert_group(number)
300
+ ones = %w[zero um dois três quatro cinco seis sete oito nove]
301
+ tens = %w[dez onze doze treze quatorze quinze dezesseis dezessete dezoito dezenove]
302
+ tens_multiples = %w[_ _ vinte trinta quarenta cinquenta sessenta setenta oitenta noventa]
303
+ hundreds = %w[
304
+ _
305
+ cento
306
+ duzentos
307
+ trezentos
308
+ quatrocentos
309
+ quinhentos
310
+ seiscentos
311
+ setecentos
312
+ oitocentos
313
+ novecentos
314
+ ]
315
+
316
+ return ones[number] if number < 10
317
+
318
+ if number < 20
319
+ return tens[number - 10]
320
+ end
321
+
322
+ if number < 100
323
+ tens_digit = number / 10
324
+ ones_digit = number % 10
325
+ if ones_digit.zero?
326
+ return tens_multiples[tens_digit]
327
+ else
328
+ return "#{tens_multiples[tens_digit]} e #{ones[ones_digit]}"
329
+ end
330
+ end
331
+
332
+ # 100-999
333
+ hundreds_digit = number / 100
334
+ remainder = number % 100
335
+
336
+ if number == 100
337
+ 'cem'
338
+ elsif remainder.zero?
339
+ hundreds[hundreds_digit]
340
+ else
341
+ "#{hundreds[hundreds_digit]} e #{convert_group(remainder)}"
342
+ end
343
+ end
344
+
345
+ private_class_method :number_to_words, :convert_group
346
+ end
347
+ end