Class: BusinessDateCalculator::Calendar

Inherits:
Object
  • Object
show all
Defined in:
lib/business_date_calculator/calendar.rb

Overview

Calculadora de dias uteis com calendario customizavel de feriados.

Mantem uma estrutura de dados indexada cobrindo um intervalo de datas, expandida dinamicamente quando consultas saem do range inicial. Thread-safe (Monitor reentrante) e Marshal-friendly para uso com Rails.cache.

Examples:

Uso basico

holidays = [Date.parse('2024-01-01'), Date.parse('2024-12-25')]
cal = BusinessDateCalculator::Calendar.new(Date.parse('2024-01-01'), Date.parse('2024-12-31'), holidays)
cal.advance(Date.parse('2024-01-08'), 5)   # => 2024-01-15

Marshal serialization collapse

Instance Method Summary collapse

Constructor Details

#initialize(start_date, end_date, holidays) ⇒ Calendar

Cria um novo calendario.

Parameters:

  • start_date (Date)

    data inicial do range (sera ajustada para o dia util anterior se nao for util)

  • end_date (Date)

    data final do range (sera ajustada para o proximo dia util se nao for util)

  • holidays (Array<Date>)

    lista de feriados a considerar como nao-uteis (dup.freeze interno)



20
21
22
23
# File 'lib/business_date_calculator/calendar.rb', line 20

def initialize(start_date, end_date, holidays)
  @monitor = Monitor.new
  build(start_date, end_date, holidays)
end

Instance Method Details

#adjust(date, convention) ⇒ Date

Ajusta uma data para o dia util mais proximo segundo a convencao indicada. Se a data ja for util, retorna ela inalterada independente da convencao.

Examples:

cal.adjust(Date.parse('2024-01-06'), :following)  # => 2024-01-08 (sabado -> segunda)

Parameters:

  • date (Date)

    data a ajustar

  • convention (Symbol)

    :following (proximo dia util), :preceding (anterior), ou :unadjusted (devolve a data sem alteracao)

Returns:

  • (Date)

    data ajustada

Raises:

  • (RuntimeError)

    :preceding quando nao ha dia util anterior conhecido



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/business_date_calculator/calendar.rb', line 75

def adjust(date, convention)
  @monitor.synchronize do
    range_check(date)
    return date if !is_holiday?(date) || convention == :unadjusted

    case convention
    when :following
      @business_dates[@next_business_date_index[date]]
    when :preceding
      raise "Erro pegando data util anterior ao dia #{date}" if @prev_business_date_index[date].nil?

      @business_dates[@prev_business_date_index[date]]
    end
  end
end

#advance(date, n, convention = :following, margin = 30) ⇒ Date

Avanca (ou recua) n dias uteis a partir de date. Expande o calendario automaticamente quando n extrapola o range conhecido.

Examples:

Avancar 5 dias uteis

cal.advance(Date.parse('2024-01-08'), 5)  # => 2024-01-15

Recuar 3 dias uteis

cal.advance(Date.parse('2024-01-15'), -3)  # => 2024-01-10

