rrule 0.0.0 → 0.1.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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -0
  3. data/.travis.yml +3 -0
  4. data/CHANGELOG.md +7 -0
  5. data/CONTRIBUTING.md +17 -0
  6. data/Gemfile +5 -0
  7. data/LICENSE.txt +202 -0
  8. data/README.md +71 -0
  9. data/Rakefile +30 -0
  10. data/lib/rrule.rb +26 -4
  11. data/lib/rrule/context.rb +137 -0
  12. data/lib/rrule/filters/by_month.rb +16 -0
  13. data/lib/rrule/filters/by_month_day.rb +16 -0
  14. data/lib/rrule/filters/by_week_day.rb +24 -0
  15. data/lib/rrule/filters/by_week_number.rb +16 -0
  16. data/lib/rrule/filters/by_year_day.rb +18 -0
  17. data/lib/rrule/frequencies/daily.rb +13 -0
  18. data/lib/rrule/frequencies/frequency.rb +34 -0
  19. data/lib/rrule/frequencies/monthly.rb +14 -0
  20. data/lib/rrule/frequencies/weekly.rb +29 -0
  21. data/lib/rrule/frequencies/yearly.rb +14 -0
  22. data/lib/rrule/generators/all_occurrences.rb +26 -0
  23. data/lib/rrule/generators/by_set_position.rb +35 -0
  24. data/lib/rrule/rule.rb +181 -0
  25. data/lib/rrule/weekday.rb +17 -0
  26. data/rrule.gemspec +20 -0
  27. data/spec/context_spec.rb +261 -0
  28. data/spec/filters/by_month_day_spec.rb +35 -0
  29. data/spec/filters/by_month_spec.rb +35 -0
  30. data/spec/filters/by_week_day_spec.rb +35 -0
  31. data/spec/filters/by_week_number_spec.rb +41 -0
  32. data/spec/filters/by_year_day_spec.rb +35 -0
  33. data/spec/frequencies/daily_spec.rb +55 -0
  34. data/spec/frequencies/monthly_spec.rb +57 -0
  35. data/spec/frequencies/weekly_spec.rb +57 -0
  36. data/spec/frequencies/yearly_spec.rb +52 -0
  37. data/spec/generators/all_occurrences_spec.rb +44 -0
  38. data/spec/generators/by_set_position_spec.rb +39 -0
  39. data/spec/rule_spec.rb +1988 -0
  40. data/spec/spec_helper.rb +21 -0
  41. data/spec/weekday_spec.rb +34 -0
  42. metadata +88 -8
