class Business::Calendar
Constants
- DAY_NAMES
- VALID_KEYS
Attributes
Public Class Methods
rubocop:enable Metrics/MethodLength
# File lib/business/calendar.rb, line 37 def self.find_calendar_data(calendar_name) calendar_directories.detect do |path| if path.is_a?(Hash) break path[calendar_name] if path[calendar_name] else next unless File.exist?(File.join(path, "#{calendar_name}.yml")) break YAML.load_file(File.join(path, "#{calendar_name}.yml")) end end end
rubocop:disable Metrics/MethodLength
# File lib/business/calendar.rb, line 20 def self.load(calendar_name) data = find_calendar_data(calendar_name) raise "No such calendar '#{calendar_name}'" unless data unless (data.keys - VALID_KEYS).empty? raise "Only valid keys are: #{VALID_KEYS.join(', ')}" end new( name: calendar_name, holidays: data["holidays"], working_days: data["working_days"], extra_working_dates: data["extra_working_dates"], ) end
# File lib/business/calendar.rb, line 50 def self.load_cached(calendar) @lock.synchronize do @cache ||= {} @cache[calendar] = self.load(calendar) unless @cache.include?(calendar) @cache[calendar] end end
# File lib/business/calendar.rb, line 62 def initialize(name: nil, extra_working_dates: nil, working_days: nil, holidays: nil) @name = name set_extra_working_dates(extra_working_dates) set_working_days(working_days) set_holidays(holidays) unless (@holidays & @extra_working_dates).none? raise ArgumentError, "Holidays cannot be extra working dates" end end
Private Class Methods
# File lib/business/calendar.rb, line 14 def self.calendar_directories @load_paths end
Public Instance Methods
Add a number of business days to a date. If a non-business day is given, counting will start from the next business day. So,
monday + 1 = tuesday friday + 1 = monday sunday + 1 = tuesday
# File lib/business/calendar.rb, line 129 def add_business_days(date, delta) date = roll_forward(date) delta.times do loop do date += day_interval_for(date) break date if business_day?(date) end end date end
Return true if the date given is a business day (typically that means a non-weekend day) and not a holiday.
# File lib/business/calendar.rb, line 75 def business_day?(date) date = date.to_date working_day?(date) && !holiday?(date) end
Count the number of business days between two dates. This method counts from start of date1 to start of date2. So, business_days_between
(mon, weds) = 2 (assuming no holidays) rubocop:disable Metrics/AbcSize rubocop:disable Metrics/MethodLength
# File lib/business/calendar.rb, line 161 def business_days_between(date1, date2) date1 = date1.to_date date2 = date2.to_date # To optimise this method we split the range into full weeks and a # remaining period. # # We then calculate business days in the full weeks period by # multiplying number of weeks by number of working days in a week and # removing holidays one by one. # For the remaining period, we just loop through each day and check # whether it is a business day. # Calculate number of full weeks and remaining days num_full_weeks, remaining_days = (date2 - date1).to_i.divmod(7) # First estimate for full week range based on # biz days in a week num_biz_days = num_full_weeks * working_days.length full_weeks_range = (date1...(date2 - remaining_days)) num_biz_days -= holidays.count do |holiday| in_range = full_weeks_range.cover?(holiday) # Only pick a holiday if its on a working day (e.g., not a weekend) on_biz_day = working_days.include?(holiday.strftime("%a").downcase) in_range && on_biz_day end remaining_range = (date2 - remaining_days...date2) # Loop through each day in remaining_range and count if a business day num_biz_days + remaining_range.count { |a| business_day?(a) } end
rubocop:enable Metrics/AbcSize rubocop:enable Metrics/MethodLength
# File lib/business/calendar.rb, line 196 def day_interval_for(date) date.is_a?(Date) ? 1 : 3600 * 24 end
If no working days are provided in the calendar config, these are used.
# File lib/business/calendar.rb, line 229 def default_working_days %w[mon tue wed thu fri] end
# File lib/business/calendar.rb, line 86 def holiday?(date) holidays.include?(date.to_date) end
Roll forward to the next business day regardless of whether the given date is a business day or not.
# File lib/business/calendar.rb, line 108 def next_business_day(date) loop do date += day_interval_for(date) break date if business_day?(date) end end
# File lib/business/calendar.rb, line 215 def parse_dates(dates) (dates || []).map { |date| date.is_a?(Date) ? date : Date.parse(date) } end
Roll backward to the previous business day regardless of whether the given date is a business day or not.
# File lib/business/calendar.rb, line 117 def previous_business_day(date) loop do date -= day_interval_for(date) break date if business_day?(date) end end
Roll backward to the previous business day. If the date given is a business day, that day will be returned. If the day given is a holiday or non-working day, the previous non-holiday working day will be returned.
# File lib/business/calendar.rb, line 101 def roll_backward(date) date -= day_interval_for(date) until business_day?(date) date end
Roll forward to the next business day. If the date given is a business day, that day will be returned. If the day given is a holiday or non-working day, the next non-holiday working day will be returned.
# File lib/business/calendar.rb, line 93 def roll_forward(date) date += day_interval_for(date) until business_day?(date) date end
# File lib/business/calendar.rb, line 224 def set_extra_working_dates(extra_working_dates) @extra_working_dates = parse_dates(extra_working_dates) end
Internal method for assigning holidays from a calendar config.
# File lib/business/calendar.rb, line 220 def set_holidays(holidays) @holidays = parse_dates(holidays) end
Internal method for assigning working days from a calendar config.
# File lib/business/calendar.rb, line 201 def set_working_days(working_days) @working_days = (working_days || default_working_days).map do |day| day.downcase.strip[0..2].tap do |normalised_day| raise "Invalid day #{day}" unless DAY_NAMES.include?(normalised_day) end end extra_working_dates_names = @extra_working_dates.map do |d| d.strftime("%a").downcase end return if (extra_working_dates_names & @working_days).none? raise ArgumentError, "Extra working dates cannot be on working days" end
Subtract a number of business days to a date. If a non-business day is given, counting will start from the previous business day. So,
friday - 1 = thursday monday - 1 = friday sunday - 1 = thursday
# File lib/business/calendar.rb, line 145 def subtract_business_days(date, delta) date = roll_backward(date) delta.times do loop do date -= day_interval_for(date) break date if business_day?(date) end end date end
# File lib/business/calendar.rb, line 80 def working_day?(date) date = date.to_date extra_working_dates.include?(date) || working_days.include?(date.strftime("%a").downcase) end