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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 76736416593db5462eed799dc6f31ef5522b7f30
4
- data.tar.gz: 20a36007d246e1b449e66e456b3a0242dcdfddcb
2
+ SHA256:
3
+ metadata.gz: 9c6c73d127ef04d88448a0c97d29827023f527fcb6c02f3beaec8385c8846850
4
+ data.tar.gz: 323d8ca926b59cbb69987ee2b7447e5350dc39cd75a60427c254c3b678253e9e
5
5
  SHA512:
6
- metadata.gz: af830900695fbaa8c0f27b15062cd49c0199e993ddae8c1a08faef4cfc6851c18e3e0d36c70ed8c3a141cdee3a1ba76e905ae17fc18b7db9dfc2ea23e9ad1be0
7
- data.tar.gz: ebd51df76d57938987f1cea71d415dbac1e71929e9fd7c944b951b388444b6a68fa0b3d4be6778c3fed3a1e01dfd594f544d299138a0b7215082900e2da2c9f0
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
@@ -1,4 +1,4 @@
1
1
  source 'https://rubygems.org'
2
- ruby '2.4.0'
2
+
3
3
  # Specify your gem's dependencies in business_date_calculator.gemspec
4
4
  gemspec
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
- # Note: The cmd option is now required due to the increasing number of ways
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: "bundle exec rspec" do
36
- require "guard/rspec/dsl"
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(erb haml slim))
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) { |m| rspec.spec.("features/#{m[1]}") }
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] || "spec/acceptance"
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
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/business_date_calculator`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ [![CI](https://github.com/investtools/business_date_calculator/actions/workflows/ci.yml/badge.svg)](https://github.com/investtools/business_date_calculator/actions/workflows/ci.yml)
4
4
 
5
- TODO: Delete this and the text above, and describe your gem
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
- ## Installation
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
- Add this line to your application's Gemfile:
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
- gem 'business_date_calculator'
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
- And then execute:
65
+ ### `advance(date, n, convention = :following)`
16
66
 
17
- $ bundle
67
+ Avança `n` dias úteis a partir de `date`. Aceita `n` negativo para recuar.
18
68
 
19
- Or install it yourself as:
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
- $ gem install business_date_calculator
79
+ Retorna a contagem de "saltos" entre dias úteis nas duas datas. Equivalente a `índice_util(date2) - índice_util(date1)`:
22
80
 
23
- ## Usage
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
- TODO: Write usage instructions here
126
+ ## Release
26
127
 
27
- ## Development
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
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment.
132
+ ## Contribuindo
30
133
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
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
- ## Contributing
140
+ ## License
34
141
 
35
- 1. Fork it ( https://github.com/[my-github-username]/business_date_calculator/fork )
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 "bundler/gem_tasks"
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", "~> 1.8"
22
- spec.add_development_dependency "rake", "~> 10.0"
23
- spec.add_development_dependency "guard-rspec"
24
- spec.add_development_dependency "terminal-notifier-guard"
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
- # Constroi a estrutura de dados a partir de uma lista de holidays que
5
- # sao os feriados, e usando um periodo especificado (fechado nas duas pontas).
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 == 0 || date.wday == 6 || @holidays.include?(date)
30
+ @monitor.synchronize { date.wday.zero? || date.wday == 6 || @holidays.include?(date) }
12
31
  end
13
32
 
14
- # Retorna o numero de dias uteis entre as duas data especificadas, inclusive.
15
- # As duas datas devem ser dias uteis, ou caso não seja, deve ser
16
- # especificado uma convenção de ajuste para cada data.
17
- # A primeira data deve ser menor ou igual a segunda.
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
- range_check(date1)
20
- range_check(date2)
21
- i1 = adjusted_date_index(date1, convention1)
22
- i2 = adjusted_date_index(date2, convention2)
23
- raise "Adjusted date1 #{date1} is out of range" if i1 == nil
24
- raise "Adjusted date2 #{date2} is out of range" if i2 == nil
25
- i2 - i1
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
- range_check(date)
30
- if not is_holiday?(date)
31
- date
32
- elsif convention == :unadjusted
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
- when :following
37
- @business_dates[@next_business_date_index[date]]
38
- when :preceding
39
- raise "Erro pegando data util anterior ao dia #{date}" if @prev_business_date_index[date] == nil
40
- @business_dates[@prev_business_date_index[date]]
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
- date = date.to_date
47
- range_check(date)
48
- index = adjusted_date_index(date, convention) + n
49
- if index < 0
50
- build(date + (index - margin).days, @end_date, @holidays)
51
- return advance(date, n, convention, margin + 30)
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
- m = date.month
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 == 0 || start_date.wday == 6 || holidays.include?(start_date) do
71
- start_date -= 1.days
72
- end
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 do
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 = i + 1
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
- # puts "Reconstruindo calculadora de feriados pois dia #{date} eh menor que #{@start_date} -> #{@end_date}"
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
- # puts "Reconstruindo calculadora de feriados pois dia #{date} eh maior que #{end_date}"
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,3 +1,3 @@
1
1
  module BusinessDateCalculator
2
- VERSION = "0.1.5"
2
+ VERSION = '1.0.0'.freeze
3
3
  end
@@ -1,2 +1,2 @@
1
- require "business_date_calculator/version"
2
- require "business_date_calculator/calendar"
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.1.5
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: 2018-11-26 00:00:00.000000000 Z
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: '1.8'
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: '1.8'
25
+ version: '2.0'
27
26
  - !ruby/object:Gem::Dependency
28
- name: rake
27
+ name: bundler-audit
29
28
  requirement: !ruby/object:Gem::Requirement
30
29
  requirements:
31
30
  - - "~>"
32
31
  - !ruby/object:Gem::Version
33
- version: '10.0'
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: '10.0'
39
+ version: '0.9'
41
40
  - !ruby/object:Gem::Dependency
42
- name: guard-rspec
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: terminal-notifier-guard
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
- - ".travis.yml"
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
- rubyforge_project:
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
@@ -1,3 +0,0 @@
1
- language: ruby
2
- rvm:
3
- - 2.1.2