rrule 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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