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,135 @@
1
+ # This class encapsulates all the information needed to describe something like
2
+ # first monday, second thursday or last wednesday of a particular month.
3
+
4
+ class ModifiedWeekday
5
+
6
+
7
+ @@valid_weekdays = [:sunday, :monday, :tuesday, :wednesday, :thursday, :friday, :saturday]
8
+ @@valid_modifiers = [:last, :first, :second, :third, :fourth]
9
+
10
+
11
+ attr_reader :weekday_name, :modifier, :wday, :weekday_occurrance, :expression, :is_last
12
+
13
+ # Instantiate a ModifiedWeekday object from either an expression such as :first_monday, or a date object
14
+ #
15
+ # params
16
+ # * param : param can either be a valid expression or a date object.
17
+ # * valid expressions are :xxxx_yyyyyy where xxxx is :first, :second, :third, :fourth, :last and yyyy is monday, tuesday, etc.
18
+ #
19
+ def initialize(param)
20
+ @weekday_name = nil
21
+ @modifier = nil
22
+ @wday = nil
23
+ @weekday_occurrance = nil # 0 = last, 1 = first, 2 = second, etc.
24
+ @is_last = false
25
+
26
+
27
+ if param.is_a? Symbol
28
+ @expression = param
29
+ string_expr = @expression.to_s
30
+ string_modifier, string_weekday = string_expr.split('_')
31
+ @weekday_name = string_weekday.to_sym
32
+ @modifier = string_modifier.to_sym
33
+ validate_weekday
34
+ validate_modifier
35
+ @is_last = true if @modifier == :last
36
+ elsif param.is_a? Date
37
+ get_modifiers_from_date(param)
38
+ else
39
+ raise ArgumentError.new("Invalid Type passed to ModifiedWeekday.new: #{param.class}")
40
+ end
41
+
42
+ end
43
+
44
+
45
+ def to_s
46
+ @modifier.to_s + "_" + @weekday_name.to_s
47
+ end
48
+
49
+
50
+
51
+ def sort_value
52
+ sort_val = @weekday_occurrance * 10
53
+ sort_val = 50 if sort_val == 0
54
+ sort_val += @wday
55
+ end
56
+
57
+
58
+
59
+ # Compare two ModifiedWeekday objects. Objects are equal if the expression is the same, or the days are the same and the modifier is :last and / or is_last is true.
60
+ def ==(other)
61
+ return true if other.expression == self.expression
62
+
63
+ if other.weekday_name == self.weekday_name
64
+ if other.is_last == true || other.modifier == :last
65
+ if self.is_last == true || self.modifier == :last
66
+ return true
67
+ end
68
+ end
69
+ end
70
+ return false
71
+ end
72
+
73
+
74
+
75
+ private
76
+
77
+ def get_modifiers_from_date(date)
78
+ @weekday_name = @@valid_weekdays[date.wday]
79
+ @wday = date.wday
80
+ @weekday_occurrance = get_weekday_occurrance(date)
81
+ determine_whether_last(date)
82
+ @modifier = make_modifier
83
+ @expression = (@modifier.to_s + '_' + @weekday_name.to_s).to_sym
84
+
85
+ end
86
+
87
+
88
+
89
+ def get_weekday_occurrance(date)
90
+ num_weeks = date.mday / 7
91
+ remainder = date.mday % 7
92
+ if remainder > 0
93
+ occurrance = num_weeks + 1
94
+ else
95
+ occurrance= num_weeks
96
+ end
97
+ occurrance
98
+ end
99
+
100
+
101
+ def determine_whether_last(date)
102
+ days_in_month = Date.new(date.year, date.month, -1).day
103
+ days_left = days_in_month - date.day
104
+ if days_left < 7
105
+ @is_last = true
106
+ end
107
+ end
108
+
109
+
110
+
111
+ def make_modifier
112
+ modifier = @@valid_modifiers[@weekday_occurrance]
113
+ if !modifier && @is_last
114
+ modifier = :last
115
+ end
116
+ modifier
117
+ end
118
+
119
+
120
+
121
+ def validate_weekday
122
+ if !@@valid_weekdays.include?(@weekday_name)
123
+ raise ArgumentError.new("Invalid Weekday component passed to ModifiedWeekday.new: #{@expression}")
124
+ end
125
+ @wday = @@valid_weekdays.index(@weekday_name)
126
+ end
127
+
128
+
129
+ def validate_modifier
130
+ if !@@valid_modifiers.include?(@modifier)
131
+ raise ArgumentError.new("Invalid weekday modifier passed to ModifiedWeekday.new: #{@expression}")
132
+ end
133
+ @weekday_occurrance = @@valid_modifiers.index(@modifier)
134
+ end
135
+ end
@@ -0,0 +1,173 @@
1
+
2
+ # Encapsulates a date which is a public holiday for a particular year derived from a
3
+ # PublicHolidaySpecification and a year
4
+
5
+ class PublicHoliday
6
+
7
+ include Comparable
8
+
9
+ attr_reader :year
10
+ attr_accessor :date, :date_adjusted_text
11
+ attr_writer :name
12
+
13
+
14
+ # Instantiates a PublicHoliday object
15
+ #
16
+ # params
17
+ # * public_holiday_specification: a PublicHolidaySpecification object
18
+ # * year: a year number
19
+ #
20
+
21
+ def initialize(public_holiday_specification, year)
22
+
23
+ @year = year
24
+ @holiday = false
25
+ @date = nil
26
+ @name = nil
27
+ @take_before = Array.new
28
+ @take_after = Array.new
29
+ @date_adjusted = false
30
+ @date_adjusted_text = nil
31
+
32
+ if public_holiday_specification.applies_to_year?(year)
33
+ @holiday = true
34
+ setup_public_holiday(public_holiday_specification, @year)
35
+ end
36
+ end
37
+
38
+
39
+ def <=>(other)
40
+ self.date <=> other.date
41
+ end
42
+
43
+
44
+ # returns the date and name of the holiday, and optionally, the date adjustedtext if any.
45
+ def to_s(date_adjusted_text = true)
46
+ description = "#{@date.strftime("%a %d %b %Y")} : #{self.name}"
47
+ # if date_adjusted_text && @date_adjusted
48
+ # description += " (#{@date_adjusted_text})"
49
+ # end
50
+ description
51
+ end
52
+
53
+
54
+ def name(date_adjusted_text = true)
55
+ description = @name
56
+ if date_adjusted_text && @date_adjusted
57
+ description += " (#{@date_adjusted_text})"
58
+ end
59
+ description
60
+ end
61
+
62
+
63
+
64
+ # returns true if the day number of the holiday date is in the taken_before array
65
+ def must_be_taken_before?
66
+ return true if @take_before.include? @date.wday
67
+ return false
68
+ end
69
+
70
+
71
+
72
+ def must_be_taken_after?
73
+ return true if @take_after.include? @date.wday
74
+ return false
75
+ end
76
+
77
+
78
+ def holiday?
79
+ @holiday
80
+ end
81
+
82
+
83
+
84
+ def adjust_date(new_date)
85
+ @date_adjusted = true
86
+ if new_date > @date
87
+ direction = 'carried'
88
+ else
89
+ direction = 'brought'
90
+ end
91
+ @date_adjusted_text = "#{direction} forward from #{@date.strftime('%a %d %b %Y')}"
92
+ @date = new_date
93
+ end
94
+
95
+
96
+
97
+ private
98
+ def setup_public_holiday(phs, year)
99
+ @name = phs.name
100
+ if phs.uses_class_method?
101
+ @date = phs.klass.send(phs.method_name, year)
102
+ elsif phs.day.is_a? Fixnum
103
+ @date = Date.new(year, phs.month, phs.day)
104
+ else
105
+ @date = generate_date_from_expression(phs, year)
106
+ end
107
+
108
+ @take_before = phs.take_before
109
+ @take_after = phs.take_after
110
+ end
111
+
112
+
113
+ # generates an actual date for @year from the PublicHolidaySpecification when the specification is written as an expression
114
+ def generate_date_from_expression(phs, year)
115
+ if phs.day.class != ModifiedWeekday
116
+ raise RuntimeError.new("Error - expecting PublicHolidaySpecification.day to be a ModifiedWeekday: is a #{phs.day.class}")
117
+ end
118
+ month = phs.month
119
+ wday = phs.day.wday
120
+ occurrance = phs.day.weekday_occurrance
121
+
122
+
123
+ if phs.day.modifier == :last
124
+ date = get_last_wday_in_month(year, month, wday)
125
+ else
126
+ date = get_nth_day_in_month(year, month, wday, occurrance)
127
+ end
128
+ date
129
+ end
130
+
131
+
132
+
133
+ # returns the date of the nth monday, tuesday, etc. in a month for a particular year
134
+ #
135
+ # params
136
+ # * year : the year for which the date is to be generated
137
+ # * month : the month number (1-12) for which the date is to be generated
138
+ # * wday : the wday number (Sunday = 0, Saturday = 6) for which the date is to be generated
139
+ # * n : the n in nth monday (range 1 - 4 inclusive)
140
+ #
141
+ def get_nth_day_in_month(year, month, wday, n)
142
+ first_day_of_month = Date.new(year, month, 1)
143
+ wday_of_first_day = first_day_of_month.wday
144
+ day_number = wday - wday_of_first_day + 1
145
+ day_number += 7 if day_number < 1
146
+
147
+ # now add on 7 days for each 'n'
148
+ n -= 1
149
+ day_number += (n * 7)
150
+
151
+ Date.new(year, month, day_number)
152
+ end
153
+
154
+
155
+
156
+
157
+
158
+
159
+ # determines the date of the last specified weekday in a month
160
+ def get_last_wday_in_month(year, month, wday)
161
+ last_day_in_month = Date.new(year, month, -1)
162
+ wday_of_last_day = last_day_in_month.wday
163
+ wday_of_last_day += 7 if wday_of_last_day < wday
164
+ difference = wday_of_last_day - wday
165
+ date = last_day_in_month - difference
166
+ date
167
+ end
168
+
169
+
170
+ end
171
+
172
+
173
+
@@ -0,0 +1,294 @@
1
+ require File.dirname(__FILE__) + '/modified_weekday'
2
+
3
+ # Encapsulates the specification for a public Holiday, from which Named Public Holidays for any year can be generated.
4
+
5
+ class PublicHolidaySpecification
6
+ include Enumerable
7
+
8
+ attr_reader :name, :day, :month, :uses_class_method, :klass, :method_name, :take_before, :take_after
9
+
10
+
11
+ @@month_names = {'January' => 1, 'February' => 2, 'March' => 3, 'April'=> 4,
12
+ 'May' => 5, 'June' => 6, 'July' => 7, 'August' => 8,
13
+ 'September' => 9, 'October' => 10, 'November' => 11, 'December' => 12}
14
+
15
+ @@valid_day_names = [:sunday, :monday, :tuesday, :wednesday, :thursday, :friday, :saturday]
16
+
17
+ # Instantiates a PublicHolidaySpecification object.
18
+ #
19
+ # params: key-value pairs as follows:
20
+ # * name: Name given to this public holiday. Mandatory.
21
+ # * years: Either :all, a single year, or a range. Mandatory.
22
+ # * month: Either an English month name, or month number in range 1-12. Mandatory.
23
+ # * day: Either a number, or a phrase like :first_monday, :third_tuesday, :last_thursday. Mandatory.
24
+ # * take_after: An array, specifying the names or the numbers of the days on which, if the holiday falls on this day, will
25
+ # be taken on the first working day after. Defaults to an empty array, i.e., no adjustment takes place.
26
+ # * take_before: An array, specifying the names or the numbers of the days on which, if the holiday falls on this day, will
27
+ # be taken on the last working day before. Defaults to an empty array, i.e., no adjustment takes place.
28
+ #
29
+ # e.g.
30
+ # phs = PublicHolidaySpecification.new(:name => 'Christmas', :years => :all, :month => 12, :day => 25, :take_after => ['saturday', 'sunday'])
31
+ #
32
+ # or
33
+ # * name: Name given to this public holiday. Mandatory.
34
+ # * years: Either :all, a single year, or a range. Mandatory
35
+ # * class_method: Name of a class method that takes a year and returns a date
36
+ #
37
+ # e.g.
38
+ # phs = PublicHolidaySpecification.new(:name => 'Good Friday', :years => :all, :class_method => 'ReligiousFestival.good_friday')
39
+ #
40
+ def initialize(params)
41
+ @name = nil
42
+ @years = nil
43
+ @month = nil
44
+ @day = nil
45
+ @uses_class_method = false
46
+ @klass = nil
47
+ @method_name = nil
48
+ @take_after = Array.new
49
+ @take_before = Array.new
50
+
51
+ validate_params(params)
52
+ end
53
+
54
+
55
+ # Instantiates a PublicHolidaySpecification from a yaml definition file
56
+ def self.instantiate_from_yaml_definition(filename, name, yaml_spec)
57
+ raise ArgumentError.new("Invalid definition of #{name} in public_holidays section of #{filename}") if !yaml_spec.is_a? Hash
58
+ params = Hash.new
59
+ params[:name] = name
60
+ yaml_spec.each do |key, value|
61
+ key = key.to_sym if key.is_a? String
62
+ unless key == :class_method
63
+ value = value.to_sym if value.is_a? String
64
+ end
65
+ params[key] = value
66
+ end
67
+ phs = PublicHolidaySpecification.new(params)
68
+ phs
69
+ end
70
+
71
+
72
+ # Returns true if a class method is used to calaculate the date a holiday falls on
73
+ def uses_class_method?
74
+ @uses_class_method
75
+ end
76
+
77
+
78
+
79
+
80
+ # Returns true if the years value for this PublicHolidaySpecification includes the specified year.
81
+ def applies_to_year?(year)
82
+ @years.include?(year)
83
+ end
84
+
85
+
86
+
87
+ def <=>(other)
88
+ self.sort_value <=> other.sort_value
89
+ end
90
+
91
+
92
+
93
+
94
+ # Displays human_readable form of the PublicHolidaySpecification
95
+ def to_s
96
+ str = @name + "\n"
97
+ str += sprintf(" %014s: %s\n", 'years', @years)
98
+ if @uses_class_method
99
+ str += sprintf(" %14s: %s.%s\n\n", 'class_method', @klass, @method_name)
100
+ else
101
+ str += sprintf(" %14s: %s\n", 'month', @month)
102
+ str += sprintf(" %14s: %s\n", 'day', @day)
103
+ str += sprintf(" %14s: %s\n\n", 'carry_forward', @carry_forward)
104
+ end
105
+ end
106
+
107
+
108
+ private
109
+ def public_holiday_on_actual_date?(date)
110
+ result = false
111
+ if @years.include?(date.year) && @month == date.month
112
+ if @day.is_a? ModifiedWeekday
113
+ result = true if ModifiedWeekday.new(date) == @day
114
+ else
115
+ result = true if @day == date.day
116
+ end
117
+ end
118
+ result
119
+ end
120
+
121
+
122
+
123
+
124
+ def validate_params(params)
125
+
126
+ params.each do |key, value|
127
+ case key
128
+ when :name
129
+ @name = value
130
+ when :years
131
+ @years = validate_years(value)
132
+ when :month
133
+ @month = validate_month(value)
134
+ when :day
135
+ @day = validate_day(value)
136
+ when :take_before
137
+ @take_before = validate_take_before_after(value)
138
+ when :take_after
139
+ @take_after = validate_take_before_after(value)
140
+ when :class_method
141
+ validate_class_method(value)
142
+ else
143
+ raise ArgumentError.new("Invalid parameter passed to PublicHolidaySpecification.new: #{key} => #{value}")
144
+ end
145
+ end
146
+
147
+ missing_params = any_mandatory_params_nil?
148
+ if missing_params.size != 0
149
+ raise ArgumentError.new("Mandatory parameters are missing in a call to PublicHolidaySpecification.new: #{missing_params.join(', ')}")
150
+ end
151
+ end
152
+
153
+
154
+ def validate_class_method(value)
155
+ class_method = value
156
+ classname, method_name = value.split('.')
157
+ klass = Kernel.const_get(classname)
158
+
159
+ begin
160
+ valid_method = klass.respond_to?(method_name)
161
+ rescue NameError => err
162
+ puts "Unknown Class passed to PublicHolidaySpecification.new as class_method parameter: #{class_method}"
163
+ raise
164
+ end
165
+
166
+ if !valid_method
167
+ raise NameError.new("Unknown method passed to PublicHolidaySpecification.new as class_method parameter: #{class_method}")
168
+ end
169
+
170
+ @uses_class_method = true
171
+ @klass = klass
172
+ @method_name = method_name
173
+ end
174
+
175
+
176
+
177
+
178
+
179
+ def validate_years(value)
180
+ if value == :all
181
+ @years = (0..9999)
182
+ elsif value.class == Fixnum
183
+ @years = (value..value)
184
+ elsif value.class == Range
185
+ @years = value
186
+ else
187
+ raise ArgumentError.new("Invalid value passed as years parameter. Must be a Range, Fixnum or :all")
188
+ end
189
+ end
190
+
191
+
192
+
193
+ def validate_month(month)
194
+ if month.is_a?(String) && @@month_names.has_key?(month)
195
+ @month = @@month_names[month]
196
+ elsif month.is_a?(Fixnum) && (1..12).include?(month)
197
+ @month = month
198
+ else
199
+ raise ArgumentError.new("Invalid month passed to PublicHolidaySpecification.new: #{month}")
200
+ end
201
+ end
202
+
203
+
204
+
205
+ def validate_day(day)
206
+ if day.is_a?(Symbol)
207
+ @day = ModifiedWeekday.new(day)
208
+ elsif day.is_a?(Fixnum) && (1..31).include?(day)
209
+ @day = day
210
+ else
211
+ raise ArgumentError.new("Invalid value passed as :day parameter to PublicHolidaySpecification.new: #{day}")
212
+ end
213
+ end
214
+
215
+ #
216
+ # def validate_carry_forward(value)
217
+ # if !value.is_a?(TrueClass) && !value.is_a?(FalseClass)
218
+ # raise ArgumentError.new(':carry_forward value passed to PublicHolidaySpecification.new must be true or false')
219
+ # end
220
+ # @carry_forward = value
221
+ # end
222
+
223
+ # validates parameters passed with the take_before or take_after keywords, and returns an array of day numbers
224
+ def validate_take_before_after(day_array)
225
+ if !day_array.is_a? Array
226
+ raise ArgumentError.new('take_before or take_after parameters must be an array')
227
+ end
228
+
229
+ day_number_array = Array.new
230
+ day_array.each do |day|
231
+ day_number_array << validate_day_name_or_number(day)
232
+ end
233
+ day_number_array
234
+ end
235
+
236
+
237
+ def validate_day_name_or_number(day)
238
+ day_number = nil
239
+ if day.is_a? Fixnum
240
+ day_number = validate_day_number(day)
241
+ else
242
+ day_number = validate_day_name(day)
243
+ end
244
+ day_number
245
+ end
246
+
247
+
248
+ def validate_day_number(day)
249
+ if day < 0 || day > 6
250
+ raise ArgumentError.new('day number passed as take_before and take_after parameters must be in range 0-6')
251
+ end
252
+ day
253
+ end
254
+
255
+
256
+ def validate_day_name(day)
257
+ day_number = nil
258
+ if day.is_a? String
259
+ day_sym = day.downcase.to_sym
260
+ elsif !day.is_a? Symbol
261
+ raise ArgumentError.new("day passed to take_before and take_after must be a Number, String or Symbol. Is #{day.class}")
262
+ else
263
+ day_sym = day
264
+ end
265
+ day_number = @@valid_day_names.index(day_sym)
266
+ if day_number.nil?
267
+ raise ArgumentError.new("#{day} is not a valid day name")
268
+ end
269
+ day_number
270
+ end
271
+
272
+
273
+
274
+
275
+
276
+
277
+ def any_mandatory_params_nil?
278
+ return_array = []
279
+ return_array << :name if !@name
280
+ return_array << :years if !@years
281
+ if !@uses_class_method
282
+ return_array << :month if !@month
283
+ return_array << :day if !@day
284
+ end
285
+ return_array
286
+ end
287
+
288
+ end
289
+
290
+
291
+
292
+
293
+
294
+