Module: Philiprehberger::DateKit

Defined in:
lib/philiprehberger/date_kit.rb,
lib/philiprehberger/date_kit/version.rb

Defined Under Namespace

Classes: Error

Constant Summary collapse

VERSION =
'0.4.0'

Class Method Summary collapse

Class Method Details

.add_business_days(date, days, holidays: []) ⇒ Date

Add business days to a date, skipping weekends and holidays

Parameters:

  • date (Date)

    the starting date

  • days (Integer)

    number of business days to add

  • holidays (Array<Date>) (defaults to: [])

    optional list of holiday dates to skip

Returns:

  • (Date)

    the resulting date

Raises:



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/philiprehberger/date_kit.rb', line 36

def self.add_business_days(date, days, holidays: [])
  date = coerce_date(date)
  holidays = holidays.map { |h| coerce_date(h) }

  raise Error, 'days must be an integer' unless days.is_a?(Integer)

  direction = days.positive? ? 1 : -1
  remaining = days.abs
  current = date

  while remaining.positive?
    current += direction
    next if weekend?(current) || holidays.include?(current)

    remaining -= 1
  end

  current
end

.beginning_of_quarter(date) ⇒ Date

Return the first day of the quarter containing the given date

Parameters:

  • date (Date)

    the input date

Returns:

  • (Date)

    the first day of the quarter



60
61
62
63
64
# File 'lib/philiprehberger/date_kit.rb', line 60

def self.beginning_of_quarter(date)
  date = coerce_date(date)
  quarter_month = (((date.month - 1) / 3) * 3) + 1
  Date.new(date.year, quarter_month, 1)
end

.business_day?(date, holidays: []) ⇒ Boolean

Check if a date is a business day (not a weekend and not a holiday)

Parameters:

  • date (Date)

    the date to check

  • holidays (Array<Date>) (defaults to: [])

    optional list of holiday dates

Returns:

  • (Boolean)


207
208
209
210
211
# File 'lib/philiprehberger/date_kit.rb', line 207

def self.business_day?(date, holidays: [])
  date = coerce_date(date)
  holidays = holidays.map { |h| coerce_date(h) }
  !weekend?(date) && !holidays.include?(date)
end

.business_days_between(start_date, finish_date) ⇒ Integer

Count business days (Mon-Fri) between two dates, exclusive of both endpoints

Parameters:

  • start_date (Date)

    the start date

  • finish_date (Date)

    the end date

Returns:

  • (Integer)

    number of business days between the dates



15
16
17
18
19
20
21
22
23
24
25
26
27
28
# File 'lib/philiprehberger/date_kit.rb', line 15

def self.business_days_between(start_date, finish_date)
  start_date = coerce_date(start_date)
  finish_date = coerce_date(finish_date)

  return 0 if start_date >= finish_date

  count = 0
  current = start_date + 1
  while current < finish_date
    count += 1 unless weekend?(current)
    current += 1
  end
  count
end

.business_days_in_month(date, holidays: []) ⇒ Array<Date>

Return all business days in the month containing the given date

Parameters:

  • date (Date)

    any date within the target month

  • holidays (Array<Date>) (defaults to: [])

    optional list of holiday dates to skip

Returns:

  • (Array<Date>)

    business days in the month



260
261
262
263
264
265
# File 'lib/philiprehberger/date_kit.rb', line 260

def self.business_days_in_month(date, holidays: [])
  date = coerce_date(date)
  first = Date.new(date.year, date.month, 1)
  last = Date.new(date.year, date.month, -1)
  business_days_in_range(first, last, holidays: holidays)
end

.business_days_in_range(start_date, finish_date, holidays: []) ⇒ Array<Date>

Return an array of business days in a date range (inclusive)

Parameters:

  • start_date (Date)

    the start date

  • finish_date (Date)

    the end date

  • holidays (Array<Date>) (defaults to: [])

    optional list of holiday dates to skip

Returns:

  • (Array<Date>)

    business days in the range



169
170
171
172
173
174
175
176
177
# File 'lib/philiprehberger/date_kit.rb', line 169

