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.
- checksums.yaml +4 -4
- data/README.md +446 -22
- data/lib/brazilian-utils/area-code-utils.rb +60 -0
- data/lib/brazilian-utils/bank-account-utils.rb +82 -0
- data/lib/brazilian-utils/bank-utils.rb +61 -0
- data/lib/brazilian-utils/boleto-utils.rb +139 -0
- data/lib/brazilian-utils/caepf-utils.rb +91 -0
- data/lib/brazilian-utils/cbo-utils.rb +74 -0
- data/lib/brazilian-utils/cei-utils.rb +79 -0
- data/lib/brazilian-utils/cep-utils.rb +25 -1
- data/lib/brazilian-utils/certidao-utils.rb +171 -0
- data/lib/brazilian-utils/cfop-utils.rb +74 -0
- data/lib/brazilian-utils/cnae-utils.rb +104 -0
- data/lib/brazilian-utils/cnh-utils.rb +53 -0
- data/lib/brazilian-utils/cno-utils.rb +76 -0
- data/lib/brazilian-utils/cnpj-utils.rb +125 -12
- data/lib/brazilian-utils/cns-utils.rb +110 -0
- data/lib/brazilian-utils/cpf-utils.rb +13 -0
- data/lib/brazilian-utils/credit-card-utils.rb +47 -0
- data/lib/brazilian-utils/csosn-utils.rb +55 -0
- data/lib/brazilian-utils/cst-utils.rb +103 -0
- data/lib/brazilian-utils/currency-utils.rb +347 -228
- data/lib/brazilian-utils/data/area_codes.json +676 -0
- data/lib/brazilian-utils/data/banks.json +357 -0
- data/lib/brazilian-utils/data/cbo.json +1 -0
- data/lib/brazilian-utils/data/cfop.json +1 -0
- data/lib/brazilian-utils/data/cnae.json +1 -0
- data/lib/brazilian-utils/data/csosn.json +42 -0
- data/lib/brazilian-utils/data/cst.json +294 -0
- data/lib/brazilian-utils/data/legal_nature.json +900 -0
- data/lib/brazilian-utils/data/municipalities.json +1 -0
- data/lib/brazilian-utils/data/ncm.json +1 -0
- data/lib/brazilian-utils/data/states.json +218 -0
- data/lib/brazilian-utils/date-utils.rb +504 -256
- data/lib/brazilian-utils/iban-utils.rb +120 -0
- data/lib/brazilian-utils/ie-utils.rb +84 -0
- data/lib/brazilian-utils/legal-nature-utils.rb +238 -235
- data/lib/brazilian-utils/legal-process-utils.rb +31 -3
- data/lib/brazilian-utils/license-plate-utils.rb +21 -5
- data/lib/brazilian-utils/municipality-utils.rb +58 -0
- data/lib/brazilian-utils/ncm-utils.rb +91 -0
- data/lib/brazilian-utils/nfe-key-utils.rb +158 -0
- data/lib/brazilian-utils/number-utils.rb +123 -0
- data/lib/brazilian-utils/passport-utils.rb +65 -0
- data/lib/brazilian-utils/phone-utils.rb +491 -280
- data/lib/brazilian-utils/pis-utils.rb +16 -1
- data/lib/brazilian-utils/pix-key-utils.rb +70 -0
- data/lib/brazilian-utils/pix-payload-utils.rb +253 -0
- data/lib/brazilian-utils/registro-profissional-utils.rb +112 -0
- data/lib/brazilian-utils/renavam-utils.rb +14 -0
- data/lib/brazilian-utils/state-utils.rb +105 -0
- data/lib/brazilian-utils/text-utils.rb +105 -0
- data/lib/brazilian-utils/vin-utils.rb +46 -0
- data/lib/brazilian-utils/voter-id-utils.rb +36 -0
- metadata +39 -1
|
@@ -1,256 +1,504 @@
|
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
[
|
|
36
|
-
[
|
|
37
|
-
[
|
|
38
|
-
[
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
#
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
'
|
|
56
|
-
'
|
|
57
|
-
'
|
|
58
|
-
'
|
|
59
|
-
'
|
|
60
|
-
'
|
|
61
|
-
'
|
|
62
|
-
'
|
|
63
|
-
'
|
|
64
|
-
'
|
|
65
|
-
'
|
|
66
|
-
'
|
|
67
|
-
'
|
|
68
|
-
'
|
|
69
|
-
'
|
|
70
|
-
'
|
|
71
|
-
'
|
|
72
|
-
'
|
|
73
|
-
'
|
|
74
|
-
'
|
|
75
|
-
'
|
|
76
|
-
'
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
#
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
#
|
|
104
|
-
#
|
|
105
|
-
#
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
#
|
|
143
|
-
#
|
|
144
|
-
#
|
|
145
|
-
#
|
|
146
|
-
#
|
|
147
|
-
# @
|
|
148
|
-
#
|
|
149
|
-
#
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
#
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
+
ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/.freeze
|
|
29
|
+
|
|
30
|
+
MIN_YEAR = 1900
|
|
31
|
+
MAX_YEAR = 2099
|
|
32
|
+
|
|
33
|
+
# Brazilian national holidays (fixed dates)
|
|
34
|
+
NATIONAL_HOLIDAYS = {
|
|
35
|
+
[1, 1] => 'Ano Novo',
|
|
36
|
+
[4, 21] => 'Tiradentes',
|
|
37
|
+
[5, 1] => 'Dia do Trabalho',
|
|
38
|
+
[9, 7] => 'Independência do Brasil',
|
|
39
|
+
[10, 12] => 'Nossa Senhora Aparecida',
|
|
40
|
+
[11, 2] => 'Finados',
|
|
41
|
+
[11, 15] => 'Proclamação da República',
|
|
42
|
+
[12, 25] => 'Natal'
|
|
43
|
+
}.freeze
|
|
44
|
+
|
|
45
|
+
# Lei 14.759/2023 made November 20th (Dia Nacional de Zumbi e da
|
|
46
|
+
# Consciência Negra) a national holiday starting in 2024; before that it
|
|
47
|
+
# was only a holiday in the states/cities that already had their own law
|
|
48
|
+
# for it (some of which are still listed in STATE_HOLIDAYS below).
|
|
49
|
+
NATIONAL_CONSCIENCIA_NEGRA_MONTH_DAY = [11, 20].freeze
|
|
50
|
+
NATIONAL_CONSCIENCIA_NEGRA_EFFECTIVE_YEAR = 2024
|
|
51
|
+
NATIONAL_CONSCIENCIA_NEGRA_NAME = 'Dia Nacional de Zumbi e da Consciência Negra'
|
|
52
|
+
|
|
53
|
+
# State-specific holidays (fixed dates)
|
|
54
|
+
STATE_HOLIDAYS = {
|
|
55
|
+
'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' },
|
|
56
|
+
'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' },
|
|
57
|
+
'AM' => { [9, 5] => 'Elevação do Amazonas à categoria de província' },
|
|
58
|
+
'AP' => { [3, 19] => 'Dia de São José', [9, 13] => 'Criação do Território Federal' },
|
|
59
|
+
'BA' => { [7, 2] => 'Independência da Bahia' },
|
|
60
|
+
'CE' => { [3, 19] => 'São José', [3, 25] => 'Data Magna do Ceará' },
|
|
61
|
+
'DF' => { [4, 21] => 'Fundação de Brasília', [11, 30] => 'Dia do Evangélico' },
|
|
62
|
+
'ES' => { [4, 21] => 'Nossa Senhora da Penha' },
|
|
63
|
+
'GO' => { [10, 24] => 'Pedra fundamental de Goiânia' },
|
|
64
|
+
'MA' => { [7, 28] => 'Adesão do Maranhão à independência do Brasil' },
|
|
65
|
+
'MG' => { [4, 21] => 'Data Magna de Minas Gerais' },
|
|
66
|
+
'MS' => { [10, 11] => 'Criação do estado' },
|
|
67
|
+
'MT' => { [11, 20] => 'Dia da Consciência Negra' },
|
|
68
|
+
'PA' => { [8, 15] => 'Adesão do Grão-Pará à independência do Brasil' },
|
|
69
|
+
'PB' => { [7, 26] => 'Homenagem à memória do ex-presidente João Pessoa', [8, 5] => 'Fundação do Estado em 1585' },
|
|
70
|
+
'PE' => { [3, 6] => 'Revolução Pernambucana de 1817', [6, 24] => 'São João' },
|
|
71
|
+
'PI' => { [10, 19] => 'Dia do Piauí' },
|
|
72
|
+
'PR' => { [12, 19] => 'Emancipação política do Paraná' },
|
|
73
|
+
'RJ' => { [4, 23] => 'Dia de São Jorge', [11, 20] => 'Dia da Consciência Negra' },
|
|
74
|
+
'RN' => { [6, 29] => 'Dia de São Pedro', [10, 3] => 'Mártires de Cunhaú e Uruaçu' },
|
|
75
|
+
'RO' => { [1, 4] => 'Criação do estado', [6, 18] => 'Dia do Evangélico' },
|
|
76
|
+
'RR' => { [10, 5] => 'Criação de Roraima' },
|
|
77
|
+
'RS' => { [9, 20] => 'Revolução Farroupilha' },
|
|
78
|
+
'SC' => { [8, 11] => 'Criação da capitania, separando-se de SP' },
|
|
79
|
+
'SE' => { [7, 8] => 'Autonomia política de Sergipe' },
|
|
80
|
+
'SP' => { [7, 9] => 'Revolução Constitucionalista de 1932' },
|
|
81
|
+
'TO' => { [10, 5] => 'Criação de Tocantins' }
|
|
82
|
+
}.freeze
|
|
83
|
+
|
|
84
|
+
VALID_UFS = %w[
|
|
85
|
+
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
|
|
86
|
+
].freeze
|
|
87
|
+
|
|
88
|
+
# Checks if the given date is a national or state holiday in Brazil.
|
|
89
|
+
#
|
|
90
|
+
# Accepts either the legacy positional form `is_holiday(date, uf)` or a
|
|
91
|
+
# single options Hash (as the contract's `IsHolidayParams`), e.g.
|
|
92
|
+
# `is_holiday(date: Date.new(2024, 7, 9), state: 'SP')`.
|
|
93
|
+
#
|
|
94
|
+
# @param target_date [Date, DateTime, Time, Hash] The date to check, or
|
|
95
|
+
# an options Hash with `:date` and an optional `:state`/`:uf`.
|
|
96
|
+
# @param uf [String, nil] The state abbreviation (UF) to check for state
|
|
97
|
+
# holidays, when `target_date` is not itself a Hash. An unknown UF is
|
|
98
|
+
# simply ignored (national holidays are still checked).
|
|
99
|
+
#
|
|
100
|
+
# @return [Boolean] true if the date is a holiday, false otherwise
|
|
101
|
+
# (including when the date is missing or invalid).
|
|
102
|
+
#
|
|
103
|
+
# @note This implementation includes fixed national and state holidays,
|
|
104
|
+
# plus the moveable Sexta-feira Santa (Good Friday).
|
|
105
|
+
#
|
|
106
|
+
# @example
|
|
107
|
+
# is_holiday(Date.new(2024, 1, 1)) #=> true (New Year)
|
|
108
|
+
# is_holiday(Date.new(2024, 1, 2)) #=> false
|
|
109
|
+
# is_holiday(Date.new(2024, 7, 9), 'SP') #=> true (SP state holiday)
|
|
110
|
+
# is_holiday(date: Date.new(2024, 12, 25)) #=> true (Christmas)
|
|
111
|
+
def self.is_holiday(target_date = nil, uf = nil)
|
|
112
|
+
if target_date.is_a?(Hash)
|
|
113
|
+
opts = target_date
|
|
114
|
+
target_date = opts[:date] || opts['date']
|
|
115
|
+
uf = opts[:state] || opts[:uf] || opts['state'] || opts['uf']
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
return false unless target_date.is_a?(Date) || target_date.is_a?(DateTime) || target_date.is_a?(Time)
|
|
119
|
+
|
|
120
|
+
date = target_date.is_a?(Date) ? target_date : target_date.to_date
|
|
121
|
+
return false unless date.year.between?(MIN_YEAR, MAX_YEAR)
|
|
122
|
+
|
|
123
|
+
month_day = [date.month, date.day]
|
|
124
|
+
|
|
125
|
+
return true if NATIONAL_HOLIDAYS.key?(month_day)
|
|
126
|
+
|
|
127
|
+
if month_day == NATIONAL_CONSCIENCIA_NEGRA_MONTH_DAY && date.year >= NATIONAL_CONSCIENCIA_NEGRA_EFFECTIVE_YEAR
|
|
128
|
+
return true
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
return true if date == good_friday(date.year)
|
|
132
|
+
|
|
133
|
+
if uf
|
|
134
|
+
state_uf = uf.to_s.upcase
|
|
135
|
+
state_holidays = STATE_HOLIDAYS[state_uf]
|
|
136
|
+
return true if state_holidays && state_holidays.key?(month_day)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
false
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Returns the date of Easter Sunday (Domingo de Páscoa) for a given
|
|
143
|
+
# Gregorian year, via the anonymous Gregorian computus algorithm
|
|
144
|
+
# (Meeus/Jones/Butcher).
|
|
145
|
+
#
|
|
146
|
+
# @param year [Integer]
|
|
147
|
+
# @return [Date]
|
|
148
|
+
#
|
|
149
|
+
# @private
|
|
150
|
+
def self.easter_sunday(year)
|
|
151
|
+
a = year % 19
|
|
152
|
+
b = year / 100
|
|
153
|
+
c = year % 100
|
|
154
|
+
d = b / 4
|
|
155
|
+
e = b % 4
|
|
156
|
+
f = (b + 8) / 25
|
|
157
|
+
g = (b - f + 1) / 3
|
|
158
|
+
h = (19 * a + b - d - g + 15) % 30
|
|
159
|
+
i = c / 4
|
|
160
|
+
k = c % 4
|
|
161
|
+
l = (32 + 2 * e + 2 * i - h - k) % 7
|
|
162
|
+
m = (a + 11 * h + 22 * l) / 451
|
|
163
|
+
month = (h + l - 7 * m + 114) / 31
|
|
164
|
+
day = ((h + l - 7 * m + 114) % 31) + 1
|
|
165
|
+
Date.new(year, month, day)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
private_class_method :easter_sunday
|
|
169
|
+
|
|
170
|
+
# @private
|
|
171
|
+
def self.good_friday(year)
|
|
172
|
+
easter_sunday(year) - 2
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
private_class_method :good_friday
|
|
176
|
+
|
|
177
|
+
# @private
|
|
178
|
+
def self.carnaval_monday(year)
|
|
179
|
+
easter_sunday(year) - 48
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
private_class_method :carnaval_monday
|
|
183
|
+
|
|
184
|
+
# @private
|
|
185
|
+
def self.carnaval_tuesday(year)
|
|
186
|
+
easter_sunday(year) - 47
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
private_class_method :carnaval_tuesday
|
|
190
|
+
|
|
191
|
+
# @private
|
|
192
|
+
def self.corpus_christi(year)
|
|
193
|
+
easter_sunday(year) + 60
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
private_class_method :corpus_christi
|
|
197
|
+
|
|
198
|
+
# Returns the Brazilian holidays of a given year, sorted by date.
|
|
199
|
+
#
|
|
200
|
+
# Each holiday is a Hash with `:name`, `:date` (a `Date`) and `:type`
|
|
201
|
+
# (`:national`, `:state`, `:optional` or `:religious`).
|
|
202
|
+
#
|
|
203
|
+
# @param year [Integer] A year between 1900 and 2099.
|
|
204
|
+
# @return [Array<Hash>] The holidays, sorted by date; an empty array when
|
|
205
|
+
# `year` is out of range.
|
|
206
|
+
#
|
|
207
|
+
# @example
|
|
208
|
+
# get_holidays(2024).first
|
|
209
|
+
# #=> { name: "Ano Novo", date: #<Date: 2024-01-01>, type: :national }
|
|
210
|
+
def self.get_holidays(year)
|
|
211
|
+
return [] unless year.is_a?(Integer) && year.between?(MIN_YEAR, MAX_YEAR)
|
|
212
|
+
|
|
213
|
+
holidays = []
|
|
214
|
+
|
|
215
|
+
NATIONAL_HOLIDAYS.each do |(month, day), name|
|
|
216
|
+
holidays << { name: name, date: Date.new(year, month, day), type: :national }
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
if year >= NATIONAL_CONSCIENCIA_NEGRA_EFFECTIVE_YEAR
|
|
220
|
+
holidays << {
|
|
221
|
+
name: NATIONAL_CONSCIENCIA_NEGRA_NAME,
|
|
222
|
+
date: Date.new(year, *NATIONAL_CONSCIENCIA_NEGRA_MONTH_DAY),
|
|
223
|
+
type: :national
|
|
224
|
+
}
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
holidays << { name: 'Sexta-feira Santa', date: good_friday(year), type: :religious }
|
|
228
|
+
holidays << { name: 'Carnaval', date: carnaval_monday(year), type: :optional }
|
|
229
|
+
holidays << { name: 'Carnaval', date: carnaval_tuesday(year), type: :optional }
|
|
230
|
+
holidays << { name: 'Corpus Christi', date: corpus_christi(year), type: :optional }
|
|
231
|
+
|
|
232
|
+
holidays.sort_by { |h| h[:date] }
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# @private
|
|
236
|
+
def self.coerce_date(value)
|
|
237
|
+
case value
|
|
238
|
+
when Date, DateTime, Time
|
|
239
|
+
value.is_a?(Date) ? value : value.to_date
|
|
240
|
+
else
|
|
241
|
+
nil
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
private_class_method :coerce_date
|
|
246
|
+
|
|
247
|
+
# Checks whether a date is a Brazilian business day (dia útil): not a
|
|
248
|
+
# Saturday, a Sunday, nor a holiday from {get_holidays}.
|
|
249
|
+
#
|
|
250
|
+
# @param value [Date, DateTime, Time] The date to check.
|
|
251
|
+
# @param options [Hash] `:optional` (default true) also counts Carnaval
|
|
252
|
+
# and Corpus Christi; `:state`/`:uf` also counts that state's holidays.
|
|
253
|
+
# @return [Boolean] false for an invalid date or one outside 1900..2099.
|
|
254
|
+
def self.is_business_day(value, options = {})
|
|
255
|
+
date = coerce_date(value)
|
|
256
|
+
return false unless date
|
|
257
|
+
return false unless date.year.between?(MIN_YEAR, MAX_YEAR)
|
|
258
|
+
return false if [0, 6].include?(date.wday) # Sunday = 0, Saturday = 6
|
|
259
|
+
|
|
260
|
+
count_optional = options[:optional].nil? && options['optional'].nil? ? true : (options[:optional] || options['optional'])
|
|
261
|
+
state = options[:state] || options[:uf] || options['state'] || options['uf']
|
|
262
|
+
|
|
263
|
+
holidays = get_holidays(date.year)
|
|
264
|
+
holidays.concat(get_holidays(date.year - 1), get_holidays(date.year + 1)) if date.month == 1 || date.month == 12
|
|
265
|
+
|
|
266
|
+
holidays.each do |holiday|
|
|
267
|
+
next if holiday[:date] != date
|
|
268
|
+
next if holiday[:type] == :optional && !count_optional
|
|
269
|
+
|
|
270
|
+
return false
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
if state
|
|
274
|
+
state_uf = state.to_s.upcase
|
|
275
|
+
state_holidays = STATE_HOLIDAYS[state_uf]
|
|
276
|
+
return false if state_holidays && state_holidays.key?([date.month, date.day])
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
true
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
class << self
|
|
283
|
+
alias business_day? is_business_day
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Adds (or, with a negative amount, subtracts) a number of Brazilian
|
|
287
|
+
# business days to a date, skipping weekends and holidays.
|
|
288
|
+
#
|
|
289
|
+
# @param date [Date, DateTime, Time] The starting date.
|
|
290
|
+
# @param amount [Integer] The number of business days to add.
|
|
291
|
+
# @param options [Hash] Same as {is_business_day}.
|
|
292
|
+
# @return [Date, DateTime, Time, nil] A new date/time of the same class
|
|
293
|
+
# as the input (time of day preserved), or nil for an invalid date, a
|
|
294
|
+
# non-integer amount, or a result outside 1900..2099.
|
|
295
|
+
def self.add_business_days(date, amount, options = {})
|
|
296
|
+
return nil unless date.is_a?(Date) || date.is_a?(DateTime) || date.is_a?(Time)
|
|
297
|
+
return nil unless amount.is_a?(Integer)
|
|
298
|
+
|
|
299
|
+
base_date = coerce_date(date)
|
|
300
|
+
return nil unless base_date
|
|
301
|
+
|
|
302
|
+
remaining = amount.abs
|
|
303
|
+
step = amount.negative? ? -1 : 1
|
|
304
|
+
current = base_date
|
|
305
|
+
|
|
306
|
+
while remaining.positive?
|
|
307
|
+
current += step
|
|
308
|
+
remaining -= 1 if is_business_day(current, options)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
return nil unless current.year.between?(MIN_YEAR, MAX_YEAR)
|
|
312
|
+
|
|
313
|
+
diff_days = (current - base_date).to_i
|
|
314
|
+
date + diff_days
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Subtracts a number of Brazilian business days from a date.
|
|
318
|
+
#
|
|
319
|
+
# @param date [Date, DateTime, Time] The starting date.
|
|
320
|
+
# @param amount [Integer] The number of business days to subtract.
|
|
321
|
+
# @param options [Hash] Same as {is_business_day}.
|
|
322
|
+
# @return [Date, DateTime, Time, nil] See {add_business_days}.
|
|
323
|
+
def self.sub_business_days(date, amount, options = {})
|
|
324
|
+
return nil unless amount.is_a?(Integer)
|
|
325
|
+
|
|
326
|
+
add_business_days(date, -amount, options)
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Counts the Brazilian business days between two dates: `earlier_date`
|
|
330
|
+
# (when it is itself a business day) and every business day strictly
|
|
331
|
+
# between the two; `later_date` is never counted.
|
|
332
|
+
#
|
|
333
|
+
# @param later_date [Date, DateTime, Time]
|
|
334
|
+
# @param earlier_date [Date, DateTime, Time]
|
|
335
|
+
# @param options [Hash] Same as {is_business_day}.
|
|
336
|
+
# @return [Integer, nil] Negative when `later_date` precedes
|
|
337
|
+
# `earlier_date`; 0 on the same calendar day; nil when either date is
|
|
338
|
+
# invalid or outside 1900..2099.
|
|
339
|
+
def self.difference_in_business_days(later_date, earlier_date, options = {})
|
|
340
|
+
later = coerce_date(later_date)
|
|
341
|
+
earlier = coerce_date(earlier_date)
|
|
342
|
+
return nil unless later && earlier
|
|
343
|
+
return nil unless later.year.between?(MIN_YEAR, MAX_YEAR) && earlier.year.between?(MIN_YEAR, MAX_YEAR)
|
|
344
|
+
|
|
345
|
+
return 0 if later == earlier
|
|
346
|
+
|
|
347
|
+
if later > earlier
|
|
348
|
+
count = 0
|
|
349
|
+
d = earlier
|
|
350
|
+
while d < later
|
|
351
|
+
count += 1 if is_business_day(d, options)
|
|
352
|
+
d += 1
|
|
353
|
+
end
|
|
354
|
+
count
|
|
355
|
+
else
|
|
356
|
+
-difference_in_business_days(earlier, later, options)
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Converts a given date to its textual representation in Brazilian
|
|
361
|
+
# Portuguese ("por extenso"), e.g. `"primeiro de janeiro de dois mil e
|
|
362
|
+
# vinte e quatro"`.
|
|
363
|
+
#
|
|
364
|
+
# @param date [String, Date, DateTime, Time] The date to convert. A
|
|
365
|
+
# string may be `dd/mm/yyyy` or ISO `yyyy-mm-dd`.
|
|
366
|
+
#
|
|
367
|
+
# @return [String] The date written out in Brazilian Portuguese (all
|
|
368
|
+
# lower case), or an empty string when the date is invalid.
|
|
369
|
+
#
|
|
370
|
+
# @example
|
|
371
|
+
# convert_date_to_text("01/01/2024") #=> "primeiro de janeiro de dois mil e vinte e quatro"
|
|
372
|
+
# convert_date_to_text("15/03/2024") #=> "quinze de março de dois mil e vinte e quatro"
|
|
373
|
+
# convert_date_to_text("invalid") #=> ""
|
|
374
|
+
def self.convert_date_to_text(date)
|
|
375
|
+
dt =
|
|
376
|
+
case date
|
|
377
|
+
when Date, DateTime, Time
|
|
378
|
+
date.is_a?(Date) ? date : date.to_date
|
|
379
|
+
when String
|
|
380
|
+
parse_text_date(date)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
return '' unless dt
|
|
384
|
+
|
|
385
|
+
day = dt.day
|
|
386
|
+
month = dt.month
|
|
387
|
+
year = dt.year
|
|
388
|
+
|
|
389
|
+
# Convert day to text (special case for 1st)
|
|
390
|
+
day_str = day == 1 ? 'primeiro' : number_to_words(day)
|
|
391
|
+
|
|
392
|
+
month_name = Months.name(month)
|
|
393
|
+
year_str = number_to_words(year)
|
|
394
|
+
|
|
395
|
+
"#{day_str} de #{month_name} de #{year_str}"
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# Parses a `dd/mm/yyyy` or ISO `yyyy-mm-dd` date string.
|
|
399
|
+
#
|
|
400
|
+
# @return [Date, nil]
|
|
401
|
+
#
|
|
402
|
+
# @private
|
|
403
|
+
def self.parse_text_date(date)
|
|
404
|
+
return nil unless date.is_a?(String)
|
|
405
|
+
|
|
406
|
+
if DATE_REGEX.match?(date)
|
|
407
|
+
begin
|
|
408
|
+
return Date.strptime(date, '%d/%m/%Y')
|
|
409
|
+
rescue ArgumentError
|
|
410
|
+
return nil
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
if ISO_DATE_REGEX.match?(date)
|
|
415
|
+
begin
|
|
416
|
+
return Date.strptime(date, '%Y-%m-%d')
|
|
417
|
+
rescue ArgumentError
|
|
418
|
+
return nil
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
nil
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
private_class_method :parse_text_date
|
|
426
|
+
|
|
427
|
+
# Converts a number to its textual representation in Brazilian Portuguese.
|
|
428
|
+
# This is a simplified version focused on dates (days 1-31, years).
|
|
429
|
+
#
|
|
430
|
+
# @param number [Integer] The number to convert
|
|
431
|
+
# @return [String] The textual representation
|
|
432
|
+
#
|
|
433
|
+
# @private
|
|
434
|
+
def self.number_to_words(number)
|
|
435
|
+
return 'zero' if number.zero?
|
|
436
|
+
|
|
437
|
+
ones = %w[zero um dois três quatro cinco seis sete oito nove]
|
|
438
|
+
tens = %w[dez onze doze treze quatorze quinze dezesseis dezessete dezoito dezenove]
|
|
439
|
+
tens_multiples = %w[_ _ vinte trinta quarenta cinquenta sessenta setenta oitenta noventa]
|
|
440
|
+
hundreds = %w[
|
|
441
|
+
_
|
|
442
|
+
cento
|
|
443
|
+
duzentos
|
|
444
|
+
trezentos
|
|
445
|
+
quatrocentos
|
|
446
|
+
quinhentos
|
|
447
|
+
seiscentos
|
|
448
|
+
setecentos
|
|
449
|
+
oitocentos
|
|
450
|
+
novecentos
|
|
451
|
+
]
|
|
452
|
+
|
|
453
|
+
if number < 10
|
|
454
|
+
return ones[number]
|
|
455
|
+
elsif number < 20
|
|
456
|
+
return tens[number - 10]
|
|
457
|
+
elsif number < 100
|
|
458
|
+
tens_digit = number / 10
|
|
459
|
+
ones_digit = number % 10
|
|
460
|
+
if ones_digit.zero?
|
|
461
|
+
return tens_multiples[tens_digit]
|
|
462
|
+
else
|
|
463
|
+
return "#{tens_multiples[tens_digit]} e #{ones[ones_digit]}"
|
|
464
|
+
end
|
|
465
|
+
elsif number == 100
|
|
466
|
+
return 'cem'
|
|
467
|
+
elsif number < 1000
|
|
468
|
+
hundreds_digit = number / 100
|
|
469
|
+
remainder = number % 100
|
|
470
|
+
if remainder.zero?
|
|
471
|
+
return hundreds[hundreds_digit]
|
|
472
|
+
else
|
|
473
|
+
return "#{hundreds[hundreds_digit]} e #{number_to_words(remainder)}"
|
|
474
|
+
end
|
|
475
|
+
elsif number < 1_000_000
|
|
476
|
+
# For years like 2024
|
|
477
|
+
thousands = number / 1000
|
|
478
|
+
remainder = number % 1000
|
|
479
|
+
|
|
480
|
+
result = []
|
|
481
|
+
|
|
482
|
+
if thousands == 1
|
|
483
|
+
result << 'mil'
|
|
484
|
+
else
|
|
485
|
+
result << "#{number_to_words(thousands)} mil"
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
if remainder > 0
|
|
489
|
+
if remainder < 100
|
|
490
|
+
result << "e #{number_to_words(remainder)}"
|
|
491
|
+
else
|
|
492
|
+
result << number_to_words(remainder)
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
result.join(' ')
|
|
497
|
+
else
|
|
498
|
+
number.to_s
|
|
499
|
+
end
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
private_class_method :number_to_words
|
|
503
|
+
end
|
|
504
|
+
end
|