holidays 0.9.3 → 1.0.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 (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