holidays 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. data/LICENSE +21 -0
  2. data/README +76 -0
  3. data/REFERENCES +16 -0
  4. data/data/SYNTAX +111 -0
  5. data/data/au.yaml +106 -0
  6. data/data/build_defs.rb +152 -0
  7. data/data/ca.yaml +141 -0
  8. data/data/de.yaml +115 -0
  9. data/data/dk.yaml +117 -0
  10. data/data/es.yaml +161 -0
  11. data/data/fr.yaml +70 -0
  12. data/data/gb.yaml +106 -0
  13. data/data/ie.yaml +58 -0
  14. data/data/index.yaml +21 -0
  15. data/data/is.yaml +136 -0
  16. data/data/it.yaml +66 -0
  17. data/data/mx.yaml +106 -0
  18. data/data/nl.yaml +67 -0
  19. data/data/north_america_informal.yaml +49 -0
  20. data/data/pt.yaml +85 -0
  21. data/data/se.yaml +91 -0
  22. data/data/united_nations.yaml +188 -0
  23. data/data/us.yaml +81 -0
  24. data/data/za.yaml +78 -0
  25. data/lib/holidays.rb +404 -0
  26. data/lib/holidays/MANIFEST +23 -0
  27. data/lib/holidays/au.rb +41 -0
  28. data/lib/holidays/ca.rb +68 -0
  29. data/lib/holidays/de.rb +52 -0
  30. data/lib/holidays/dk.rb +47 -0
  31. data/lib/holidays/es.rb +52 -0
  32. data/lib/holidays/europe.rb +215 -0
  33. data/lib/holidays/fr.rb +36 -0
  34. data/lib/holidays/gb.rb +40 -0
  35. data/lib/holidays/ie.rb +32 -0
  36. data/lib/holidays/is.rb +61 -0
  37. data/lib/holidays/it.rb +35 -0
  38. data/lib/holidays/mx.rb +51 -0
  39. data/lib/holidays/nl.rb +36 -0
  40. data/lib/holidays/north_america.rb +107 -0
  41. data/lib/holidays/pt.rb +52 -0
  42. data/lib/holidays/scandinavia.rb +114 -0
  43. data/lib/holidays/se.rb +52 -0
  44. data/lib/holidays/united_nations.rb +24 -0
  45. data/lib/holidays/us.rb +48 -0
  46. data/lib/holidays/za.rb +35 -0
  47. data/test/defs/test_defs_au.rb +36 -0
  48. data/test/defs/test_defs_ca.rb +29 -0
  49. data/test/defs/test_defs_de.rb +46 -0
  50. data/test/defs/test_defs_dk.rb +30 -0
  51. data/test/defs/test_defs_es.rb +57 -0
  52. data/test/defs/test_defs_europe.rb +240 -0
  53. data/test/defs/test_defs_fr.rb +26 -0
  54. data/test/defs/test_defs_gb.rb +36 -0
  55. data/test/defs/test_defs_ie.rb +21 -0
  56. data/test/defs/test_defs_is.rb +33 -0
  57. data/test/defs/test_defs_it.rb +25 -0
  58. data/test/defs/test_defs_mx.rb +22 -0
  59. data/test/defs/test_defs_nl.rb +24 -0
  60. data/test/defs/test_defs_north_america.rb +54 -0
  61. data/test/defs/test_defs_pt.rb +32 -0
  62. data/test/defs/test_defs_scandinavia.rb +75 -0
  63. data/test/defs/test_defs_se.rb +32 -0
  64. data/test/defs/test_defs_us.rb +36 -0
  65. data/test/defs/test_defs_za.rb +25 -0
  66. metadata +118 -0
