holidays 0.9.3 → 1.0.0

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