Class: Business::Calendar
- Inherits:
-
Object
- Object
- Business::Calendar
- Defined in:
- lib/business/calendar.rb
Constant Summary collapse
- VALID_KEYS =
%w[holidays working_days extra_working_dates].freeze
- DAY_NAMES =
%( mon tue wed thu fri sat sun )
Class Attribute Summary collapse
-
.load_paths ⇒ Object
Returns the value of attribute load_paths.
Instance Attribute Summary collapse
-
#extra_working_dates ⇒ Object
readonly
Returns the value of attribute extra_working_dates.
-
#holidays ⇒ Object
readonly
Returns the value of attribute holidays.
-
#name ⇒ Object
readonly
Returns the value of attribute name.
-
#working_days ⇒ Object
readonly
Returns the value of attribute working_days.
Class Method Summary collapse
- .find_calendar_data(calendar_name) ⇒ Object
- .load(calendar_name) ⇒ Object
- .load_cached(calendar) ⇒ Object
Instance Method Summary collapse
-
#add_business_days(date, delta) ⇒ Object
Add a number of business days to a date.
-
#business_day?(date) ⇒ Boolean
Return true if the date given is a business day (typically that means a non-weekend day) and not a holiday.
-
#business_days_between(date1, date2) ⇒ Object
Count the number of business days between two dates.
- #day_interval_for(date) ⇒ Object
-
#default_working_days ⇒ Object
If no working days are provided in the calendar config, these are used.
- #holiday?(date) ⇒ Boolean
-
#initialize(name: nil, extra_working_dates: nil, working_days: nil, holidays: nil) ⇒ Calendar
constructor
A new instance of Calendar.
-
#next_business_day(date) ⇒ Object
Roll forward to the next business day regardless of whether the given date is a business day or not.
- #parse_dates(dates) ⇒ Object
-
#previous_business_day(date) ⇒ Object
Roll backward to the previous business day regardless of whether the given date is a business day or not.
-
#roll_backward(date) ⇒ Object
Roll backward to the previous business day.
-
#roll_forward(date) ⇒ Object
Roll forward to the next business day.
- #set_extra_working_dates(extra_working_dates) ⇒ Object
-
#set_holidays(holidays) ⇒ Object
Internal method for assigning holidays from a calendar config.
-
#set_working_days(working_days) ⇒ Object
Internal method for assigning working days from a calendar config.
-
#subtract_business_days(date, delta) ⇒ Object
Subtract a number of business days to a date.
- #working_day?(date) ⇒ Boolean
Constructor Details
#initialize(name: nil, extra_working_dates: nil, working_days: nil, holidays: nil) ⇒ Calendar
Returns a new instance of Calendar.
62 63 64 65 66 67 68 69 70 71 |
# 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 |
Class Attribute Details
.load_paths ⇒ Object
Returns the value of attribute load_paths.
12 13 14 |
# File 'lib/business/calendar.rb', line 12 def load_paths @load_paths end |
Instance Attribute Details
#extra_working_dates ⇒ Object (readonly)
Returns the value of attribute extra_working_dates.
60 61 62 |
# File 'lib/business/calendar.rb', line 60 def extra_working_dates @extra_working_dates end |
#holidays ⇒ Object (readonly)
Returns the value of attribute holidays.
60 61 62 |
# File 'lib/business/calendar.rb', line 60 def holidays @holidays end |
#name ⇒ Object (readonly)
Returns the value of attribute name.
60 61 62 |
# File 'lib/business/calendar.rb', line 60 def name @name end |
#working_days ⇒ Object (readonly)
Returns the value of attribute working_days.
60 61 62 |
# File 'lib/business/calendar.rb', line 60 def working_days @working_days end |
Class Method Details
.find_calendar_data(calendar_name) ⇒ Object
36 37 38 39 40 41 42 43 44 45 46 47 |
# File 'lib/business/calendar.rb', line 36 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 calendar_path = Pathname.new(path).join("#{calendar_name}.yml") next unless calendar_path.exist? break YAML.safe_load(calendar_path.read, permitted_classes: [Date]) end end end |
.load(calendar_name) ⇒ Object
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
# 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 |
.load_cached(calendar) ⇒ Object
50 51 52 53 54 55 56 |
# 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 |
Instance Method Details
#add_business_days(date, delta) ⇒ Object
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
129 130 131 132 133 134 135 136 137 138 |
# 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 |
#business_day?(date) ⇒ Boolean
Return true if the date given is a business day (typically that means a non-weekend day) and not a holiday.
75 76 77 78 |
# File 'lib/business/calendar.rb', line 75 def business_day?(date) date = date.to_date working_day?(date) && !holiday?(date) end |
#business_days_between(date1, date2) ⇒ Object
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)
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 |
# File 'lib/business/calendar.rb', line 159 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 |
#day_interval_for(date) ⇒ Object
192 193 194 |
# File 'lib/business/calendar.rb', line 192 def day_interval_for(date) date.is_a?(Date) ? 1 : 3600 * 24 end |
#default_working_days ⇒ Object
If no working days are provided in the calendar config, these are used.
225 226 227 |
# File 'lib/business/calendar.rb', line 225 def default_working_days %w[mon tue wed thu fri] end |
#holiday?(date) ⇒ Boolean
86 87 88 |
# File 'lib/business/calendar.rb', line 86 def holiday?(date) holidays.include?(date.to_date) end |
#next_business_day(date) ⇒ Object
Roll forward to the next business day regardless of whether the given date is a business day or not.
108 109 110 111 112 113 |
# 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 |
#parse_dates(dates) ⇒ Object
211 212 213 |
# File 'lib/business/calendar.rb', line 211 def parse_dates(dates) (dates || []).map { |date| date.is_a?(Date) ? date : Date.parse(date) } end |
#previous_business_day(date) ⇒ Object
Roll backward to the previous business day regardless of whether the given date is a business day or not.
117 118 119 120 121 122 |
# 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(date) ⇒ Object
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.
101 102 103 104 |
# File 'lib/business/calendar.rb', line 101 def roll_backward(date) date -= day_interval_for(date) until business_day?(date) date end |
#roll_forward(date) ⇒ Object
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.
93 94 95 96 |
# File 'lib/business/calendar.rb', line 93 def roll_forward(date) date += day_interval_for(date) until business_day?(date) date end |
#set_extra_working_dates(extra_working_dates) ⇒ Object
220 221 222 |
# File 'lib/business/calendar.rb', line 220 def set_extra_working_dates(extra_working_dates) @extra_working_dates = parse_dates(extra_working_dates) end |
#set_holidays(holidays) ⇒ Object
Internal method for assigning holidays from a calendar config.
216 217 218 |
# File 'lib/business/calendar.rb', line 216 def set_holidays(holidays) @holidays = parse_dates(holidays) end |
#set_working_days(working_days) ⇒ Object
Internal method for assigning working days from a calendar config.
197 198 199 200 201 202 203 204 205 206 207 208 209 |
# File 'lib/business/calendar.rb', line 197 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_business_days(date, delta) ⇒ Object
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
145 146 147 148 149 150 151 152 153 154 |
# 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 |
#working_day?(date) ⇒ Boolean
80 81 82 83 84 |
# 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 |