def self.business_days_in_range(start_date, finish_date, holidays: [])
  start_date = coerce_date(start_date)
  finish_date = coerce_date(finish_date)
  holidays = holidays.map { |h| coerce_date(h) }

  return [] if start_date > finish_date

  (start_date..finish_date).reject { |d| weekend?(d) || holidays.include?(d) }
end

.each_business_day(start_date, finish_date, holidays: []) {|Date| ... } ⇒ Enumerator

Iterate over business days in a date range

Parameters:

  • start_date (Date)

    the start date

  • finish_date (Date)

    the end date

  • holidays (Array<Date>) (defaults to: [])

    optional list of holiday dates to skip

Yields:

  • (Date)

    each business day in the range

Returns:

  • (Enumerator)

    if no block is given



186
187
188
189
190
191
# File 'lib/philiprehberger/date_kit.rb', line 186

def self.each_business_day(start_date, finish_date, holidays: [], &block)
  days = business_days_in_range(start_date, finish_date, holidays: holidays)
  return days.each unless block

  days.each(&block)
end

.end_of_quarter(date) ⇒ Date

Return the last day of the quarter containing the given date

Parameters:

  • date (Date)

    the input date

Returns:

  • (Date)

    the last day of the quarter



70
71
72
73
74
# File 'lib/philiprehberger/date_kit.rb', line 70

def self.end_of_quarter(date)
  date = coerce_date(date)
  quarter_month = (((date.month - 1) / 3) * 3) + 3
  Date.new(date.year, quarter_month, -1)
end

.first_business_day_of_month(date, holidays: []) ⇒ Date

Return the first business day of the month containing the given date

Parameters:

  • date (Date)

    any date within the target month

  • holidays (Array<Date>) (defaults to: [])

    optional list of holiday dates to skip

Returns:

  • (Date)

    the first business day of the month



231
232
233
234
235
236
237
# File 'lib/philiprehberger/date_kit.rb', line 231

def self.first_business_day_of_month(date, holidays: [])
  date = coerce_date(date)
  holidays = holidays.map { |h| coerce_date(h) }
  current = Date.new(date.year, date.month, 1)
  current += 1 while weekend?(current) || holidays.include?(current)
  current
end

.last_business_day_of_month(date, holidays: []) ⇒ Date

Return the last business day of the month containing the given date

Parameters:

  • date (Date)

    any date within the target month

  • holidays (Array<Date>) (defaults to: [])

    optional list of holiday dates to skip

Returns:

  • (Date)

    the last business day of the month



218
219
220
221
222
223
224
# File 'lib/philiprehberger/date_kit.rb', line 218

def self.last_business_day_of_month(date, holidays: [])
  date = coerce_date(date)
  holidays = holidays.map { |h| coerce_date(h) }
  current = Date.new(date.year, date.month, -1)
  current -= 1 while weekend?(current) || holidays.include?(current)
  current
end

.next_business_day(date, holidays: []) ⇒ Date

Return the next business day after the given date (skips weekends and holidays)

Parameters:

  • date (Date)

    the starting date

  • holidays (Array<Date>) (defaults to: [])

    optional list of holiday dates to skip

Returns:

  • (Date)

    the next business day



140
141
142
143
144
145
146
147
# File 'lib/philiprehberger/date_kit.rb', line 140

def self.next_business_day(date, holidays: [])
  date = coerce_date(date)
  holidays = holidays.map { |h| coerce_date(h) }

  current = date + 1
  current += 1 while weekend?(current) || holidays.include?(current)
  current
end

.nth_business_day_of_month(date, n, holidays: []) ⇒ Date

Return the nth business day of the month containing the given date

Parameters:

  • date (Date)

    any date within the target month

  • n (Integer)

    the 1-based ordinal (e.g., 1 for first, 5 for fifth)

  • holidays (Array<Date>) (defaults to: [])

    optional list of holiday dates to skip

Returns:

  • (Date)

    the nth business day of the month

Raises:

  • (Error)

    if n is not a positive integer or exceeds the number of business days in the month



