stephenrichards-holiday_calendar 1.0.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,82 @@
1
+ # == HolidayCalendar
2
+ #
3
+ #
4
+ # HolidayCalendar is a library for determining public holidays in different countries.
5
+ #
6
+ # Its main features are:
7
+ # * Can load standard holiday calendars for UK (England), France, U.S. Federal Holidays
8
+ # * Can determine whether or not a particular day is a public holiday
9
+ # * Can return the name of a public holiday for a particular date
10
+ # * Can determine whether or not a date is a weekend
11
+ # * Can determine whether or not a date is a working day (i.e. not a weekend nor a public holiday)
12
+ # * Can determine the number of working days between two dates
13
+ # * Can determine the date a specified number of working days before or after a specified date
14
+ # * Users can define public holidays for other territories and add them into the calendar
15
+ #
16
+ #
17
+ #
18
+ # == Overview
19
+ # The library is made up of two main elements:
20
+ # * PublicHolidaySpecification
21
+ # * HolidayCalendar
22
+ #
23
+ # A PublicHolidaySpecification defines the name of the holiday, which years it applies to, when
24
+ # it is taken (e.g. 25th December or third Thursday in November), and what to do if it falls on
25
+ # a weekend (ee.g. nothing, take it the last working day before, take it the next working day after).
26
+ #
27
+ # A HolidayCalendar has a name (the territory for which it is valid), defines which days are weekends,
28
+ # and contains an array of PublicHolidaySpecifications. Given this data, the status of any date can be generated.
29
+ #
30
+ # A HolidayCalendar can be instantiated from the standard configurations distributed with this library, from a yaml
31
+ # file provided by the user, or from an array of PublicHolidaySpecification objects.
32
+ #
33
+ # == Example
34
+ # This example loads the UK standard public holidays from the yaml file distributed with this gem.
35
+ # It then loads a second copy, but modifies it for Scotland by removing the Boxing Day holiday and adding a 2nd January holiday.
36
+ # Finally, various calls are made agains the two calendars.
37
+ #
38
+ #
39
+ #
40
+ #
41
+ # require 'holiday_calendar'
42
+ #
43
+ # en = HolidayCalendar.load(:uk)
44
+ # sco = HolidayCalendar.load(:uk)
45
+ # sco.delete('Boxing Day')
46
+ # sco << PublicHolidaySpecification.new(:name => "2nd Jan", :years => :all, :month => 1, :day => 2, :take_after => [:saturday, :sunday])
47
+ #
48
+ #
49
+ # dates = [
50
+ # Date.new(2009,12,25),
51
+ # Date.new(2009,12,26),
52
+ # Date.new(2009,12,27),
53
+ # Date.new(2009,12,28),
54
+ # Date.new(2010,1,1),
55
+ # Date.new(2010,1,4)
56
+ # ]
57
+ #
58
+ #
59
+ # dates.each do |date|
60
+ # if en.public_holiday?(date)
61
+ # puts "#{date} is a public holiday in England: #{en.holiday_name(date)}"
62
+ # end
63
+ # if sco.public_holiday?(date)
64
+ # puts "#{date} is a public holiday in Scotland: #{sco.holiday_name(date)}"
65
+ # end
66
+ # end
67
+ #
68
+ # puts "\n\nScottish Holidays in 2010"
69
+ # puts "==========================="
70
+ # hols = sco.list_for_year(2010)
71
+ # hols.each { |h| puts h }
72
+ #
73
+ # == Feedback
74
+ #
75
+ # Feedback on this project is welcomed, particularly sets of rules for different countries, or useful additions.
76
+ # please mail me at dev@stephenrichards.eu
77
+ #
78
+ # Stephen Richards
79
+
80
+
81
+
82
+
@@ -0,0 +1,72 @@
1
+ territory: fr
2
+
3
+ weekend:
4
+ - saturday
5
+ - sunday
6
+
7
+ public_holidays:
8
+ Jour de l'An:
9
+ years: all
10
+ month: 1
11
+ day: 1
12
+
13
+
14
+ Fête du Travail:
15
+ years: all
16
+ month: 5
17
+ day: 1
18
+
19
+
20
+ Fête de la Victoire 1945:
21
+ years: all
22
+ month: 5
23
+ day: 8
24
+
25
+
26
+ lundi de Pâques:
27
+ years: all
28
+ class_method: ReligiousFestival.easter_monday
29
+
30
+
31
+ Ascension catholique:
32
+ years: all
33
+ class_method: ReligiousFestival.ascension_day
34
+
35
+
36
+ Lundi de Pentecôte:
37
+ years: all
38
+ class_method: ReligiousFestival.whit_monday
39
+
40
+
41
+ Fête nationale:
42
+ years: all
43
+ month: 7
44
+ day: 14
45
+
46
+
47
+ Assomption:
48
+ years: all
49
+ month: 8
50
+ day: 15
51
+
52
+
53
+ Toussaint:
54
+ years: all
55
+ month: 11
56
+ day: 1
57
+
58
+
59
+ Armistice:
60
+ years: all
61
+ month: 11
62
+ day: 11
63
+
64
+
65
+ Noel:
66
+ years: all
67
+ month: 12
68
+ day: 25
69
+ take_after:
70
+ - Saturday
71
+ - Sunday
72
+
@@ -0,0 +1,54 @@
1
+ # holiday calendar schema for UK
2
+
3
+ territory: uk
4
+ weekend:
5
+ - saturday
6
+ - sunday
7
+
8
+ public_holidays:
9
+ New Year's Day:
10
+ years: all
11
+ month: 1
12
+ day: 1
13
+ take_after:
14
+ - saturday
15
+ - sunday
16
+
17
+ Good Friday:
18
+ years: all
19
+ class_method: ReligiousFestival.good_friday
20
+
21
+ Easter Monday:
22
+ years: all
23
+ class_method: ReligiousFestival.easter_monday
24
+
25
+ May Day:
26
+ years: all
27
+ month: 5
28
+ day: first_monday
29
+
30
+ Spring Bank Holiday:
31
+ years: all
32
+ month: 5
33
+ day: last_monday
34
+
35
+ Summer Bank Holiday:
36
+ years: all
37
+ month: 8
38
+ day: last_monday
39
+
40
+ Christmas Day:
41
+ years: all
42
+ month: 12
43
+ day: 25
44
+ take_after:
45
+ - saturday
46
+ - sunday
47
+
48
+ Boxing Day:
49
+ years: all
50
+ month: 12
51
+ day: 26
52
+ take_after:
53
+ - saturday
54
+ - sunday
@@ -0,0 +1,71 @@
1
+ territory: us
2
+ weekend:
3
+ - saturday
4
+ - sunday
5
+
6
+ public_holidays:
7
+ New Year's Day:
8
+ years: all
9
+ month: 1
10
+ day: 1
11
+ take_after:
12
+ - Sunday
13
+ take_before:
14
+ - Saturday
15
+
16
+ Birthday of Martin Luther King, Jr.:
17
+ years: all
18
+ month: 1
19
+ day: third_monday
20
+
21
+ Washington's Birthday:
22
+ years: all
23
+ month: 2
24
+ day: third_monday
25
+
26
+ Memorial Day:
27
+ years: all
28
+ month: 5
29
+ day: last_monday
30
+
31
+ Independence Day:
32
+ years: all
33
+ month: 7
34
+ day: 4
35
+ take_before:
36
+ - Saturday
37
+ take_after:
38
+ - Sunday
39
+
40
+ Labor Day:
41
+ years: all
42
+ month: 9
43
+ day: first_monday
44
+
45
+ Columbus Day:
46
+ years: all
47
+ month: 10
48
+ day: second_monday
49
+
50
+ Veterans' Day:
51
+ years: all
52
+ month: 11
53
+ day: 11
54
+ take_before:
55
+ - Saturday
56
+ take_after:
57
+ - Sunday
58
+
59
+ Thanksgiving Day:
60
+ years: all
61
+ month: 11
62
+ day: fourth_thursday
63
+
64
+ Christmas Day:
65
+ years: all
66
+ month: 12
67
+ day: 25
68
+ take_before:
69
+ - Saturday
70
+ take_after:
71
+ - Sunday
@@ -0,0 +1,522 @@
1
+ require File.dirname(__FILE__) + '/public_holiday_specification'
2
+ require File.dirname(__FILE__) + '/public_holiday'
3
+ require File.dirname(__FILE__) + '/religious_festival'
4
+ require 'yaml'
5
+
6
+
7
+
8
+ # This class generates the public holiday date data from PublicHolidaySpecifications. The class can be instantiated in one of three
9
+ # ways:
10
+ #
11
+ # * HolidayCalendar.load(:territory)
12
+ # * HolidayCalendar.load_file(yaml_file)
13
+ # * HolidayCalendar.create(:territory, [weekends], [public_holidays])
14
+
15
+
16
+ class HolidayCalendar
17
+
18
+ attr_accessor :territory, :weekend, :public_holiday_specifications
19
+
20
+ @@keys_for_std_config = [:territory]
21
+ @@keys_for_yaml = [:filename]
22
+ @@keys_for_array = [:territory, :weekend, :specs]
23
+ @@valid_day_names = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday']
24
+
25
+
26
+ # Instantiates a HolidayCalendar object with empty values
27
+ def initialize
28
+ @territory = nil
29
+ @weekend = nil
30
+ @public_holiday_specifications = nil
31
+ @generated_years = Array.new
32
+ @public_holiday_collection = Array.new
33
+ @public_holiday_hash = Hash.new
34
+ end
35
+
36
+
37
+ # Instantiates a HolidayCalendar object from the standard configuration file for the specified territory
38
+ def self.load(territory)
39
+ cal = HolidayCalendar.new
40
+ self.instantiate_from_std_config(cal, territory)
41
+ cal
42
+ end
43
+
44
+
45
+ # Instantiates a HolidayCalendar object from an array of PublicHolidaySpecifications
46
+ #
47
+ # params
48
+ # * territory: territory name as a symbol
49
+ # * weekend: an array of days which are to be conisidered weekends. Days can be specified as day numbers (Sunday = 0, Saturday = 6), or day names as symbols in lower case.
50
+ # * public_holidays: an array of PublicHolidaySpecification objects
51
+ def self.create(territory, weekend, public_holidays)
52
+ cal = HolidayCalendar.new
53
+ instantiate_from_array(cal, territory, weekend, public_holidays)
54
+ cal
55
+ end
56
+
57
+
58
+ # Instantiates a HolidayCalendar object from a yaml file containing the definition of holiday specifications.
59
+ def self.load_file(filename)
60
+ cal = self.new
61
+ self.instantiate_from_yaml(cal, filename)
62
+ cal
63
+ end
64
+
65
+
66
+
67
+
68
+
69
+ # Adds a new PublicHolidaySpecification or Array of PublicHollidaySpecifications to the Holiday Calendar
70
+ #
71
+ # params
72
+ # * phs: must be a single PublicHolidaySpecification object or an array of PublicHolidaySpecification
73
+ # objects to be added to the calendar.
74
+ def <<(phs)
75
+ if phs.is_a? PublicHolidaySpecification
76
+ phs_array = [phs]
77
+ else
78
+ phs_array = phs
79
+ end
80
+
81
+ if !all_occurrances_of(PublicHolidaySpecification, phs_array)
82
+ raise ArgumentError.new('you must pass a PublicHolidaySpecification or an array of PublicHolidaySpecification objects to << method of HolidayCalendar')
83
+ end
84
+
85
+ @public_holiday_specifications += phs_array
86
+ @generated_years.clear
87
+ @public_holiday_collection.clear
88
+ end
89
+
90
+
91
+ # Deletes a public holiday by names from the holiday calendar.
92
+ #
93
+ # params
94
+ # * public_holiday_name: a string containing the name of the public holiday to be deleted
95
+ #
96
+ # returns
97
+ # * true if a public holiday with that name existed in the calendar and was deleted
98
+ # * false if no public holiday with that name was found in the calendar
99
+ #
100
+ def delete(public_holiday_name)
101
+ holiday_found = false
102
+ index = 0
103
+ @public_holiday_specifications.each do |phs|
104
+ if phs.name == public_holiday_name
105
+ holiday_found = true
106
+ break
107
+ end
108
+ index += 1
109
+ end
110
+ @public_holiday_specifications.delete_at(index)
111
+ @generated_years.clear
112
+ @public_holiday_collection.clear
113
+ holiday_found
114
+ end
115
+
116
+
117
+
118
+
119
+
120
+ # Returns the count of public holidays
121
+ #
122
+ def size
123
+ @public_holiday_specifications.size
124
+ end
125
+
126
+
127
+ # Returns true if the specified date is a weekend
128
+ #
129
+ def weekend?(date)
130
+ populate_public_holiday_collection_for_year(date.year)
131
+ @weekend.include?(date.wday)
132
+ end
133
+
134
+
135
+ # Returns true if the specified date is a public holiday
136
+ #
137
+ def public_holiday?(date)
138
+ return false if weekend?(date) # weekend are never public holidays
139
+ populate_public_holiday_collection_for_year(date.year)
140
+
141
+ return true if @public_holiday_hash.has_key?(date)
142
+ return false
143
+ end
144
+
145
+
146
+
147
+
148
+
149
+ # Returns true if the specified date is neither a weekend nor a public holiday
150
+ #
151
+ def working_day?(date, repopulate = true)
152
+ if repopulate
153
+ populate_public_holiday_collection_for_year(date.year)
154
+ end
155
+ !weekend?(date) && !public_holiday?(date)
156
+ end
157
+
158
+
159
+ # Returns the count of the number of working days between two dates (does not count the start date as 1 day).
160
+ #
161
+ def count_working_days_between(start_date, end_date)
162
+ populate_public_holiday_collection_for_year(start_date.year)
163
+ populate_public_holiday_collection_for_year(end_date.year)
164
+ if start_date >= end_date
165
+ raise ArgumentError.new("start_date passed to HolidayCalendar.count_days_between() is not before end_date")
166
+ end
167
+
168
+ working_day_count = 0
169
+ date = start_date
170
+ while date < end_date
171
+ date += 1
172
+ working_day_count += 1 if working_day?(date)
173
+ end
174
+ working_day_count
175
+ end
176
+
177
+
178
+
179
+
180
+ # Returns a date a number of working days after a specified date
181
+ #
182
+ # params
183
+ # * start_date : the date at which to start counting
184
+ # * num_working_days : the number of working days to count
185
+ #
186
+ def working_days_after(start_date, num_working_days)
187
+ populate_public_holiday_collection_for_year(start_date.year)
188
+ working_days_offset(start_date, num_working_days, :forward)
189
+ end
190
+
191
+
192
+
193
+ # Returns a date a number of working days before a specified date
194
+ #
195
+ # params
196
+ # * start_date : the date at which to start counting
197
+ # * num_working_days : the number of working days to count
198
+ #
199
+ def working_days_before(start_date, num_working_days)
200
+ populate_public_holiday_collection_for_year(start_date.year)
201
+ working_days_offset(start_date, num_working_days, :backward)
202
+ end
203
+
204
+
205
+ # Returns an array of string descriptions of each of the PublicHolidaySpecifications in the calendar
206
+ #
207
+ def list
208
+ specs = Array.new
209
+ @public_holiday_specifications.each { |phs| specs << phs.to_s }
210
+ specs
211
+ end
212
+
213
+
214
+ # Returns an array of strings giving the date and the name of the holiday for each holiday in the year
215
+ #
216
+ def list_for_year(year)
217
+ populate_public_holiday_collection_for_year(year)
218
+ holiday_dates = Array.new
219
+ @public_holiday_collection.each do |ph|
220
+ holiday_dates << ph.to_s if ph.year == year
221
+ end
222
+ holiday_dates
223
+ end
224
+
225
+ # Returns the name of the holiday for a particular date, or nil if the date is not a holiday
226
+ #
227
+ # params
228
+ # * date: the date for which the holiday name is to be returned
229
+ # * date_adjusted_text: true, if the phrase 'carried forward from' or 'brought forward from' is to be included
230
+ # in cases where the date of the holiday was adjusted because it fell on a weekend (or to be more precise, on
231
+ # a day specified in the take_before or take_after options of the PublicHolidaySpecification)
232
+ #
233
+ def holiday_name(date, date_adjusted_text = true)
234
+ populate_public_holiday_collection_for_year(date.year)
235
+ ph = @public_holiday_hash[date]
236
+ if ph.nil?
237
+ return nil
238
+ else
239
+ return ph.name(date_adjusted_text)
240
+ end
241
+ end
242
+
243
+
244
+
245
+ private
246
+
247
+ # returns true if array contains only instances of klass
248
+ def all_occurrances_of(klass, array)
249
+ array.each do |instance|
250
+ if !instance.instance_of?(klass)
251
+ return false
252
+ end
253
+ end
254
+ return true
255
+ end
256
+
257
+
258
+
259
+ # read all yaml files in the config directory looking for one with a territory of the specified name,
260
+ # and load it
261
+ def self.instantiate_from_std_config(cal, territory)
262
+ raise ArgumentError.new('No territory specified for HolidayCalendar.new in std_config mode') if territory.nil?
263
+
264
+ territory = territory.to_s
265
+ yaml_files = Dir[File.dirname(__FILE__) + '/../config/*.yaml']
266
+ yaml_files.each do |yf|
267
+ begin
268
+ yaml_spec = YAML.load_file(yf)
269
+ next if yaml_spec['territory'] != territory
270
+ instantiate_from_yaml(cal, yf)
271
+ rescue => err
272
+ puts "ERROR while loading #{yf}"
273
+ raise
274
+ end
275
+ end
276
+
277
+ end
278
+
279
+
280
+ def self.instantiate_from_yaml(cal, filename)
281
+
282
+
283
+ if !File.exist?(filename)
284
+ raise ArgumentError.new("The filename specified in HolidayCalender.new cannot be found: #{filename}")
285
+ end
286
+
287
+ yaml_file_contents = YAML.load_file(filename)
288
+ validate_yaml_file_contents(cal, filename, yaml_file_contents)
289
+ end
290
+
291
+
292
+ def self.validate_yaml_file_contents(cal, filename, yaml_file_contents)
293
+ validate_and_populate_territory(cal, filename, yaml_file_contents)
294
+ validate_and_populate_weekend(cal, filename, yaml_file_contents)
295
+ validate_and_populate_public_holidays(cal, filename, yaml_file_contents)
296
+ end
297
+
298
+
299
+ def self.validate_and_populate_territory(cal, filename, yaml_file_contents)
300
+ if !yaml_file_contents.has_key? 'territory'
301
+ raise ArgumentError.new("YAML file #{filename} does not have a 'territory' setting")
302
+ end
303
+ cal.territory = yaml_file_contents['territory'].to_sym
304
+ end
305
+
306
+
307
+ def self.validate_and_populate_weekend(cal, filename, yaml_file_contents)
308
+ raise ArgumentError.new("YAML file #{filename} does not have a 'weekend' setting") if !yaml_file_contents.has_key? 'weekend'
309
+ klass = yaml_file_contents['weekend'].class
310
+ raise ArgumentError.new("Invalid YAML file element 'weekend' - must be an Array, is #{klass}") if klass != Array
311
+
312
+ weekend_array = Array.new
313
+
314
+ yaml_file_contents['weekend'].each do |day|
315
+ day_num = @@valid_day_names.index(day.downcase)
316
+ raise ArgumentError.new("Invalid day specified as weekend: #{day}") if !day_num
317
+ weekend_array << day_num
318
+ end
319
+ cal.weekend = weekend_array
320
+ end
321
+
322
+
323
+ def self.validate_and_populate_public_holidays(cal, filename, yaml_file_contents)
324
+ raise ArgumentError.new("YAML file #{filename} does not have a 'public_holidays' setting") if !yaml_file_contents.has_key? 'public_holidays'
325
+ klass = yaml_file_contents['public_holidays'].class
326
+ raise ArgumentError.new("Invalid YAML file element 'public_holidays' - must be an Hash, is #{klass}") if klass != Hash
327
+
328
+ phs_array = Array.new
329
+ yaml_file_contents['public_holidays'].each do |name, yaml_spec|
330
+ phs = PublicHolidaySpecification.instantiate_from_yaml_definition(filename, name, yaml_spec)
331
+ phs_array<< phs
332
+ end
333
+ cal.public_holiday_specifications = phs_array
334
+ end
335
+
336
+
337
+
338
+ def self.instantiate_from_array(cal, territory, weekend, public_holiday_specifications)
339
+ validate_territory(territory)
340
+ valid_weekend = validate_weekend(weekend)
341
+ validate_public_holiday_specifications(public_holiday_specifications)
342
+
343
+ cal.territory = territory
344
+ cal.weekend = valid_weekend
345
+ cal.public_holiday_specifications = public_holiday_specifications
346
+ end
347
+
348
+
349
+ def self.validate_territory(territory)
350
+ raise ArgumentError.new('territory must be specified as symbol in HolidayCalendar.create') if !territory.is_a? Symbol
351
+ end
352
+
353
+
354
+ def self.validate_weekend(weekend)
355
+ raise ArgumentError.new('weekend must be specified as an Array in HolidayCalendar.create') if !weekend.is_a? Array
356
+ valid_weekend = Array.new
357
+ weekend.each do |day|
358
+ valid_weekend << self.normalise_day(day)
359
+ end
360
+ valid_weekend
361
+ end
362
+
363
+
364
+ def self.validate_public_holiday_specifications(public_holiday_specifications)
365
+ raise ArgumentError.new('public_holidays must be specified as an Array in HolidayCalendar.create') if !public_holiday_specifications.is_a? Array
366
+ public_holiday_specifications.each do |phs|
367
+ raise ArgumentError.new('public holidays must be an array of PublicHolidaySpecification objects in HolidayCalendar.create') if !phs.instance_of?(PublicHolidaySpecification)
368
+ end
369
+ end
370
+
371
+
372
+
373
+ def self.normalise_day(day)
374
+ if day.is_a? Fixnum
375
+ if day >= 0 && day < 7
376
+ return day
377
+ end
378
+ end
379
+ if day.is_a? String
380
+ day.downcase!
381
+ if @@valid_day_names.include?(day)
382
+ return @@valid_day_names.index(day)
383
+ end
384
+ end
385
+ raise ArgumentError.new('Invalid weekend array passsed to HolidayCalendar.create: each day must be day number in range 0-6 or day name')
386
+ end
387
+
388
+
389
+
390
+ # for the specified year (and a year either side so as to cater for holidays that get taken before in the last day of the
391
+ # previous year, or taken after in the first day of the next year), iterates through the collection of public holiday
392
+ # specifications and
393
+ # generates a PublicHoliday object for each one for the specified year and adds it to the collection
394
+ # of public holidays.
395
+ def populate_public_holiday_collection_for_year(year)
396
+ return if @generated_years.include?(year) # don't generate if we've already done it
397
+ @public_holiday_specifications.each do |phs|
398
+ next if !phs.applies_to_year?(year)
399
+ @public_holiday_collection << PublicHoliday.new(phs, year)
400
+ end
401
+ @generated_years << year # add to the list of years we've generated
402
+ @public_holiday_collection.sort!
403
+ populate_public_holiday_hash
404
+ adjust_carry_forwards
405
+ populate_holidays_brought_forward_from_next_year(year + 1)
406
+ end
407
+
408
+
409
+ # handles the edge case where early holidays in the following year are brought forward to this year.
410
+ # this method is called with the following year as a parameter
411
+ def populate_holidays_brought_forward_from_next_year(year)
412
+ return if @generated_years.include?(year) # no need to do anything if the holidays have already been generated for this year
413
+ @public_holiday_specifications.each do |phs|
414
+ next if !phs.applies_to_year?(year)
415
+ ph = PublicHoliday.new(phs, year)
416
+ if ph.must_be_taken_before?
417
+ new_date = last_working_day_before(ph.date)
418
+ if new_date.year < ph.date.year
419
+ ph.adjust_date(new_date)
420
+ @public_holiday_hash[new_date] = ph
421
+ end
422
+ end
423
+ end
424
+ end
425
+
426
+
427
+
428
+ # refreshes the public_holiday_hash based on the contents of the public_holiday_collection
429
+ def populate_public_holiday_hash
430
+ @public_holiday_hash = Hash.new
431
+ @public_holiday_collection.each do |ph|
432
+ @public_holiday_hash[ph.date] = ph
433
+ end
434
+ end
435
+
436
+
437
+ # iterates through the public_holiday_collection, and where a public holiday falls on a day
438
+ # which is in the take_before or take_after array, then adjusts the date accordingly
439
+ def adjust_carry_forwards
440
+ @public_holiday_collection.each do |ph|
441
+ if ph.must_be_taken_after?
442
+ new_date = next_working_day_after(ph.date)
443
+ @public_holiday_hash.delete(ph.date)
444
+ ph.adjust_date(new_date)
445
+ @public_holiday_hash[new_date] = ph
446
+ elsif ph.must_be_taken_before?
447
+ new_date = last_working_day_before(ph.date)
448
+ @public_holiday_hash.delete(ph.date)
449
+ ph.adjust_date(new_date)
450
+ @public_holiday_hash[new_date] = ph
451
+ end
452
+
453
+ end
454
+ end
455
+
456
+
457
+
458
+ def next_working_day_after(date)
459
+ date +=1
460
+ while !working_day?(date) do
461
+ date += 1
462
+ end
463
+ date
464
+ end
465
+
466
+
467
+
468
+ def last_working_day_before(date)
469
+ date -= 1
470
+ while !working_day?(date) do
471
+ date -= 1
472
+ end
473
+ date
474
+ end
475
+
476
+
477
+
478
+
479
+
480
+
481
+
482
+ def working_days_offset(start_date, num_working_days, direction)
483
+ date = start_date
484
+ while num_working_days > 0 do
485
+ if direction == :forward
486
+ date += 1
487
+ else
488
+ date -= 1
489
+ end
490
+ # puts "#{date} #{num_working_days} #{working_day?(date)}"
491
+ num_working_days -= 1 if working_day?(date)
492
+ end
493
+ date
494
+ end
495
+
496
+
497
+ end
498
+
499
+
500
+
501
+
502
+
503
+
504
+
505
+
506
+
507
+
508
+
509
+
510
+
511
+
512
+
513
+
514
+
515
+
516
+
517
+
518
+
519
+
520
+
521
+
522
+