stephenrichards-holiday_calendar 1.0.10
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rb +82 -0
- data/config/fr.yaml +72 -0
- data/config/uk.yaml +54 -0
- data/config/us.yaml +71 -0
- data/lib/holiday_calendar.rb +522 -0
- data/lib/modified_weekday.rb +135 -0
- data/lib/public_holiday.rb +173 -0
- data/lib/public_holiday_specification.rb +294 -0
- data/lib/religious_festival.rb +99 -0
- data/test/holiday_calendar_test.rb +664 -0
- data/test/modified_weekday_test.rb +201 -0
- data/test/public_holiday_specification_test.rb +254 -0
- data/test/public_holiday_test.rb +197 -0
- data/test/religious_festival_test.rb +25 -0
- data/test/test_helper.rb +14 -0
- data/test/units.rb +8 -0
- metadata +84 -0
data/README.rb
ADDED
@@ -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
|
+
|
data/config/fr.yaml
ADDED
@@ -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
|
+
|
data/config/uk.yaml
ADDED
@@ -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
|
data/config/us.yaml
ADDED
@@ -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
|
+
|