246
247
248
249
250
251
252
253
# File 'lib/philiprehberger/date_kit.rb', line 246

def self.nth_business_day_of_month(date, n, holidays: [])
  raise Error, 'n must be a positive integer' unless n.is_a?(Integer) && n.positive?

  days = business_days_in_month(date, holidays: holidays)
  raise Error, "month has only #{days.size} business days" if n > days.size

  days[n - 1]
end

.parse_relative(str, relative_to: Date.today) ⇒ Date

Parse a relative date expression into a Date

Parameters:

  • str (String)

    the expression (e.g., “2 weeks ago”, “next month”, “yesterday”)

  • relative_to (Date) (defaults to: Date.today)

    the reference date (defaults to today)

Returns:

  • (Date)

    the parsed date

Raises:

  • (Error)

    if the expression cannot be parsed



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
128
129
130
131
132
133
# File 'lib/philiprehberger/date_kit.rb', line 91

def self.parse_relative(str, relative_to: Date.today)
  relative_to = coerce_date(relative_to)
  normalized = str.to_s.strip.downcase

  case normalized
  when 'today'
    relative_to
  when 'yesterday'
    relative_to - 1
  when 'tomorrow'
    relative_to + 1
  when /\A(\d+)\s+days?\s+ago\z/
    relative_to - ::Regexp.last_match(1).to_i
  when /\Ain\s+(\d+)\s+days?\z/
    relative_to + ::Regexp.last_match(1).to_i
  when /\A(\d+)\s+weeks?\s+ago\z/
    relative_to - (::Regexp.last_match(1).to_i * 7)
  when /\Ain\s+(\d+)\s+weeks?\z/
    relative_to + (::Regexp.last_match(1).to_i * 7)
  when /\A(\d+)\s+months?\s+ago\z/
    months_ago(relative_to, ::Regexp.last_match(1).to_i)
  when /\Ain\s+(\d+)\s+months?\z/
    months_ahead(relative_to, ::Regexp.last_match(1).to_i)
  when /\A(\d+)\s+years?\s+ago\z/
    years_ago(relative_to, ::Regexp.last_match(1).to_i)
  when /\Ain\s+(\d+)\s+years?\z/
    years_ahead(relative_to, ::Regexp.last_match(1).to_i)
  when 'last week'
    relative_to - 7
  when 'next week'
    relative_to + 7
  when 'last month'
    months_ago(relative_to, 1)
  when 'next month'
    months_ahead(relative_to, 1)
  when 'last year'
    years_ago(relative_to, 1)
  when 'next year'
    years_ahead(relative_to, 1)
  else
    raise Error, "cannot parse relative date: #{str}"
  end
end

.prev_business_day(date, holidays: []) ⇒ Date

Return the previous business day before the given date (skips weekends and holidays)

Parameters:

  • date (Date)

    the starting date

  • holidays (Array<Date>) (defaults to: [])

    optional list of holiday dates to skip

Returns:

  • (Date)

    the previous business day



154
155
156
157
158
159
160
161
# File 'lib/philiprehberger/date_kit.rb', line 154

def self.prev_business_day(date, holidays: [])
  date = coerce_date(date)
  holidays = holidays.map { |h| coerce_date(h) }

  current = date - 1
  current -= 1 while weekend?(current) || holidays.include?(current)
  current
end

.quarter(date) ⇒ Integer

Return the quarter number (1-4) for the given date

Parameters:

  • date (Date)

    the input date

Returns:

  • (Integer)

    the quarter number (1-4)



197
198
199
200
# File 'lib/philiprehberger/date_kit.rb', line 197

def self.quarter(date)
  date = coerce_date(date)
  ((date.month - 1) / 3) + 1
end

.weekend?(date) ⇒ Boolean

Check if a date falls on a weekend (Saturday or Sunday)

Parameters:

  • date (Date)

    the date to check

Returns:

  • (Boolean)


80
81
82
83
# File 'lib/philiprehberger/date_kit.rb', line 80

def self.weekend?(date)
  date = coerce_date(date)
  date.saturday? || date.sunday?
end