@@ -0,0 +1,17 @@
1
+ module RRule
2
+ class Weekday
3
+ attr_reader :index, :ordinal
4
+
5
+ def initialize(index, ordinal = nil)
6
+ @index = index
7
+ @ordinal = ordinal
8
+ end
9
+
10
+ def self.parse(weekday)
11
+ match = /([+-]?\d)?([A-Z]{2})/.match(weekday)
12
+ index = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'].index(match[2])
13
+ ordinal = match[1] ? match[1].to_i : nil
14
+ new(index, ordinal)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,20 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'rrule'
3
+ s.version = '0.1.0'
4
+ s.date = '2016-06-06'
5
+ s.summary = 'RRule expansion'
6
+ s.description = 'A gem for expanding dates according to the RRule specification'
7
+ s.authors = ['Ryan Mitchell']
8
+ s.email = 'rmitchell@squareup.com'
9
+ s.files = `git ls-files`.split($/)
10
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
11
+ s.require_paths = ['lib']
12
+ s.homepage = 'http://rubygems.org/gems/rrule'
13
+
14
+ # Since Ruby 1.9.2, Time implementation uses a signed 63 bit integer, Bignum
15
+ # or Rational. This enables Time to finally work with years after 2038 which
16
+ # is critical for this library.
17
+ s.required_ruby_version = '>= 1.9.2'
18
+ s.add_runtime_dependency 'activesupport', '>= 4.1'
19
+ s.add_development_dependency 'rspec', '~> 3.4'
20
+ end
@@ -0,0 +1,261 @@
1
+ require 'spec_helper'
2
+
3
+ describe RRule::Context do
4
+ let(:context) do
5
+ RRule::Context.new(
6
+ { freq: 'DAILY', count: 3 },
7
+ Time.parse('Tue Sep 2 06:00:00 PDT 1997'),
8
+ 'America/Los_Angeles'
9
+ )
10
+ end
11
+
12
+ before(:each) { context.rebuild(1997, 1) }
13
+
14
+ describe '#year_length_in_days' do
15
+ subject { context.year_length_in_days }
16
+
17
+ context 'in a non leap year' do
18
+ before(:each) { context.rebuild(1997, 1) }
19
+
20
+ it { is_expected.to eql 365 }
21
+ end
22
+
23
+ context 'in a leap year' do
24
+ before(:each) { context.rebuild(2000, 1) }
25
+
26
+ it { is_expected.to eql 366 }
27
+ end
28
+ end
29
+
30
+ describe '#next_year_length_in_days' do
31
+ subject { context.next_year_length_in_days }
32
+
33
+ context 'in a year not prior to a leap year' do
34
+ before(:each) { context.rebuild(1997, 1) }
35
+
36
+ it { is_expected.to eql 365 }
37
+ end
38
+
39
+ context 'in a year prior to a leap year' do
40
+ before(:each) { context.rebuild(1999, 1) }
41
+
42
+ it { is_expected.to eql 366 }
43
+ end
44
+ end
45
+
46
+ describe '#first_day_of_year' do
47
+ subject { context.first_day_of_year }
48
+
49
+ it { is_expected.to eq Date.new(1997, 1, 1) }
50
+ end
51
+
52
+ describe '#month_by_day_of_year' do
53
+ subject(:month_by_day_of_year) { context.month_by_day_of_year }
54
+
55
+ context 'in a leap year' do
56
+ before(:each) { context.rebuild(2000, 1) }
57
+
58
+ it 'maps the day of the year to the month number' do
59
+ expect(month_by_day_of_year.length).to eql 366 + 7 # 7 padding days
60
+ expect(month_by_day_of_year[0]).to eql 1
61
+ expect(month_by_day_of_year[59]).to eql 2
62
+ expect(month_by_day_of_year[365]).to eql 12
63
+ end
64
+ end
65
+
66
+ context 'in a non leap year' do
67
+ before(:each) { context.rebuild(1997, 1) }
68
+
69
+ it 'maps the day of the year to the month number' do
70
+ expect(month_by_day_of_year.length).to eql 365 + 7 # 7 padding days
71
+ expect(month_by_day_of_year[0]).to eql 1
72
+ expect(month_by_day_of_year[59]).to eql 3
73
+ expect(month_by_day_of_year[364]).to eql 12
74
+ end
75
+ end
76
+ end
77
+
78
+ describe '#month_day_by_day_of_year' do
79
+ subject(:month_day_by_day_of_year) { context.month_day_by_day_of_year }
80
+
81
+ context 'in a leap year' do
82
+ before(:each) { context.rebuild(2000, 1) }
83
+
84
+ it 'maps the month day of the year to the month number' do
85
+ expect(month_day_by_day_of_year.length).to eql 366 + 7 # 7 padding days
86
+ expect(month_day_by_day_of_year[0]).to eql 1
87
+ expect(month_day_by_day_of_year[1]).to eql 2
88
+ expect(month_day_by_day_of_year[59]).to eql 29
89
+ expect(month_day_by_day_of_year[365]).to eql 31
90
+ end
91
+ end
92
+
93
+ context 'in a non leap year' do
94
+ before(:each) { context.rebuild(1997, 1) }
95
+
96
+ it 'maps the month day of the year to the month number' do
97
+ expect(month_day_by_day_of_year.length).to eql 365 + 7 # 7 padding days
98
+ expect(month_day_by_day_of_year[0]).to eql 1
99
+ expect(month_day_by_day_of_year[1]).to eql 2
100
+ expect(month_day_by_day_of_year[59]).to eql 1
101
+ expect(month_day_by_day_of_year[364]).to eql 31
102
+ end
103
+ end
104
+ end
105
+
106
+ describe '#negative_month_day_by_day_of_year' do
107
+ subject(:negative_month_day_by_day_of_year) { context.negative_month_day_by_day_of_year }
108
+
109
+ context 'in a leap year' do
110
+ before(:each) { context.rebuild(2000, 1) }
111
+
112
+ it 'maps the month day of the year to the month number' do
113
+ expect(negative_month_day_by_day_of_year.length).to eql 366 + 7 # 7 padding days
114
+ expect(negative_month_day_by_day_of_year[0]).to eql -31
115
+ expect(negative_month_day_by_day_of_year[1]).to eql -30
116
+ expect(negative_month_day_by_day_of_year[59]).to eql -1
117
+ expect(negative_month_day_by_day_of_year[365]).to eql -1
118
+ end
119
+ end
120
+
121
+ context 'in a non leap year' do
122
+ before(:each) { context.rebuild(1997, 1) }
123
+
124
+ it 'maps the month day of the year to the month number' do
125
+ expect(negative_month_day_by_day_of_year.length).to eql 365 + 7 # 7 padding days
126
+ expect(negative_month_day_by_day_of_year[0]).to eql -31
127
+ expect(negative_month_day_by_day_of_year[1]).to eql -30
128
+ expect(negative_month_day_by_day_of_year[59]).to eql -31
129
+ expect(negative_month_day_by_day_of_year[364]).to eql -1
130
+ end
131
+ end
132
+ end
133
+
134
+ describe '#week_number_by_day_of_year' do
135
+ subject(:week_number_by_day_of_year) { context.week_number_by_day_of_year }
136
+
137
+ context 'when the first day of the year is in the first week of that calendar-week-based year' do
138
+ before(:each) { context.rebuild(1997, 1) }
139
+
140
+ it 'is part of the current calendar-week-based year' do
141
+ expect(week_number_by_day_of_year[0]).to eql 1
142
+ end
143
+ end
144
+
145
+ context 'when the first day of the year is in the last week of the previous calendar-week-based year' do
146
+ before(:each) { context.rebuild(1999, 1) }
147
+
148
+ it 'is part of the previous calendar-week-based year' do
149
+ expect(week_number_by_day_of_year[0]).to eql 53
150
+ end
151
+ end
152
+
153
+ context 'when the last day of the year is in the last week of that calendar-week-based year' do
154
+ before(:each) { context.rebuild(1999, 1) }
155
+
156
+ it 'is part of the current calendar-week-based year' do
157
+ expect(week_number_by_day_of_year[364]).to eql 52
158
+ end
159
+ end
160
+
161
+ context 'when the last day of the year is in the first week of the next calendar-week-based year' do
162
+ before(:each) { context.rebuild(1997, 1) }
163
+
164
+ it 'is part of the next calendar-week-based year' do
165
+ expect(week_number_by_day_of_year[364]).to eql 1
166
+ end
167
+ end
168
+ end
169
+
170
+ describe '#negative_week_number_by_day_of_year' do
171
+ subject(:negative_week_number_by_day_of_year) { context.negative_week_number_by_day_of_year }
172
+
173
+ context 'when the first day of the year is in the first week of that calendar-week-based year' do
174
+ before(:each) { context.rebuild(1997, 1) }
175
+
176
+ it 'is part of the current calendar-week-based year' do
177
+ expect(negative_week_number_by_day_of_year[0]).to eql -52
178
+ end
179
+ end
180
+
181
+ context 'when the first day of the year is in the last week of the previous calendar-week-based year' do
182
+ before(:each) { context.rebuild(1999, 1) }
183
+
184
+ it 'is part of the previous calendar-week-based year' do
185
+ expect(negative_week_number_by_day_of_year[0]).to eql -1
186
+ end
187
+ end
188
+
189
+ context 'when the last day of the year is in the last week of that calendar-week-based year' do
190
+ before(:each) { context.rebuild(1999, 1) }
191
+
192
+ it 'is part of the current calendar-week-based year' do
193
+ expect(negative_week_number_by_day_of_year[364]).to eql -1
194
+ end
195
+ end
196
+
197
+ context 'when the last day of the year is in the first week of the next calendar-week-based year' do
198
+ before(:each) { context.rebuild(1997, 1) }
199
+
200
+ it 'is part of the next calendar-week-based year' do
201
+ expect(negative_week_number_by_day_of_year[364]).to eql -53
202
+ end
203
+ end
204
+ end
205
+
206
+ describe '#first_weekday_of_year' do
207
+ subject { context.first_weekday_of_year }
208
+
209
+ it { is_expected.to eq 3 }
210
+ end
211
+
212
+ describe '#weekday_by_day_of_year' do
213
+ subject { context.weekday_by_day_of_year }
214
+
215
+ it { is_expected.to start_with(3, 4, 5, 6, 0, 1, 2) }
216
+ end
217
+
218
+ describe '#elapsed_days_in_year_by_month' do
219
+ subject { context.elapsed_days_in_year_by_month }
220
+
221
+ context 'in a leap year' do
222
+ before(:each) { context.rebuild(2000, 1) }
223
+
224
+ it { is_expected.to start_with(0, 31, 60, 91) }
225
+ end
226
+
227
+ context 'in a non leap year' do
228
+ before(:each) { context.rebuild(1997, 1) }
229
+
230
+ it { is_expected.to start_with(0, 31, 59, 90) }
231
+ end
232
+ end
233
+
234
+ describe '#day_of_year_mask' do
235
+ let(:context) do
236
+ RRule::Context.new(
237
+ { freq: 'MONTHLY', count: 3, bynweekday: [RRule::Weekday.parse('3TU'), RRule::Weekday.parse('-2MO')] },
238
+ Time.parse('Wed Jan 1 00:00:00 PST 1997'),
239
+ 'America/Los_Angeles'
240
+ )
241
+ end
242
+
243
+ subject(:day_of_year_mask) { context.day_of_year_mask }
244
+
245
+ it 'correctly masks all days except the third Tuesday and the next-to-last Monday in January 1997' do
246
+ day_of_year_mask.each_with_index do |available, day_of_year|
247
+ expect(available).to be [19, 20].include?(day_of_year)
248
+ end
249
+ end
250
+
251
+ context 'when the month is advanced' do
252
+ before(:each) { context.rebuild(1997, 2) }
253
+
254
+ it 'correctly masks all days except the third Tuesday and the next-to-last Monday in February 1997' do
255
+ day_of_year_mask.each_with_index do |available, day_of_year|
256
+ expect(available).to be [48, 47].include?(day_of_year)
257
+ end
258
+ end
259
+ end
260
+ end
261
+ end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+
3
+ describe RRule::ByMonthDay do
4
+ let(:context) do
5
+ RRule::Context.new(
6
+ { freq: 'MONTHLY', count: 4 },
7
+ Time.parse('Wed Jan 1 00:00:00 PST 1997'),
8
+ 'America/Los_Angeles'
9
+ )
10
+ end
11
+
12
+ subject { described_class.new([3, -3], context).reject?(date.yday - 1) }
13
+
14
+ before(:each) { context.rebuild(1997, 1) }
15
+
16
+ describe '#reject?' do
17
+ context 'for the third day of the month' do
18
+ let(:date) { Date.new(1997, 1, 3) }
19
+
20
+ it { is_expected.to be false }
21
+ end
22
+
23
+ context 'for the fourth day of the month' do
24
+ let(:date) { Date.new(1997, 1, 4) }
25
+
26
+ it { is_expected.to be true }
27
+ end
28
+
29
+ context 'for the third-to-last day of the month' do
30
+ let(:date) { Date.new(1997, 1, 29) }
31
+
32
+ it { is_expected.to be false }
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+
3
+ describe RRule::ByMonth do
4
+ let(:context) do
5
+ RRule::Context.new(
6
+ { freq: 'MONTHLY', count: 4 },
7
+ Time.parse('Wed Jan 1 00:00:00 PST 1997'),
8
+ 'America/Los_Angeles'
9
+ )
10
+ end
11
+
12
+ subject { described_class.new([1, 3], context).reject?(date.yday) }
13
+
14
+ before(:each) { context.rebuild(1997, 1) }
15
+
16
+ describe '#reject?' do
17
+ context 'for a day in January' do
18
+ let(:date) { Date.new(1997, 1, 15) }
19
+
20
+ it { is_expected.to be false }
21
+ end
22
+
23
+ context 'for a day in February' do
24
+ let(:date) { Date.new(1997, 2, 15) }
25
+
26
+ it { is_expected.to be true }
27
+ end
28
+
29
+ context 'for a day in March' do
30
+ let(:date) { Date.new(1997, 3, 15) }
31
+
32
+ it { is_expected.to be false }
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+
3
+ describe RRule::ByWeekDay do
4
+ let(:context) do
5
+ RRule::Context.new(
6
+ { freq: 'WEEKLY', count: 4 },
7
+ Time.parse('Wed Jan 1 00:00:00 PST 1997'),
8
+ 'America/Los_Angeles'
9
+ )
10
+ end
11
+
12
+ subject { described_class.new([RRule::Weekday.parse('TU'), RRule::Weekday.parse('FR')], context).reject?(date.yday - 1) }
13
+
14
+ before(:each) { context.rebuild(1997, 1) }
15
+
16
+ describe '#reject?' do
17
+ context 'for the Friday of the week' do
18
+ let(:date) { Date.new(1997, 1, 3) }
19
+
20
+ it { is_expected.to be false }
21
+ end
22
+
23
+ context 'for the Saturday of the week' do
24
+ let(:date) { Date.new(1997, 1, 4) }
25
+
26
+ it { is_expected.to be true }
27
+ end
28
+
29
+ context 'for the Tuesday of the next week' do
30
+ let(:date) { Date.new(1997, 1, 7) }
31
+
32
+ it { is_expected.to be false }
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+
3
+ describe RRule::ByWeekNumber do
4
+ let(:context) do
5
+ RRule::Context.new(
6
+ { freq: 'YEARLY', wkst: 1 },
7
+ Time.parse('Wed Jan 1 00:00:00 PST 1997'),
8
+ 'America/Los_Angeles'
9
+ )
10
+ end
11
+
12
+ subject { described_class.new([2, -3], context).reject?(date.yday - 1) }
13
+
14
+ before(:each) { context.rebuild(1997, 1) }
15
+
16
+ describe '#reject?' do
17
+ context 'for the first week of the year' do
18
+ let(:date) { Date.new(1997, 1, 2) }
19
+
20
+ it { is_expected.to be true }
21
+ end
22
+
23
+ context 'for the second week of the year' do
24
+ let(:date) { Date.new(1997, 1, 8) }
25
+
26
+ it { is_expected.to be false }
27
+ end
28
+
29
+ context 'for the fourth week of the year' do
30
+ let(:date) { Date.new(1997, 1, 22) }
31
+
32
+ it { is_expected.to be true }
33
+ end
34
+
35
+ context 'for the third-to-last week of the year' do
36
+ let(:date) { Date.new(1997, 12, 9) }
37
+
38
+ it { is_expected.to be false }
39
+ end
40
+ end
41
+ end