data/data/us.yaml ADDED
@@ -0,0 +1,81 @@
1
+ # United States holiday definitions for the Ruby Holiday gem.
2
+ #
3
+ # Updated: 2008-11-24.
4
+ # Source: http://en.wikipedia.org/wiki/Public_holidays_of_the_United_States
5
+ ---
6
+ months:
7
+ 0:
8
+ - name: Good Friday
9
+ regions: [us]
10
+ function: easter(year)-2
11
+ type: informal
12
+ 1:
13
+ - name: New Year's Day
14
+ regions: [us]
15
+ mday: 1
16
+ observed: to_weekday_if_weekend
17
+ - name: Martin Luther King, Jr. Day
18
+ week: 3
19
+ regions: [us]
20
+ wday: 1
21
+ - name: Inauguration Day
22
+ function: us_inauguration_day(year)
23
+ regions: [us_dc]
24
+ 2:
25
+ - name: Presidents' Day
26
+ week: 3
27
+ regions: [us]
28
+ wday: 1
29
+ 5:
30
+ - name: Memorial Day
31
+ week: -1
32
+ regions: [us]
33
+ wday: 1
34
+ 7:
35
+ - name: Independence Day
36
+ regions: [us]
37
+ mday: 4
38
+ observed: to_weekday_if_weekend
39
+ 9:
40
+ - name: Labor Day
41
+ week: 1
42
+ regions: [us]
43
+ wday: 1
44
+ 10:
45
+ - name: Columbus Day
46
+ week: 2
47
+ regions: [us]
48
+ wday: 1
49
+ 11:
50
+ - name: Veterans Day
51
+ regions: [us]
52
+ mday: 11
53
+ observed: to_weekday_if_weekend
54
+ - name: Thanksgiving
55
+ week: 4
56
+ regions: [us]
57
+ wday: 4
58
+ 12:
59
+ - name: Christmas Day
60
+ regions: [us]
61
+ mday: 25
62
+ observed: to_weekday_if_weekend
63
+ methods:
64
+ us_inauguration_day: |
65
+ # January 20, every fourth year, following Presidential election
66
+ def self.us_inauguration_day(year)
67
+ year % 4 == 1 ? 20 : nil
68
+ end
69
+ tests: |
70
+ {Date.civil(2008,1,1) => 'New Year\'s Day',
71
+ Date.civil(2008,1,21) => 'Martin Luther King, Jr. Day',
72
+ Date.civil(2008,2,18) => 'Presidents\' Day',
73
+ Date.civil(2008,5,26) => 'Memorial Day',
74
+ Date.civil(2008,7,4) => 'Independence Day',
75
+ Date.civil(2008,9,1) => 'Labor Day',
76
+ Date.civil(2008,10,13) => 'Columbus Day',
77
+ Date.civil(2008,11,11) => 'Veterans Day',
78
+ Date.civil(2008,11,27) => 'Thanksgiving',
79
+ Date.civil(2008,12,25) => 'Christmas Day'}.each do |date, name|
80
+ assert_equal name, Holidays.on(date, :us)[0][:name]
81
+ end
data/data/za.yaml ADDED
@@ -0,0 +1,78 @@
1
+ # South African holiday definitions for the Ruby Holiday gem.
2
+ #
3
+ # Updated: 2008-11-29.
4
+ # Sources:
5
+ # - http://en.wikipedia.org/wiki/Public_holidays_in_South_Africa
6
+ # - http://www.info.gov.za/aboutsa/holidays.htm
7
+ ---
8
+ months:
9
+ 0:
10
+ - name: Good Friday
11
+ regions: [za]
12
+ function: easter(year)-2
13
+ - name: Family Day
14
+ regions: [za]
15
+ function: easter(year)+1
16
+ 1:
17
+ - name: New Year's Day
18
+ regions: [za]
19
+ mday: 1
20
+ observed: to_monday_if_sunday
21
+ 3:
22
+ - name: Human Rights Day
23
+ regions: [za]
24
+ mday: 21
25
+ observed: to_monday_if_sunday
26
+ 4:
27
+ - name: Freedom Day
28
+ regions: [za]
29
+ mday: 27
30
+ observed: to_monday_if_sunday
31
+ 5:
32
+ - name: Workers Day
33
+ regions: [za]
34
+ mday: 1
35
+ observed: to_monday_if_sunday
36
+ 6:
37
+ - name: Youth Day
38
+ regions: [za]
39
+ mday: 16
40
+ observed: to_monday_if_sunday
41
+ 8:
42
+ - name: National Women's Day
43
+ regions: [za]
44
+ mday: 9
45
+ observed: to_monday_if_sunday
46
+ 9:
47
+ - name: Heritage Day
48
+ regions: [za]
49
+ mday: 24
50
+ observed: to_monday_if_sunday
51
+ 12:
52
+ - name: Day of Reconciliation
53
+ regions: [za]
54
+ mday: 16
55
+ observed: to_monday_if_sunday
56
+ - name: Christmas Day
57
+ regions: [za]
58
+ mday: 25
59
+ observed: to_monday_if_sunday
60
+ - name: Day of Goodwill
61
+ regions: [za]
62
+ mday: 26
63
+ observed: to_weekday_if_boxing_weekend
64
+ tests: |
65
+ {Date.civil(2007,1,1) => 'New Year\'s Day',
66
+ Date.civil(2007,3,21) => 'Human Rights Day',
67
+ Date.civil(2007,4,6) => 'Good Friday',
68
+ Date.civil(2007,4,9) => 'Family Day',
69
+ Date.civil(2007,4,27) => 'Freedom Day',
70
+ Date.civil(2007,5,1) => 'Workers Day',
71
+ Date.civil(2007,6,16) => 'Youth Day',
72
+ Date.civil(2007,8,9) => 'National Women\'s Day',
73
+ Date.civil(2007,9,24) => 'Heritage Day',
74
+ Date.civil(2007,12,16) => 'Day of Reconciliation',
75
+ Date.civil(2007,12,25) => 'Christmas Day',
76
+ Date.civil(2007,12,26) => 'Day of Goodwill'}.each do |date, name|
77
+ assert_equal name, Holidays.on(date, :za, :informal)[0][:name]
78
+ end
data/lib/holidays.rb ADDED
@@ -0,0 +1,404 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'digest/md5'
4
+ require 'date'
5
+
6
+ # == Region options
7
+ # Holidays can be defined as belonging to one or more regions and sub regions.
8
+ # The Holidays#on, Holidays#between, Date#holidays and Date#holiday? methods
9
+ # each allow you to specify a specific region.
10
+ #
11
+ # There are several different ways that you can specify a region:
12
+ #
13
+ # [<tt>:region</tt>]
14
+ # By region. For example, return holidays in the Canada with <tt>:ca</tt>.
15
+ # [<tt>:region_</tt>]
16
+ # By region and sub regions. For example, return holidays in Germany
17
+ # and all its sub regions with <tt>:de_</tt>.
18
+ # [<tt>:region_sub</tt>]
19
+ # By sub region. Return national holidays in Spain plus holidays in Spain's
20
+ # Valencia region with <tt>:es_v</tt>.
21
+ # [<tt>:any</tt>]
22
+ # Any region. Return holidays from any loaded region.
23
+ #
24
+ # == Other options
25
+ # [<tt>:observed</tt>] Return holidays on the day they are observed (e.g. on a Monday if they fall on a Sunday).
26
+ # [<tt>:informal</tt>] Include informal holidays (e.g. Valentine's Day)
27
+ #
28
+ # == Examples
29
+ # Return all holidays in the <tt>:ca</tt> and <tt>:us</tt> regions on the day that they are
30
+ # observed.
31
+ #
32
+ # Holidays.between(from, to, :ca, :us, :observed)
33
+ #
34
+ # Return all holidays in <tt>:ca</tt> and any <tt>:ca</tt> sub-region.
35
+ #
36
+ # Holidays.between(from, to, :ca_)
37
+ #
38
+ # Return all holidays in <tt>:ca_bc</tt> sub-region (which includes the <tt>:ca</tt>), including informal holidays.
39
+ #
40
+ # Holidays.between(from, to, :ca_bc, :informal)
41
+ module Holidays
42
+ # Exception thrown when an unknown region is requested.
43
+ class UnkownRegionError < ArgumentError; end
44
+
45
+ VERSION = '0.9.0'
46
+
47
+ @@regions = []
48
+ @@holidays_by_month = {}
49
+ @@proc_cache = {}
50
+
51
+ WEEKS = {:first => 1, :second => 2, :third => 3, :fourth => 4, :fifth => 5, :last => -1}
52
+ MONTH_LENGTHS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
53
+ DAY_SYMBOLS = Date::DAYNAMES.collect { |n| n.downcase.intern }
54
+
55
+ # Get all holidays on a given date.
56
+ #
57
+ # [<tt>date</tt>] A Date object.
58
+ # [<tt>:options</tt>] One or more region symbols, <tt>:informal</tt> and/or <tt>:observed</tt>.
59
+ #
60
+ # Returns an array of hashes or nil. See Holidays#between for the output
61
+ # format.
62
+ #
63
+ # Also available via Date#holidays.
64
+ def self.on(date, *options)
65
+ self.between(date, date, options)
66
+ end
67
+
68
+ # Get all holidays occuring between two dates, inclusively.
69
+ #
70
+ # Returns an array of hashes or nil.
71
+ #
72
+ # Each holiday is returned as a hash with the following fields:
73
+ # [<tt>start_date</tt>] Ruby Date object.
74
+ # [<tt>end_date</tt>] Ruby Date object.
75
+ # [<tt>options</tt>] One or more region symbols, <tt>:informal</tt> and/or <tt>:observed</tt>.
76
+ #
77
+ # ==== Example
78
+ # from = Date.civil(2008,7,1)
79
+ # to = Date.civil(2008,7,31)
80
+ #
81
+ # Holidays.between(from, to, :ca, :us)
82
+ # => [{:name => 'Canada Day', :regions => [:ca]...}
83
+ # {:name => 'Independence Day'', :regions => [:us], ...}]
84
+ def self.between(start_date, end_date, *options)
85
+ regions, observed, informal = parse_options(options)
86
+ holidays = []
87
+
88
+ dates = {}
89
+ (start_date..end_date).each do |date|
90
+ # Always include month '0' for variable-month holidays
91
+ dates[date.year] = [0] unless dates[date.year]
92
+ # TODO: test this, maybe should push then flatten
93
+ dates[date.year] << date.month unless dates[date.year].include?(date.month)
94
+ end
95
+
96
+ dates.each do |year, months|
97
+ months.each do |month|
98
+ next unless hbm = @@holidays_by_month[month]
99
+
100
+ hbm.each do |h|
101
+ next unless in_region?(regions, h[:regions])
102
+
103
+ # Skip informal holidays unless they have been requested
104
+ next if h[:type] == :informal and not informal
105
+
106
+ if h[:function]
107
+ # Holiday definition requires a calculation
108
+ result = call_proc(h[:function], year)
109
+
110
+ # Procs may return either Date or an integer representing mday
111
+ if result.kind_of?(Date)
112
+ month = result.month
113
+ mday = result.mday
114
+ else
115
+ mday = result
116
+ end
117
+ else
118
+ # Calculate the mday
119
+ mday = h[:mday] || Date.calculate_mday(year, month, h[:week], h[:wday])
120
+ end
121
+
122
+ # Silently skip bad mdays
123
+ begin
124
+ date = Date.civil(year, month, mday)
125
+ rescue; next; end
126
+
127
+ # If the :observed option is set, calculate the date when the holiday
128
+ # is observed.
129
+ if observed and h[:observed]
130
+ date = call_proc(h[:observed], date)
131
+ end
132
+
133
+ if date.between?(start_date, end_date)
134
+ holidays << {:date => date, :name => h[:name], :regions => h[:regions]}
135
+ end
136
+
137
+ end
138
+ end
139
+ end
140
+
141
+ holidays
142
+ end
143
+
144
+ # Merge a new set of definitions into the Holidays module.
145
+ #
146
+ # This method is automatically called when including holiday definition
147
+ # files.
148
+ def self.merge_defs(regions, holidays) # :nodoc:
149
+ @@regions = @@regions | regions
150
+ @@regions.uniq!
151
+
152
+ holidays.each do |month, holiday_defs|
153
+ @@holidays_by_month[month] = [] unless @@holidays_by_month[month]
154
+ holiday_defs.each do |holiday_def|
155
+
156
+ exists = false
157
+ @@holidays_by_month[month].each do |ex|
158
+ # TODO: gross.
159
+ if ex[:name] == holiday_def[:name] and ex[:wday] == holiday_def[:wday] and ex[:mday] == holiday_def[:mday] and ex[:week] == holiday_def[:week] and ex[:function_id] == holiday_def[:function_id] and ex[:type] == holiday_def[:type] and ex[:observed_id] == holiday_def[:observed_id]
160
+ # append regions
161
+ ex[:regions] << holiday_def[:regions]
162
+
163
+ # Should do this once we're done
164
+ ex[:regions].flatten!
165
+ ex[:regions].uniq!
166
+ exists = true
167
+ end
168
+ end
169
+
170
+ @@holidays_by_month[month] << holiday_def unless exists
171
+ end
172
+ end
173
+ end
174
+
175
+ # Get the date of Easter Sunday in a given year. From Easter Sunday, it is
176
+ # possible to calculate many traditional holidays in Western countries.
177
+ # Returns a Date object.
178
+ def self.easter(year)
179
+ y = year
180
+ a = y % 19
181
+ b = y / 100
182
+ c = y % 100
183
+ d = b / 4
184
+ e = b % 4
185
+ f = (b + 8) / 25
186
+ g = (b - f + 1) / 3
187
+ h = (19 * a + b - d - g + 15) % 30
188
+ i = c / 4
189
+ k = c % 4
190
+ l = (32 + 2 * e + 2 * i - h - k) % 7
191
+ m = (a + 11 * h + 22 * l) / 451
192
+ month = (h + l - 7 * m + 114) / 31
193
+ day = ((h + l - 7 * m + 114) % 31) + 1
194
+ Date.civil(year, month, day)
195
+ end
196
+
197
+ # Move date to Monday if it occurs on a Sunday.
198
+ # Used as a callback function.
199
+ def self.to_monday_if_sunday(date)
200
+ date += 1 if date.wday == 0
201
+ date
202
+ end
203
+
204
+ # Move date to Monday if it occurs on a Saturday on Sunday.
205
+ # Used as a callback function.
206
+ def self.to_monday_if_weekend(date)
207
+ date += 1 if date.wday == 0
208
+ date += 2 if date.wday == 6
209
+ date
210
+ end
211
+
212
+ # Move Boxing Day if it falls on a weekend, leaving room for Christmas.
213
+ # Used as a callback function.
214
+ def self.to_weekday_if_boxing_weekend(date)
215
+ date += 2 if date.wday == 6 or date.wday == 0
216
+ date
217
+ end
218
+
219
+ # Move date to Monday if it occurs on a Sunday or to Friday if it occurs on a
220
+ # Saturday.
221
+ # Used as a callback function.
222
+ def self.to_weekday_if_weekend(date)
223
+ date += 1 if date.wday == 0
224
+ date -= 1 if date.wday == 6
225
+ date
226
+ end
227
+
228
+ private
229
+ # Returns [(arr)regions, (bool)observed, (bool)informal]
230
+ def self.parse_options(*options) # :nodoc:
231
+ options.flatten!
232
+ observed = options.delete(:observed) ? true : false
233
+ informal = options.delete(:informal) ? true : false
234
+ regions = parse_regions(options)
235
+ return regions, observed, informal
236
+ end
237
+
238
+ # Check regions against list of supported regions and return an array of
239
+ # symbols.
240
+ #
241
+ # If a wildcard region is found (e.g. <tt>:ca_</tt>) it is expanded into all
242
+ # of its available sub regions.
243
+ def self.parse_regions(regions) # :nodoc:
244
+ regions = [regions] unless regions.kind_of?(Array)
245
+ return [:any] if regions.empty?
246
+
247
+ regions = regions.collect { |r| r.to_sym }
248
+
249
+ # Found sub region wild-card
250
+ regions.delete_if do |reg|
251
+ if reg.to_s =~ /_$/
252
+ regions << @@regions.select { |dr| dr.to_s =~ Regexp.new("^#{reg}") }
253
+ true
254
+ end
255
+ end
256
+
257
+ regions.flatten!
258
+
259
+ raise UnkownRegionError unless regions.all? { |r| r == :any or @@regions.include?(r) }
260
+
261
+ regions
262
+ end
263
+
264
+ # Check sub regions.
265
+ #
266
+ # When request :any, all holidays should be returned.
267
+ # When requesting :ca_bc, holidays in :ca or :ca_bc should be returned.
268
+ # When requesting :ca, holidays in :ca but not its subregions should be returned.
269
+ def self.in_region?(requested, available) # :nodoc:
270
+ return true if requested.include?(:any)
271
+
272
+ # When an underscore is encountered, derive the parent regions
273
+ # symbol and include both in the requested array.
274
+ requested = requested.collect do |r|
275
+ r.to_s =~ /_/ ? [r, r.to_s.gsub(/_[\w]*$/, '').to_sym] : r
276
+ end
277
+
278
+ requested = requested.flatten.uniq
279
+
280
+ available.any? { |avail| requested.include?(avail) }
281
+ end
282
+
283
+ # Call a proc function defined in a holiday definition file.
284
+ #
285
+ # Procs are cached.
286
+ #
287
+ # ==== Benchmarks
288
+ #
289
+ # Lookup Easter Sunday, with caching, by number of iterations:
290
+ #
291
+ # user system total real
292
+ # 0001 0.000000 0.000000 0.000000 ( 0.000000)
293
+ # 0010 0.000000 0.000000 0.000000 ( 0.000000)
294
+ # 0100 0.078000 0.000000 0.078000 ( 0.078000)
295
+ # 1000 0.641000 0.000000 0.641000 ( 0.641000)
296
+ # 5000 3.172000 0.015000 3.187000 ( 3.219000)
297
+ #
298
+ # Lookup Easter Sunday, without caching, by number of iterations:
299
+ #
300
+ # user system total real
301
+ # 0001 0.000000 0.000000 0.000000 ( 0.000000)
302
+ # 0010 0.016000 0.000000 0.016000 ( 0.016000)
303
+ # 0100 0.125000 0.000000 0.125000 ( 0.125000)
304
+ # 1000 1.234000 0.000000 1.234000 ( 1.234000)
305
+ # 5000 6.094000 0.031000 6.125000 ( 6.141000)
306
+ def self.call_proc(function, year) # :nodoc:
307
+ proc_key = Digest::MD5.hexdigest("#{function.to_s}_#{year.to_s}")
308
+ @@proc_cache[proc_key] = function.call(year) unless @@proc_cache[proc_key]
309
+ @@proc_cache[proc_key]
310
+ end
311
+ end
312
+
313
+ # === Extending Ruby's Date class with the Holidays gem
314
+ # The Holidays gem automatically extends Ruby's Date class and gives you access
315
+ # to three new methods: holiday?, #holidays and #calculate_mday.
316
+ #
317
+ # ==== Examples
318
+ # Lookup Canada Day in the <tt>:ca</tt> region
319
+ # Date.civil(2008,7,1).holiday?(:ca)
320
+ # => true
321
+ #
322
+ # Lookup Canada Day in the <tt>:fr</tt> region
323
+ # Date.civil(2008,7,1).holiday?(:fr)
324
+ # => false
325
+ #
326
+ # Lookup holidays on North America in January 1.
327
+ # Date.civil(2008,1,1).holidays(:ca, :mx, :us, :informal, :observed)
328
+ # => [{:name => 'New Year\'s Day'...}]
329
+ class Date
330
+ include Holidays
331
+
332
+ # Get holidays on the current date.
333
+ #
334
+ # Returns an array of hashes or nil. See Holidays#between for options
335
+ # and the output format.
336
+ #
337
+ # Date.civil('2008-01-01').holidays(:ca_)
338
+ # => [{:name => 'New Year\'s Day',...}]
339
+ #
340
+ # Also available via Holidays#on.
341
+ def holidays(*options)
342
+ Holidays.on(self, options)
343
+ end
344
+
345
+ # Check if the current date is a holiday.
346
+ #
347
+ # Returns true or false.
348
+ #
349
+ # Date.civil('2008-01-01').holiday?(:ca)
350
+ # => true
351
+ def holiday?(*options)
352
+ holidays = self.holidays(options)
353
+ holidays && !holidays.empty?
354
+ end
355
+
356
+ # Calculate day of the month based on the week number and the day of the
357
+ # week.
358
+ #
359
+ # ==== Parameters
360
+ # [<tt>year</tt>] Integer.
361
+ # [<tt>month</tt>] Integer from 1-12.
362
+ # [<tt>week</tt>] One of <tt>:first</tt>, <tt>:second</tt>, <tt>:third</tt>,
363
+ # <tt>:fourth</tt>, <tt>:fifth</tt> or <tt>:last</tt>.
364
+ # [<tt>wday</tt>] Day of the week as an integer from 0 (Sunday) to 6
365
+ # (Saturday) or as a symbol (e.g. <tt>:monday</tt>).
366
+ #
367
+ # Returns an integer.
368
+ #
369
+ # ===== Examples
370
+ # First Monday of January, 2008:
371
+ # Date.calculate_mday(2008, 1, :first, :monday)
372
+ # => 7
373
+ #
374
+ # Third Thursday of December, 2008:
375
+ # Date.calculate_mday(2008, 12, :third, :thursday)
376
+ # => 18
377
+ #
378
+ # Last Monday of January, 2008:
379
+ # Date.calculate_mday(2008, 1, :last, 1)
380
+ # => 28
381
+ #--
382
+ # see http://www.irt.org/articles/js050/index.htm
383
+ def self.calculate_mday(year, month, week, wday)
384
+ raise ArgumentError, "Week parameter must be one of Holidays::WEEKS (provided #{week})." unless WEEKS.include?(week) or WEEKS.has_value?(week)
385
+
386
+ unless wday.kind_of?(Numeric) and wday.between?(0,6) or DAY_SYMBOLS.index(wday)
387
+ raise ArgumentError, "Wday parameter must be an integer between 0 and 6 or one of Date::DAY_SYMBOLS."
388
+ end
389
+
390
+ week = WEEKS[week] if week.kind_of?(Symbol)
391
+ wday = DAY_SYMBOLS.index(wday) if wday.kind_of?(Symbol)
392
+
393
+ # :first, :second, :third, :fourth or :fifth
394
+ if week > 0
395
+ return ((week - 1) * 7) + 1 + ((7 + wday - Date.civil(year, month,(week-1)*7 + 1).wday) % 7)
396
+ end
397
+
398
+ days = MONTH_LENGTHS[month-1]
399
+
400
+ days = 29 if month == 1 and Date.civil(year,1,1).leap?
401
+
402
+ return days - ((Date.civil(year, month, days).wday - wday + 7) % 7)
403
+ end
404
+ end