Parameters:

  • date (Date, #to_date)

    data de partida

  • n (Integer)

    numero de dias uteis a avancar (negativo para recuar)

  • convention (Symbol) (defaults to: :following)

    convencao para ajustar date caso ela seja nao-util

  • margin (Integer) (defaults to: 30)

    folga em dias corridos para expansao do calendario (uso interno em recursao)

Returns:

  • (Date)

    dia util resultante



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/business_date_calculator/calendar.rb', line 105

def advance(date, n, convention = :following, margin = 30)
  @monitor.synchronize do
    date = date.to_date
    range_check(date)
    index = adjusted_date_index(date, convention) + n
    if index.negative?
      # 2x folga sobre dias uteis cobre fins de semana e feriados em uma unica reconstrucao
      build(date + ((index * 2) - margin).days, @end_date, @holidays)
      return advance(date, n, convention, margin + 30)
    elsif index >= @business_dates.length
      overshoot = index - @business_dates.length + 1
      build(@start_date, @end_date + ((overshoot * 2) + margin).days, @holidays)
      return advance(date, n, convention, margin + 30)
    end
    @business_dates[adjusted_date_index(date, convention) + n]
  end
end

#is_holiday?(date) ⇒ Boolean

Verifica se uma data nao e util (fim de semana ou feriado).

Parameters:

  • date (Date)

    data a verificar

Returns:

  • (Boolean)

    true se for sabado, domingo ou estiver na lista de feriados



29
30
31
# File 'lib/business_date_calculator/calendar.rb', line 29

def is_holiday?(date)
  @monitor.synchronize { date.wday.zero? || date.wday == 6 || @holidays.include?(date) }
end

#last_day_of_previous_month(date) ⇒ Date

Ultimo dia util do mes anterior ao da data passada.

Examples:

cal.last_day_of_previous_month(Date.parse('2024-03-15'))  # => 2024-02-29

Parameters:

  • date (Date)

    data de referencia

Returns:

  • (Date)

    ultimo dia util do mes anterior (com ajuste :preceding se for nao-util)



130
131
132
# File 'lib/business_date_calculator/calendar.rb', line 130

def last_day_of_previous_month(date)
  @monitor.synchronize { adjust(Date.civil(date.year, date.month, 1) - 1, :preceding) }
end

#marshal_dumpObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Monitor nao e serializavel via Marshal (Rails.cache usa Marshal). Pula o monitor na serializacao e recria fresh na deserializacao.



139
140
141
142
143
144
145
146
147
148
149
# File 'lib/business_date_calculator/calendar.rb', line 139

def marshal_dump
  {
    start_date: @start_date,
    end_date: @end_date,
    holidays: @holidays,
    business_dates: @business_dates,
    business_date_index: @business_date_index,
    next_business_date_index: @next_business_date_index,
    prev_business_date_index: @prev_business_date_index
  }
end

#marshal_load(data) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



152
153
154
155
156
157
158
159
160
161
# File 'lib/business_date_calculator/calendar.rb', line 152

def marshal_load(data)
  @monitor = Monitor.new
  @start_date = data[:start_date]
  @end_date = data[:end_date]
  @holidays = data[:holidays]
  @business_dates = data[:business_dates]
  @business_date_index = data[:business_date_index]
  @next_business_date_index = data[:next_business_date_index]
  @prev_business_date_index = data[:prev_business_date_index]
end

#networkdays(date1, date2, convention1 = :unadjusted, convention2 = :unadjusted) ⇒ Integer

Conta dias uteis entre duas datas como “saltos” no indice de dias uteis. Equivalente a indice_util(date2) - indice_util(date1): mesma data retorna 0, segunda-feira ate sexta-feira da mesma semana retorna 4.

Examples:

cal.networkdays(Date.parse('2024-01-08'), Date.parse('2024-01-12'))  # => 4

Parameters:

  • date1 (Date)

    data inicial (deve ser menor ou igual a date2)

  • date2 (Date)

    data final

  • convention1 (Symbol) (defaults to: :unadjusted)

    convencao de ajuste para date1: :unadjusted, :following, :preceding

  • convention2 (Symbol) (defaults to: :unadjusted)

    convencao de ajuste para date2

Returns:

  • (Integer)

    numero de saltos entre dias uteis

Raises:

  • (ArgumentError)

    quando date1 > date2



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/business_date_calculator/calendar.rb', line 46

def networkdays(date1, date2, convention1 = :unadjusted, convention2 = :unadjusted)
  if date1 > date2
    raise ArgumentError,
          "date1 must be less than or equal to date2 (got date1=#{date1}, date2=#{date2})"
  end

  @monitor.synchronize do
    range_check(date1)
    range_check(date2)
    i1 = adjusted_date_index(date1, convention1)
    i2 = adjusted_date_index(date2, convention2)
    raise "Adjusted date1 #{date1} is out of range"  if i1.nil?
    raise "Adjusted date2 #{date2} is out of range"  if i2.nil?

    i2 - i1
  end
end