business_date_calculator 0.1.5 → 1.0.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 +5 -5
- data/.github/workflows/ci.yml +56 -0
- data/.rubocop.yml +46 -0
- data/CHANGELOG.md +33 -0
- data/Gemfile +1 -1
- data/Guardfile +9 -9
- data/README.md +123 -20
- data/Rakefile +1 -2
- data/business_date_calculator.gemspec +5 -4
- data/lib/business_date_calculator/calendar.rb +155 -57
- data/lib/business_date_calculator/version.rb +1 -1
- data/lib/business_date_calculator.rb +2 -2
- metadata +35 -23
- data/.travis.yml +0 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9c6c73d127ef04d88448a0c97d29827023f527fcb6c02f3beaec8385c8846850
|
|
4
|
+
data.tar.gz: 323d8ca926b59cbb69987ee2b7447e5350dc39cd75a60427c254c3b678253e9e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d966e9d83e0506a35b9c46469baeb19b769216e6e9a27808c2b405733bfc37b80b98f0196cd089ff73f92437f00eb6458adfbadba0de448e27c7669629e5302b
|
|
7
|
+
data.tar.gz: 90cf355df47f0a03bf0c2dd646788cd9f63fec2381b2896816eb87d3b9158587063bb83a3d9f34fafd6d085d38d75d4a336a109b1873620943d4c969807800ab
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
ruby: ['3.3']
|
|
15
|
+
name: rspec (ruby ${{ matrix.ruby }})
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Setup Ruby ${{ matrix.ruby }}
|
|
20
|
+
uses: ruby/setup-ruby@v1
|
|
21
|
+
with:
|
|
22
|
+
ruby-version: ${{ matrix.ruby }}
|
|
23
|
+
bundler-cache: true
|
|
24
|
+
|
|
25
|
+
- name: Run rspec
|
|
26
|
+
run: bundle exec rspec --format documentation
|
|
27
|
+
|
|
28
|
+
lint:
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
name: rubocop
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v4
|
|
33
|
+
|
|
34
|
+
- uses: ruby/setup-ruby@v1
|
|
35
|
+
with:
|
|
36
|
+
ruby-version: '3.3'
|
|
37
|
+
bundler-cache: true
|
|
38
|
+
|
|
39
|
+
- name: Run rubocop
|
|
40
|
+
run: bundle exec rubocop --no-color
|
|
41
|
+
|
|
42
|
+
audit:
|
|
43
|
+
runs-on: ubuntu-latest
|
|
44
|
+
name: bundler-audit
|
|
45
|
+
steps:
|
|
46
|
+
- uses: actions/checkout@v4
|
|
47
|
+
|
|
48
|
+
- uses: ruby/setup-ruby@v1
|
|
49
|
+
with:
|
|
50
|
+
ruby-version: '3.3'
|
|
51
|
+
bundler-cache: true
|
|
52
|
+
|
|
53
|
+
- name: Run bundler-audit
|
|
54
|
+
run: |
|
|
55
|
+
bundle exec bundler-audit update
|
|
56
|
+
bundle exec bundler-audit check
|
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
AllCops:
|
|
2
|
+
TargetRubyVersion: 3.1
|
|
3
|
+
NewCops: enable
|
|
4
|
+
SuggestExtensions: false
|
|
5
|
+
Exclude:
|
|
6
|
+
- 'bin/**/*'
|
|
7
|
+
- 'vendor/**/*'
|
|
8
|
+
- 'Guardfile'
|
|
9
|
+
- '*.gemspec'
|
|
10
|
+
|
|
11
|
+
Style/Documentation:
|
|
12
|
+
Enabled: false
|
|
13
|
+
|
|
14
|
+
Style/FrozenStringLiteralComment:
|
|
15
|
+
Enabled: false
|
|
16
|
+
|
|
17
|
+
Layout/LineLength:
|
|
18
|
+
Max: 120
|
|
19
|
+
|
|
20
|
+
Metrics/MethodLength:
|
|
21
|
+
Max: 30
|
|
22
|
+
|
|
23
|
+
Metrics/AbcSize:
|
|
24
|
+
Max: 35
|
|
25
|
+
|
|
26
|
+
Metrics/ClassLength:
|
|
27
|
+
Max: 150
|
|
28
|
+
|
|
29
|
+
Metrics/CyclomaticComplexity:
|
|
30
|
+
Max: 10
|
|
31
|
+
|
|
32
|
+
Metrics/PerceivedComplexity:
|
|
33
|
+
Max: 10
|
|
34
|
+
|
|
35
|
+
Metrics/BlockLength:
|
|
36
|
+
Exclude:
|
|
37
|
+
- 'spec/**/*'
|
|
38
|
+
|
|
39
|
+
# API publica do gem usa nomes legados (is_holiday?, parametro `n` em advance).
|
|
40
|
+
# Renomear quebraria callers existentes (ex.: ivt-funds-api).
|
|
41
|
+
Naming/PredicatePrefix:
|
|
42
|
+
Enabled: false
|
|
43
|
+
|
|
44
|
+
Naming/MethodParameterName:
|
|
45
|
+
AllowedNames:
|
|
46
|
+
- n
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
### Bug fixes (breaking behavior changes)
|
|
6
|
+
|
|
7
|
+
- **`Calendar#build`**: o segundo `while` checava `start_date.wday == 6` no lugar de `end_date.wday == 6` (copy-paste). Com isso, `@end_date` podia terminar num sábado sem feriado explícito, gerando entradas `nil` em `@business_dates` e fazendo `adjust(:following)` e `advance` retornarem `nil` silenciosamente em datas no fim do range.
|
|
8
|
+
|
|
9
|
+
- **`Calendar#advance`**: para `n` positivo grande o suficiente para que `adjusted_date_index(date) + n` ultrapassasse o tamanho de `@business_dates`, o método retornava `nil` silenciosamente. Agora reconstrói o calendário simetricamente ao caso `n` negativo. Resolve o cenário onde fundos com `redemption_conversion_days` alto (ex.: 270) produziam datas nulas.
|
|
10
|
+
|
|
11
|
+
- **`Calendar#networkdays`**: agora levanta `ArgumentError` quando `date1 > date2`, em vez de devolver valor negativo silencioso. Comportamento documentado mas não enforçado anteriormente.
|
|
12
|
+
|
|
13
|
+
### Behavior changes (non-breaking)
|
|
14
|
+
|
|
15
|
+
- **`Calendar#range_check`**: expansão para trás passou de 2 dias para 252 dias (simétrico com a expansão para frente). Elimina reconstruções repetidas em consultas batch retroativas.
|
|
16
|
+
|
|
17
|
+
- **`Calendar#advance`**: recursão para `n` muito negativo agora converge em uma única reconstrução (folga 2x sobre dias úteis pedidos), em vez de múltiplas iterações reconstruindo todo o calendário.
|
|
18
|
+
|
|
19
|
+
- **Thread-safety**: métodos públicos protegidos por `Monitor` (reentrante). Múltiplas threads agora podem chamar `advance`, `adjust`, `networkdays`, `is_holiday?` e `last_day_of_previous_month` simultaneamente sem corromper estado interno durante reconstruções.
|
|
20
|
+
|
|
21
|
+
- **Cópia defensiva de `holidays`**: a lista de feriados passada ao construtor agora é `dup.freeze`. Mutações externas pós-construção não afetam mais o estado interno.
|
|
22
|
+
|
|
23
|
+
- **`Calendar#last_day_of_previous_month`**: reescrito de forma legível usando `Date.civil(year, month, 1) - 1`, sem mudança de comportamento.
|
|
24
|
+
|
|
25
|
+
### Doc
|
|
26
|
+
|
|
27
|
+
- Comentário de `networkdays` reescrito para refletir a semântica real ("saltos entre dias úteis", não contagem inclusiva).
|
|
28
|
+
|
|
29
|
+
### Dev / build
|
|
30
|
+
|
|
31
|
+
- Atualizado `bundler` para `>= 2.0` e `rake` para `>= 12.0`. Adicionado `rspec ~> 3.0` como dev dep explícita.
|
|
32
|
+
- Removido pin de Ruby 2.4 do `Gemfile`.
|
|
33
|
+
- Removidas dev deps `guard-rspec` e `terminal-notifier-guard` (não eram usadas em CI).
|
data/Gemfile
CHANGED
data/Guardfile
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
#
|
|
24
24
|
# and, you'll have to watch "config/Guardfile" instead of "Guardfile"
|
|
25
25
|
|
|
26
|
-
#
|
|
26
|
+
# NOTE: The cmd option is now required due to the increasing number of ways
|
|
27
27
|
# rspec may be run, below are examples of the most common uses.
|
|
28
28
|
# * bundler: 'bundle exec rspec'
|
|
29
29
|
# * bundler binstubs: 'bin/rspec'
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
# * zeus: 'zeus rspec' (requires the server to be started separately)
|
|
33
33
|
# * 'just' rspec: 'rspec'
|
|
34
34
|
|
|
35
|
-
guard :rspec, cmd:
|
|
36
|
-
require
|
|
35
|
+
guard :rspec, cmd: 'bundle exec rspec' do
|
|
36
|
+
require 'guard/rspec/dsl'
|
|
37
37
|
dsl = Guard::RSpec::Dsl.new(self)
|
|
38
38
|
|
|
39
39
|
# Feel free to open issues for suggestions and improvements
|
|
@@ -49,15 +49,15 @@ guard :rspec, cmd: "bundle exec rspec" do
|
|
|
49
49
|
dsl.watch_spec_files_for(ruby.lib_files)
|
|
50
50
|
|
|
51
51
|
# Rails files
|
|
52
|
-
rails = dsl.rails(view_extensions: %w
|
|
52
|
+
rails = dsl.rails(view_extensions: %w[erb haml slim])
|
|
53
53
|
dsl.watch_spec_files_for(rails.app_files)
|
|
54
54
|
dsl.watch_spec_files_for(rails.views)
|
|
55
55
|
|
|
56
56
|
watch(rails.controllers) do |m|
|
|
57
57
|
[
|
|
58
|
-
rspec.spec.("routing/#{m[1]}_routing"),
|
|
59
|
-
rspec.spec.("controllers/#{m[1]}_controller"),
|
|
60
|
-
rspec.spec.("acceptance/#{m[1]}")
|
|
58
|
+
rspec.spec.call("routing/#{m[1]}_routing"),
|
|
59
|
+
rspec.spec.call("controllers/#{m[1]}_controller"),
|
|
60
|
+
rspec.spec.call("acceptance/#{m[1]}")
|
|
61
61
|
]
|
|
62
62
|
end
|
|
63
63
|
|
|
@@ -67,11 +67,11 @@ guard :rspec, cmd: "bundle exec rspec" do
|
|
|
67
67
|
watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" }
|
|
68
68
|
|
|
69
69
|
# Capybara features specs
|
|
70
|
-
watch(rails.view_dirs)
|
|
70
|
+
watch(rails.view_dirs) { |m| rspec.spec.call("features/#{m[1]}") }
|
|
71
71
|
|
|
72
72
|
# Turnip features and steps
|
|
73
73
|
watch(%r{^spec/acceptance/(.+)\.feature$})
|
|
74
74
|
watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m|
|
|
75
|
-
Dir[File.join("**/#{m[1]}.feature")][0] ||
|
|
75
|
+
Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance'
|
|
76
76
|
end
|
|
77
77
|
end
|
data/README.md
CHANGED
|
@@ -1,39 +1,142 @@
|
|
|
1
1
|
# BusinessDateCalculator
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/investtools/business_date_calculator/actions/workflows/ci.yml)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Biblioteca Ruby para cálculos com calendário de dias úteis: identificar feriados, mover datas entre dias úteis, contar dias úteis entre datas e ajustar datas não-úteis para o próximo ou anterior dia útil.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Pensada para casos de uso financeiros (cotização e liquidação de fundos, D+N, dias úteis no calendário ANBIMA/B3), mas sem dependências de feriados específicos — você fornece a lista.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## Instalação
|
|
10
|
+
|
|
11
|
+
Adicione ao `Gemfile`:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
gem 'business_date_calculator', '~> 1.0'
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
E rode:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bundle install
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Ou instale isoladamente:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
gem install business_date_calculator
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Uso
|
|
30
|
+
|
|
31
|
+
### Criando um calendário
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
require 'business_date_calculator'
|
|
35
|
+
|
|
36
|
+
start_date = Date.parse('2024-01-01')
|
|
37
|
+
end_date = Date.parse('2024-12-31')
|
|
38
|
+
holidays = [Date.parse('2024-01-01'), Date.parse('2024-12-25')]
|
|
39
|
+
|
|
40
|
+
calendar = BusinessDateCalculator::Calendar.new(start_date, end_date, holidays)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
O calendário expande automaticamente seu range internamente quando você pergunta por datas fora do intervalo inicial — você não precisa pré-dimensionar.
|
|
44
|
+
|
|
45
|
+
### `is_holiday?(date)`
|
|
46
|
+
|
|
47
|
+
Retorna `true` para fins de semana ou datas na lista de feriados:
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
calendar.is_holiday?(Date.parse('2024-01-01')) # => true (feriado)
|
|
51
|
+
calendar.is_holiday?(Date.parse('2024-01-06')) # => true (sábado)
|
|
52
|
+
calendar.is_holiday?(Date.parse('2024-01-08')) # => false (segunda)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### `adjust(date, convention)`
|
|
56
|
+
|
|
57
|
+
Ajusta uma data não-útil para o próximo dia útil (`:following`) ou anterior (`:preceding`). Use `:unadjusted` para devolver a data sem modificar.
|
|
10
58
|
|
|
11
59
|
```ruby
|
|
12
|
-
|
|
60
|
+
calendar.adjust(Date.parse('2024-01-06'), :following) # => 2024-01-08 (segunda)
|
|
61
|
+
calendar.adjust(Date.parse('2024-01-06'), :preceding) # => 2024-01-05 (sexta)
|
|
62
|
+
calendar.adjust(Date.parse('2024-01-08'), :following) # => 2024-01-08 (já é dia útil)
|
|
13
63
|
```
|
|
14
64
|
|
|
15
|
-
|
|
65
|
+
### `advance(date, n, convention = :following)`
|
|
16
66
|
|
|
17
|
-
|
|
67
|
+
Avança `n` dias úteis a partir de `date`. Aceita `n` negativo para recuar.
|
|
18
68
|
|
|
19
|
-
|
|
69
|
+
```ruby
|
|
70
|
+
calendar.advance(Date.parse('2024-01-08'), 5) # => 2024-01-15
|
|
71
|
+
calendar.advance(Date.parse('2024-01-15'), -3) # => 2024-01-10
|
|
72
|
+
calendar.advance(Date.parse('2024-01-06'), 1) # => 2024-01-09 (sábado avança para seg + 1)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Se `date` cai num dia não-útil, ele é ajustado primeiro segundo a `convention` antes de avançar.
|
|
76
|
+
|
|
77
|
+
### `networkdays(date1, date2, convention1 = :unadjusted, convention2 = :unadjusted)`
|
|
20
78
|
|
|
21
|
-
|
|
79
|
+
Retorna a contagem de "saltos" entre dias úteis nas duas datas. Equivalente a `índice_util(date2) - índice_util(date1)`:
|
|
22
80
|
|
|
23
|
-
|
|
81
|
+
```ruby
|
|
82
|
+
mon = Date.parse('2024-01-08')
|
|
83
|
+
fri = Date.parse('2024-01-12')
|
|
84
|
+
|
|
85
|
+
calendar.networkdays(mon, fri) # => 4 (segunda→sexta: 4 saltos)
|
|
86
|
+
calendar.networkdays(mon, mon) # => 0 (mesma data)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Levanta `ArgumentError` se `date1 > date2`.**
|
|
90
|
+
|
|
91
|
+
Se as datas passadas não são úteis, especifique convenções de ajuste:
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
sat = Date.parse('2024-01-06')
|
|
95
|
+
fri = Date.parse('2024-01-12')
|
|
96
|
+
|
|
97
|
+
calendar.networkdays(sat, fri, :following, :unadjusted) # ajusta sat → mon, depois conta
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### `last_day_of_previous_month(date)`
|
|
101
|
+
|
|
102
|
+
Retorna o último dia útil do mês anterior:
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
calendar.last_day_of_previous_month(Date.parse('2024-03-15'))
|
|
106
|
+
# => 2024-02-29 (sexta — último dia útil de fevereiro)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Thread-safety
|
|
110
|
+
|
|
111
|
+
Métodos públicos são protegidos por `Monitor` (reentrante). Múltiplas threads podem chamar métodos da mesma instância concorrentemente sem corrupção de estado durante reconstruções internas.
|
|
112
|
+
|
|
113
|
+
## Marshal / Rails.cache
|
|
114
|
+
|
|
115
|
+
A classe implementa `marshal_dump` / `marshal_load` para funcionar com `Rails.cache.fetch` e outros consumidores que serializam via `Marshal`. O `Monitor` interno é recriado fresh na desserialização.
|
|
116
|
+
|
|
117
|
+
## Desenvolvimento
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
bundle install
|
|
121
|
+
bundle exec rspec # roda testes
|
|
122
|
+
bundle exec rubocop # lint
|
|
123
|
+
bundle exec bundler-audit # security audit das deps
|
|
124
|
+
```
|
|
24
125
|
|
|
25
|
-
|
|
126
|
+
## Release
|
|
26
127
|
|
|
27
|
-
|
|
128
|
+
1. Atualize `lib/business_date_calculator/version.rb`
|
|
129
|
+
2. Atualize `CHANGELOG.md`
|
|
130
|
+
3. `bundle exec rake release` — cria tag git, faz push e publica no rubygems.org
|
|
28
131
|
|
|
29
|
-
|
|
132
|
+
## Contribuindo
|
|
30
133
|
|
|
31
|
-
|
|
134
|
+
1. Fork
|
|
135
|
+
2. `git checkout -b minha-feature`
|
|
136
|
+
3. Adicione testes pra mudança (TDD recomendado)
|
|
137
|
+
4. Garanta `bundle exec rspec` e `bundle exec rubocop` passando
|
|
138
|
+
5. Pull Request pra `master`
|
|
32
139
|
|
|
33
|
-
##
|
|
140
|
+
## License
|
|
34
141
|
|
|
35
|
-
|
|
36
|
-
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
37
|
-
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
38
|
-
4. Push to the branch (`git push origin my-new-feature`)
|
|
39
|
-
5. Create a new Pull Request
|
|
142
|
+
MIT. Veja [LICENSE.txt](LICENSE.txt) (se ausente, MIT padrão).
|
data/Rakefile
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
require
|
|
2
|
-
|
|
1
|
+
require 'bundler/gem_tasks'
|
|
@@ -18,9 +18,10 @@ Gem::Specification.new do |spec|
|
|
|
18
18
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
19
19
|
spec.require_paths = ["lib"]
|
|
20
20
|
|
|
21
|
-
spec.add_development_dependency "bundler", "
|
|
22
|
-
spec.add_development_dependency "
|
|
23
|
-
spec.add_development_dependency "
|
|
24
|
-
spec.add_development_dependency "
|
|
21
|
+
spec.add_development_dependency "bundler", ">= 2.0"
|
|
22
|
+
spec.add_development_dependency "bundler-audit", "~> 0.9"
|
|
23
|
+
spec.add_development_dependency "rake", ">= 12.0"
|
|
24
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
|
25
|
+
spec.add_development_dependency "rubocop", "~> 1.60"
|
|
25
26
|
spec.add_dependency "activesupport"
|
|
26
27
|
end
|
|
@@ -1,79 +1,175 @@
|
|
|
1
|
+
require 'monitor'
|
|
2
|
+
|
|
1
3
|
module BusinessDateCalculator
|
|
4
|
+
# Calculadora de dias uteis com calendario customizavel de feriados.
|
|
5
|
+
#
|
|
6
|
+
# Mantem uma estrutura de dados indexada cobrindo um intervalo de datas, expandida
|
|
7
|
+
# dinamicamente quando consultas saem do range inicial. Thread-safe (Monitor reentrante)
|
|
8
|
+
# e Marshal-friendly para uso com Rails.cache.
|
|
9
|
+
#
|
|
10
|
+
# @example Uso basico
|
|
11
|
+
# holidays = [Date.parse('2024-01-01'), Date.parse('2024-12-25')]
|
|
12
|
+
# cal = BusinessDateCalculator::Calendar.new(Date.parse('2024-01-01'), Date.parse('2024-12-31'), holidays)
|
|
13
|
+
# cal.advance(Date.parse('2024-01-08'), 5) # => 2024-01-15
|
|
2
14
|
class Calendar
|
|
3
|
-
|
|
4
|
-
#
|
|
5
|
-
#
|
|
15
|
+
# Cria um novo calendario.
|
|
16
|
+
#
|
|
17
|
+
# @param start_date [Date] data inicial do range (sera ajustada para o dia util anterior se nao for util)
|
|
18
|
+
# @param end_date [Date] data final do range (sera ajustada para o proximo dia util se nao for util)
|
|
19
|
+
# @param holidays [Array<Date>] lista de feriados a considerar como nao-uteis (dup.freeze interno)
|
|
6
20
|
def initialize(start_date, end_date, holidays)
|
|
21
|
+
@monitor = Monitor.new
|
|
7
22
|
build(start_date, end_date, holidays)
|
|
8
23
|
end
|
|
9
24
|
|
|
25
|
+
# Verifica se uma data nao e util (fim de semana ou feriado).
|
|
26
|
+
#
|
|
27
|
+
# @param date [Date] data a verificar
|
|
28
|
+
# @return [Boolean] true se for sabado, domingo ou estiver na lista de feriados
|
|
10
29
|
def is_holiday?(date)
|
|
11
|
-
date.wday
|
|
30
|
+
@monitor.synchronize { date.wday.zero? || date.wday == 6 || @holidays.include?(date) }
|
|
12
31
|
end
|
|
13
32
|
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
33
|
+
# Conta dias uteis entre duas datas como "saltos" no indice de dias uteis.
|
|
34
|
+
# Equivalente a +indice_util(date2) - indice_util(date1)+: mesma data retorna 0,
|
|
35
|
+
# segunda-feira ate sexta-feira da mesma semana retorna 4.
|
|
36
|
+
#
|
|
37
|
+
# @param date1 [Date] data inicial (deve ser menor ou igual a date2)
|
|
38
|
+
# @param date2 [Date] data final
|
|
39
|
+
# @param convention1 [Symbol] convencao de ajuste para date1: +:unadjusted+, +:following+, +:preceding+
|
|
40
|
+
# @param convention2 [Symbol] convencao de ajuste para date2
|
|
41
|
+
# @return [Integer] numero de saltos entre dias uteis
|
|
42
|
+
# @raise [ArgumentError] quando date1 > date2
|
|
43
|
+
#
|
|
44
|
+
# @example
|
|
45
|
+
# cal.networkdays(Date.parse('2024-01-08'), Date.parse('2024-01-12')) # => 4
|
|
18
46
|
def networkdays(date1, date2, convention1 = :unadjusted, convention2 = :unadjusted)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
47
|
+
if date1 > date2
|
|
48
|
+
raise ArgumentError,
|
|
49
|
+
"date1 must be less than or equal to date2 (got date1=#{date1}, date2=#{date2})"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
@monitor.synchronize do
|
|
53
|
+
range_check(date1)
|
|
54
|
+
range_check(date2)
|
|
55
|
+
i1 = adjusted_date_index(date1, convention1)
|
|
56
|
+
i2 = adjusted_date_index(date2, convention2)
|
|
57
|
+
raise "Adjusted date1 #{date1} is out of range" if i1.nil?
|
|
58
|
+
raise "Adjusted date2 #{date2} is out of range" if i2.nil?
|
|
59
|
+
|
|
60
|
+
i2 - i1
|
|
61
|
+
end
|
|
26
62
|
end
|
|
27
63
|
|
|
64
|
+
# Ajusta uma data para o dia util mais proximo segundo a convencao indicada.
|
|
65
|
+
# Se a data ja for util, retorna ela inalterada independente da convencao.
|
|
66
|
+
#
|
|
67
|
+
# @param date [Date] data a ajustar
|
|
68
|
+
# @param convention [Symbol] +:following+ (proximo dia util), +:preceding+ (anterior),
|
|
69
|
+
# ou +:unadjusted+ (devolve a data sem alteracao)
|
|
70
|
+
# @return [Date] data ajustada
|
|
71
|
+
# @raise [RuntimeError] +:preceding+ quando nao ha dia util anterior conhecido
|
|
72
|
+
#
|
|
73
|
+
# @example
|
|
74
|
+
# cal.adjust(Date.parse('2024-01-06'), :following) # => 2024-01-08 (sabado -> segunda)
|
|
28
75
|
def adjust(date, convention)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
date
|
|
32
|
-
|
|
33
|
-
date
|
|
34
|
-
else
|
|
76
|
+
@monitor.synchronize do
|
|
77
|
+
range_check(date)
|
|
78
|
+
return date if !is_holiday?(date) || convention == :unadjusted
|
|
79
|
+
|
|
35
80
|
case convention
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
81
|
+
when :following
|
|
82
|
+
@business_dates[@next_business_date_index[date]]
|
|
83
|
+
when :preceding
|
|
84
|
+
raise "Erro pegando data util anterior ao dia #{date}" if @prev_business_date_index[date].nil?
|
|
85
|
+
|
|
86
|
+
@business_dates[@prev_business_date_index[date]]
|
|
41
87
|
end
|
|
42
88
|
end
|
|
43
89
|
end
|
|
44
90
|
|
|
91
|
+
# Avanca (ou recua) +n+ dias uteis a partir de +date+. Expande o calendario
|
|
92
|
+
# automaticamente quando +n+ extrapola o range conhecido.
|
|
93
|
+
#
|
|
94
|
+
# @param date [Date, #to_date] data de partida
|
|
95
|
+
# @param n [Integer] numero de dias uteis a avancar (negativo para recuar)
|
|
96
|
+
# @param convention [Symbol] convencao para ajustar +date+ caso ela seja nao-util
|
|
97
|
+
# @param margin [Integer] folga em dias corridos para expansao do calendario (uso interno em recursao)
|
|
98
|
+
# @return [Date] dia util resultante
|
|
99
|
+
#
|
|
100
|
+
# @example Avancar 5 dias uteis
|
|
101
|
+
# cal.advance(Date.parse('2024-01-08'), 5) # => 2024-01-15
|
|
102
|
+
#
|
|
103
|
+
# @example Recuar 3 dias uteis
|
|
104
|
+
# cal.advance(Date.parse('2024-01-15'), -3) # => 2024-01-10
|
|
45
105
|
def advance(date, n, convention = :following, margin = 30)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
106
|
+
@monitor.synchronize do
|
|
107
|
+
date = date.to_date
|
|
108
|
+
range_check(date)
|
|
109
|
+
index = adjusted_date_index(date, convention) + n
|
|
110
|
+
if index.negative?
|
|
111
|
+
# 2x folga sobre dias uteis cobre fins de semana e feriados em uma unica reconstrucao
|
|
112
|
+
build(date + ((index * 2) - margin).days, @end_date, @holidays)
|
|
113
|
+
return advance(date, n, convention, margin + 30)
|
|
114
|
+
elsif index >= @business_dates.length
|
|
115
|
+
overshoot = index - @business_dates.length + 1
|
|
116
|
+
build(@start_date, @end_date + ((overshoot * 2) + margin).days, @holidays)
|
|
117
|
+
return advance(date, n, convention, margin + 30)
|
|
118
|
+
end
|
|
119
|
+
@business_dates[adjusted_date_index(date, convention) + n]
|
|
52
120
|
end
|
|
53
|
-
@business_dates[adjusted_date_index(date, convention) + n]
|
|
54
121
|
end
|
|
55
122
|
|
|
123
|
+
# Ultimo dia util do mes anterior ao da data passada.
|
|
124
|
+
#
|
|
125
|
+
# @param date [Date] data de referencia
|
|
126
|
+
# @return [Date] ultimo dia util do mes anterior (com ajuste +:preceding+ se for nao-util)
|
|
127
|
+
#
|
|
128
|
+
# @example
|
|
129
|
+
# cal.last_day_of_previous_month(Date.parse('2024-03-15')) # => 2024-02-29
|
|
56
130
|
def last_day_of_previous_month(date)
|
|
57
|
-
|
|
58
|
-
y = date.year
|
|
59
|
-
if m == 1
|
|
60
|
-
m = 0
|
|
61
|
-
y = y - 1
|
|
62
|
-
end
|
|
63
|
-
adjust(Date.civil(y, (m - 1), -1), :preceding)
|
|
131
|
+
@monitor.synchronize { adjust(Date.civil(date.year, date.month, 1) - 1, :preceding) }
|
|
64
132
|
end
|
|
65
133
|
|
|
134
|
+
# @!group Marshal serialization
|
|
135
|
+
|
|
136
|
+
# Monitor nao e serializavel via Marshal (Rails.cache usa Marshal). Pula o monitor
|
|
137
|
+
# na serializacao e recria fresh na deserializacao.
|
|
138
|
+
# @api private
|
|
139
|
+
def marshal_dump
|
|
140
|
+
{
|
|
141
|
+
start_date: @start_date,
|
|
142
|
+
end_date: @end_date,
|
|
143
|
+
holidays: @holidays,
|
|
144
|
+
business_dates: @business_dates,
|
|
145
|
+
business_date_index: @business_date_index,
|
|
146
|
+
next_business_date_index: @next_business_date_index,
|
|
147
|
+
prev_business_date_index: @prev_business_date_index
|
|
148
|
+
}
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# @api private
|
|
152
|
+
def marshal_load(data)
|
|
153
|
+
@monitor = Monitor.new
|
|
154
|
+
@start_date = data[:start_date]
|
|
155
|
+
@end_date = data[:end_date]
|
|
156
|
+
@holidays = data[:holidays]
|
|
157
|
+
@business_dates = data[:business_dates]
|
|
158
|
+
@business_date_index = data[:business_date_index]
|
|
159
|
+
@next_business_date_index = data[:next_business_date_index]
|
|
160
|
+
@prev_business_date_index = data[:prev_business_date_index]
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# @!endgroup
|
|
164
|
+
|
|
66
165
|
protected
|
|
67
166
|
|
|
68
167
|
def build(start_date, end_date, holidays)
|
|
168
|
+
holidays = holidays.dup.freeze
|
|
69
169
|
# garante que start_date e end_date sao dias uteis
|
|
70
|
-
while start_date.wday
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
while end_date.wday == 0 || start_date.wday == 6 || holidays.include?(end_date) do
|
|
74
|
-
end_date += 1.days
|
|
75
|
-
end
|
|
76
|
-
|
|
170
|
+
start_date -= 1.days while start_date.wday.zero? || start_date.wday == 6 || holidays.include?(start_date)
|
|
171
|
+
end_date += 1.days while end_date.wday.zero? || end_date.wday == 6 || holidays.include?(end_date)
|
|
172
|
+
|
|
77
173
|
@start_date = start_date
|
|
78
174
|
@end_date = end_date
|
|
79
175
|
@holidays = holidays
|
|
@@ -81,10 +177,10 @@ module BusinessDateCalculator
|
|
|
81
177
|
@business_date_index = {}
|
|
82
178
|
@next_business_date_index = {}
|
|
83
179
|
@prev_business_date_index = {}
|
|
84
|
-
|
|
180
|
+
|
|
85
181
|
d = start_date
|
|
86
182
|
i = 0
|
|
87
|
-
while d <= end_date
|
|
183
|
+
while d <= end_date
|
|
88
184
|
if is_holiday?(d)
|
|
89
185
|
# dia não útil, mapeia o indice do dia util anterior e proximo
|
|
90
186
|
@next_business_date_index[d] = i
|
|
@@ -93,25 +189,27 @@ module BusinessDateCalculator
|
|
|
93
189
|
# dia util, adiciona ao final do array, e mapeia o indice do array no mapa
|
|
94
190
|
@business_dates << d
|
|
95
191
|
@business_date_index[d] = i
|
|
96
|
-
i
|
|
192
|
+
i += 1
|
|
97
193
|
end
|
|
98
194
|
d += 1.days
|
|
99
195
|
end
|
|
100
196
|
end
|
|
101
|
-
|
|
197
|
+
|
|
198
|
+
EXPANSION_MARGIN_DAYS = 252
|
|
199
|
+
|
|
102
200
|
# Verifica se a data passada esta entre o periodo desta instancia.
|
|
201
|
+
# Quando fora do range, reconstroi simetricamente com EXPANSION_MARGIN_DAYS dias de folga
|
|
202
|
+
# para evitar rebuilds em sequencia em consultas em batch.
|
|
103
203
|
def range_check(date)
|
|
104
204
|
if date < @start_date
|
|
105
|
-
|
|
106
|
-
build(date - 2.days, @end_date, @holidays)
|
|
205
|
+
build(date - EXPANSION_MARGIN_DAYS.days, @end_date, @holidays)
|
|
107
206
|
elsif date > @end_date
|
|
108
|
-
|
|
109
|
-
build(@start_date, date + 252.days, @holidays)
|
|
207
|
+
build(@start_date, date + EXPANSION_MARGIN_DAYS.days, @holidays)
|
|
110
208
|
end
|
|
111
209
|
end
|
|
112
|
-
|
|
210
|
+
|
|
113
211
|
def adjusted_date_index(date, convention)
|
|
114
212
|
@business_date_index[adjust(date, convention)]
|
|
115
213
|
end
|
|
116
214
|
end
|
|
117
|
-
end
|
|
215
|
+
end
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
require
|
|
2
|
-
require
|
|
1
|
+
require 'business_date_calculator/version'
|
|
2
|
+
require 'business_date_calculator/calendar'
|
metadata
CHANGED
|
@@ -1,71 +1,84 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: business_date_calculator
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lucas Pérez
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: bundler
|
|
15
14
|
requirement: !ruby/object:Gem::Requirement
|
|
16
15
|
requirements:
|
|
17
|
-
- - "
|
|
16
|
+
- - ">="
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '
|
|
18
|
+
version: '2.0'
|
|
20
19
|
type: :development
|
|
21
20
|
prerelease: false
|
|
22
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
22
|
requirements:
|
|
24
|
-
- - "
|
|
23
|
+
- - ">="
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '
|
|
25
|
+
version: '2.0'
|
|
27
26
|
- !ruby/object:Gem::Dependency
|
|
28
|
-
name:
|
|
27
|
+
name: bundler-audit
|
|
29
28
|
requirement: !ruby/object:Gem::Requirement
|
|
30
29
|
requirements:
|
|
31
30
|
- - "~>"
|
|
32
31
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: '
|
|
32
|
+
version: '0.9'
|
|
34
33
|
type: :development
|
|
35
34
|
prerelease: false
|
|
36
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
36
|
requirements:
|
|
38
37
|
- - "~>"
|
|
39
38
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: '
|
|
39
|
+
version: '0.9'
|
|
41
40
|
- !ruby/object:Gem::Dependency
|
|
42
|
-
name:
|
|
41
|
+
name: rake
|
|
43
42
|
requirement: !ruby/object:Gem::Requirement
|
|
44
43
|
requirements:
|
|
45
44
|
- - ">="
|
|
46
45
|
- !ruby/object:Gem::Version
|
|
47
|
-
version: '0'
|
|
46
|
+
version: '12.0'
|
|
48
47
|
type: :development
|
|
49
48
|
prerelease: false
|
|
50
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
50
|
requirements:
|
|
52
51
|
- - ">="
|
|
53
52
|
- !ruby/object:Gem::Version
|
|
54
|
-
version: '0'
|
|
53
|
+
version: '12.0'
|
|
55
54
|
- !ruby/object:Gem::Dependency
|
|
56
|
-
name:
|
|
55
|
+
name: rspec
|
|
57
56
|
requirement: !ruby/object:Gem::Requirement
|
|
58
57
|
requirements:
|
|
59
|
-
- - "
|
|
58
|
+
- - "~>"
|
|
60
59
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: '0'
|
|
60
|
+
version: '3.0'
|
|
62
61
|
type: :development
|
|
63
62
|
prerelease: false
|
|
64
63
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
64
|
requirements:
|
|
66
|
-
- - "
|
|
65
|
+
- - "~>"
|
|
67
66
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: '0'
|
|
67
|
+
version: '3.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rubocop
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '1.60'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '1.60'
|
|
69
82
|
- !ruby/object:Gem::Dependency
|
|
70
83
|
name: activesupport
|
|
71
84
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -87,9 +100,11 @@ executables: []
|
|
|
87
100
|
extensions: []
|
|
88
101
|
extra_rdoc_files: []
|
|
89
102
|
files:
|
|
103
|
+
- ".github/workflows/ci.yml"
|
|
90
104
|
- ".gitignore"
|
|
91
105
|
- ".rspec"
|
|
92
|
-
- ".
|
|
106
|
+
- ".rubocop.yml"
|
|
107
|
+
- CHANGELOG.md
|
|
93
108
|
- Gemfile
|
|
94
109
|
- Guardfile
|
|
95
110
|
- README.md
|
|
@@ -104,7 +119,6 @@ homepage: https://github.com/investtools/business_date_calculator
|
|
|
104
119
|
licenses:
|
|
105
120
|
- MIT
|
|
106
121
|
metadata: {}
|
|
107
|
-
post_install_message:
|
|
108
122
|
rdoc_options: []
|
|
109
123
|
require_paths:
|
|
110
124
|
- lib
|
|
@@ -119,9 +133,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
119
133
|
- !ruby/object:Gem::Version
|
|
120
134
|
version: '0'
|
|
121
135
|
requirements: []
|
|
122
|
-
|
|
123
|
-
rubygems_version: 2.6.8
|
|
124
|
-
signing_key:
|
|
136
|
+
rubygems_version: 3.6.9
|
|
125
137
|
specification_version: 4
|
|
126
138
|
summary: A Ruby Library for dealing with business calendar.
|
|
127
139
|
test_files: []
|
data/.travis.yml
DELETED