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.
- checksums.yaml +4 -4
- data/.gitignore +4 -0
- data/.travis.yml +3 -0
- data/CHANGELOG.md +7 -0
- data/CONTRIBUTING.md +17 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +202 -0
- data/README.md +71 -0
- data/Rakefile +30 -0
- data/lib/rrule.rb +26 -4
- data/lib/rrule/context.rb +137 -0
- data/lib/rrule/filters/by_month.rb +16 -0
- data/lib/rrule/filters/by_month_day.rb +16 -0
- data/lib/rrule/filters/by_week_day.rb +24 -0
- data/lib/rrule/filters/by_week_number.rb +16 -0
- data/lib/rrule/filters/by_year_day.rb +18 -0
- data/lib/rrule/frequencies/daily.rb +13 -0
- data/lib/rrule/frequencies/frequency.rb +34 -0
- data/lib/rrule/frequencies/monthly.rb +14 -0
- data/lib/rrule/frequencies/weekly.rb +29 -0
- data/lib/rrule/frequencies/yearly.rb +14 -0
- data/lib/rrule/generators/all_occurrences.rb +26 -0
- data/lib/rrule/generators/by_set_position.rb +35 -0
- data/lib/rrule/rule.rb +181 -0
- data/lib/rrule/weekday.rb +17 -0
- data/rrule.gemspec +20 -0
- data/spec/context_spec.rb +261 -0
- data/spec/filters/by_month_day_spec.rb +35 -0
- data/spec/filters/by_month_spec.rb +35 -0
- data/spec/filters/by_week_day_spec.rb +35 -0
- data/spec/filters/by_week_number_spec.rb +41 -0
- data/spec/filters/by_year_day_spec.rb +35 -0
- data/spec/frequencies/daily_spec.rb +55 -0
- data/spec/frequencies/monthly_spec.rb +57 -0
- data/spec/frequencies/weekly_spec.rb +57 -0
- data/spec/frequencies/yearly_spec.rb +52 -0
- data/spec/generators/all_occurrences_spec.rb +44 -0
- data/spec/generators/by_set_position_spec.rb +39 -0
- data/spec/rule_spec.rb +1988 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/weekday_spec.rb +34 -0
- 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
|
data/rrule.gemspec
ADDED
@@ -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
|