texel-recurrence-rule-parser 0.0.4 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/README.txt +24 -7
- data/lib/rrule_parser.rb +67 -9
- data/spec/lib/rrule_parser_spec.rb +215 -1
- metadata +1 -1
data/README.txt
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
= RruleParser
|
2
2
|
|
3
|
-
|
3
|
+
http://www.github.com/texel/recurrence-rule-parser
|
4
4
|
|
5
5
|
== DESCRIPTION:
|
6
6
|
|
@@ -10,18 +10,35 @@ and handles translating recurrence rules into temporal expressions.
|
|
10
10
|
== FEATURES/PROBLEMS:
|
11
11
|
|
12
12
|
Current list of supported expressions:
|
13
|
-
FREQ
|
14
|
-
|
15
|
-
|
13
|
+
FREQ, INTERVAL, BYDAY, BYMONTHDAY, UNTIL
|
14
|
+
|
15
|
+
Many values are still unimplemented.
|
16
16
|
|
17
17
|
== SYNOPSIS:
|
18
18
|
|
19
|
-
|
19
|
+
RruleParser should be able to consume any object acting like Icalendar::Event, containing
|
20
|
+
recurrence rules as specified by the iCalendar (RFC 2445) specification. For more information
|
21
|
+
on formatting of recurrence rules, visit http://www.kanzaki.com/docs/ical/rrule.html
|
22
|
+
|
23
|
+
== EXAMPLES:
|
24
|
+
|
25
|
+
event = Icalendar::Event.new
|
26
|
+
event.start = Time.parse('12/1/2008 3pm')
|
27
|
+
event.end = Time.parse('12/1/2008 6pm')
|
28
|
+
|
29
|
+
event.recurrence_rules = ["FREQ=WEEKLY;INTERVAL=1;COUNT=10"]
|
30
|
+
|
31
|
+
parser = RruleParser.new(event)
|
32
|
+
date_range = Date.parse('11/30/2008')..Date.parse('1/1/2009')
|
33
|
+
|
34
|
+
parser.dates(date_range)
|
35
|
+
|
36
|
+
#=> [Mon, 01 Dec 2008, Mon, 08 Dec 2008, Mon, 15 Dec 2008, Mon, 22 Dec 2008, Mon, 29 Dec 2008]
|
20
37
|
|
21
38
|
== REQUIREMENTS:
|
22
39
|
|
23
|
-
requires Runt
|
24
|
-
requires RSpec if you wish to run the specs.
|
40
|
+
requires Runt.
|
41
|
+
requires Icalendar and RSpec if you wish to run the specs.
|
25
42
|
|
26
43
|
== INSTALL:
|
27
44
|
|
data/lib/rrule_parser.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
+
require 'rubygems'
|
1
2
|
require 'runt'
|
2
|
-
require 'icalendar'
|
3
3
|
|
4
4
|
class RruleParser
|
5
5
|
VERSION = '1.0.0'
|
@@ -38,7 +38,7 @@ class RruleParser
|
|
38
38
|
def expressions
|
39
39
|
@expressions = []
|
40
40
|
@expressions << parse_frequency_and_interval
|
41
|
-
@expressions <<
|
41
|
+
@expressions << send(:"parse_#{self.rules[:freq].downcase}")
|
42
42
|
@expressions << parse_start
|
43
43
|
@expressions << parse_until
|
44
44
|
@expressions.compact!
|
@@ -51,15 +51,22 @@ class RruleParser
|
|
51
51
|
|
52
52
|
# Accepts a range of dates and outputs an array of dates matching the temporal expression.
|
53
53
|
def dates(range)
|
54
|
+
dates = []
|
55
|
+
|
54
56
|
if @count <= 0
|
55
|
-
self.expression.dates(range)
|
57
|
+
dates << self.expression.dates(range)
|
56
58
|
else
|
57
|
-
temp_range = self.event.start.to_date..(range.last)
|
59
|
+
temp_range = (self.event.start.send :to_date)..(range.last)
|
58
60
|
temp_dates = self.expression.dates(temp_range, @count)
|
59
|
-
temp_dates.select do |date|
|
61
|
+
dates << temp_dates.select do |date|
|
60
62
|
range.include?(date)
|
61
63
|
end
|
62
64
|
end
|
65
|
+
|
66
|
+
# TODO put original date back in if recurrence rule doesn't define it.
|
67
|
+
start_date = self.event.start.send(:to_date)
|
68
|
+
dates << start_date if range.include?(start_date)
|
69
|
+
dates.flatten.uniq
|
63
70
|
end
|
64
71
|
|
65
72
|
protected
|
@@ -83,7 +90,7 @@ class RruleParser
|
|
83
90
|
end
|
84
91
|
|
85
92
|
def parse_start
|
86
|
-
start_date = Date.civil(self.event.start.year, self.event.start.month, self.event.start.day - 1
|
93
|
+
start_date = Date.civil(self.event.start.year, self.event.start.month, self.event.start.day) - 1
|
87
94
|
Runt::AfterTE.new(start_date)
|
88
95
|
end
|
89
96
|
|
@@ -95,13 +102,53 @@ class RruleParser
|
|
95
102
|
end
|
96
103
|
end
|
97
104
|
|
98
|
-
|
99
|
-
|
105
|
+
def parse_daily
|
106
|
+
return
|
107
|
+
end
|
108
|
+
|
109
|
+
def parse_weekly
|
110
|
+
if self.rules[:byday]
|
111
|
+
self.rules[:byday].map { |day| parse_byday(day) }.inject do |m, expr|
|
112
|
+
m | expr
|
113
|
+
end
|
114
|
+
else
|
115
|
+
# Make the event recur on the day of the original event.
|
116
|
+
Runt::DIWeek.new(self.event.start.wday)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def parse_monthly
|
100
121
|
if self.rules[:byday]
|
101
|
-
self.rules[:byday].map
|
122
|
+
self.rules[:byday].map do |day_string|
|
123
|
+
parse_byday(day_string)
|
124
|
+
end.inject {|m, expr| m | expr}
|
125
|
+
elsif self.rules[:bymonthday]
|
126
|
+
self.rules[:bymonthday].map { |day| Runt::REMonth.new(day.to_i) }.inject do |m, expr|
|
127
|
+
m | expr
|
128
|
+
end
|
129
|
+
else
|
130
|
+
Runt::REMonth.new(self.event.start.day, self.event.start.day)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def parse_yearly
|
135
|
+
expressions = []
|
136
|
+
|
137
|
+
if self.rules[:bymonth]
|
138
|
+
expressions << self.rules[:bymonth].map { |month| Runt::REYear.new(month.to_i) }.inject do |m, expr|
|
102
139
|
m | expr
|
103
140
|
end
|
141
|
+
else
|
142
|
+
expressions << Runt::REYear.new(self.event.start.month)
|
143
|
+
end
|
144
|
+
|
145
|
+
if self.rules[:byday]
|
146
|
+
expressions << self.rules[:byday].map { |day_string| parse_byday(day_string) }.inject {|m, v| m | v}
|
147
|
+
else
|
148
|
+
expressions << Runt::REMonth.new(self.event.start.day)
|
104
149
|
end
|
150
|
+
|
151
|
+
expressions.inject {|m, expr| m & expr}
|
105
152
|
end
|
106
153
|
|
107
154
|
def parse_until
|
@@ -113,4 +160,15 @@ class RruleParser
|
|
113
160
|
def parse_count
|
114
161
|
@count = self.rules[:count].to_i if self.rules[:count]
|
115
162
|
end
|
163
|
+
|
164
|
+
def parse_byday(day_string)
|
165
|
+
# BYDAY rules can be in one of two formats: 2TU (2nd Tuesday), or TU (every Tuesday)
|
166
|
+
if day_string =~ /\d/
|
167
|
+
day_index = day_string.to_i
|
168
|
+
day = DAYS[day_string.gsub(day_index.to_s, '')] # Why is abbreviation such a long word?
|
169
|
+
Runt::DIMonth.new(day_index, day)
|
170
|
+
else
|
171
|
+
Runt::DIWeek.new(RruleParser::DAYS[day_string])
|
172
|
+
end
|
173
|
+
end
|
116
174
|
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
require 'lib/rrule_parser'
|
2
2
|
require 'icalendar'
|
3
|
+
require 'spec'
|
4
|
+
require 'redgreen'
|
3
5
|
|
4
6
|
module RruleParserSpecHelper
|
5
7
|
def create_default_event
|
@@ -12,6 +14,12 @@ module RruleParserSpecHelper
|
|
12
14
|
@event.recurrence_rules = ["FREQ=#{@frequency};INTERVAL=#{@interval};BYDAY=#{@byday};WKST=SU"]
|
13
15
|
end
|
14
16
|
|
17
|
+
def create_event
|
18
|
+
@event = Icalendar::Event.new
|
19
|
+
end
|
20
|
+
|
21
|
+
# TODO Make other test objects for shared specs :)
|
22
|
+
|
15
23
|
def create_parser(event)
|
16
24
|
@parser = RruleParser.new(event)
|
17
25
|
end
|
@@ -76,7 +84,7 @@ describe RruleParser do
|
|
76
84
|
@result = @parser.send(:parse_frequency_and_interval)
|
77
85
|
end
|
78
86
|
|
79
|
-
it "
|
87
|
+
it "returns a valid temporal expression" do
|
80
88
|
@result.should be_an_instance_of(Runt::EveryTE)
|
81
89
|
end
|
82
90
|
|
@@ -88,4 +96,210 @@ describe RruleParser do
|
|
88
96
|
@result.instance_variable_get("@precision").should == Runt::DPrecision.const_get(RruleParser::ADVERB_MAP[@frequency])
|
89
97
|
end
|
90
98
|
end
|
99
|
+
|
100
|
+
describe "#dates" do
|
101
|
+
context "with an event starting on Monday, 12/1/2008" do
|
102
|
+
before(:each) do
|
103
|
+
create_event
|
104
|
+
@event.start = Time.parse('12/1/2008 3pm')
|
105
|
+
@event.end = Time.parse('12/1/2008 5pm')
|
106
|
+
end
|
107
|
+
|
108
|
+
context "with a one-month range" do
|
109
|
+
before(:each) do
|
110
|
+
@range = (Date.civil(2008, 12, 1)..(Date.civil(2009, 1, 1)))
|
111
|
+
end
|
112
|
+
|
113
|
+
context "recurring every week" do
|
114
|
+
before(:each) do
|
115
|
+
@event.recurrence_rules = ['FREQ=WEEKLY;INTERVAL=1']
|
116
|
+
@range = (Date.civil(2008, 12, 1)..(Date.civil(2009, 1, 1)))
|
117
|
+
create_parser(@event)
|
118
|
+
end
|
119
|
+
|
120
|
+
it "should return 5 dates" do
|
121
|
+
@parser.dates(@range).size.should == 5
|
122
|
+
end
|
123
|
+
|
124
|
+
it "should return only Monday dates" do
|
125
|
+
@parser.dates(@range).map {|d| d.wday == Runt::Monday }.all?.should be_true
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
context "recurring every other week" do
|
130
|
+
before(:each) do
|
131
|
+
@event.recurrence_rules = ['FREQ=WEEKLY;INTERVAL=2']
|
132
|
+
@range = (Date.civil(2008, 12, 1)..(Date.civil(2008, 12, 31)))
|
133
|
+
create_parser(@event)
|
134
|
+
end
|
135
|
+
|
136
|
+
it "should return 3 dates" do
|
137
|
+
@parser.dates(@range).size.should == 3
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
context "recurring every Monday, Wednesday, and Friday" do
|
142
|
+
before(:each) do
|
143
|
+
@event.recurrence_rules = ['FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,WE,FR']
|
144
|
+
create_parser(@event)
|
145
|
+
end
|
146
|
+
|
147
|
+
it "should return 14 dates" do
|
148
|
+
@parser.dates(@range).size.should == 14
|
149
|
+
end
|
150
|
+
|
151
|
+
it "should return only dates on Monday, Wednesday, and Friday" do
|
152
|
+
@parser.dates(@range).map {|d| [Runt::Mon, Runt::Wed, Runt::Fri].include?(d.wday) }.all?.should be_true
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
context "recurring every day" do
|
157
|
+
before(:each) do
|
158
|
+
@event.recurrence_rules = ['FREQ=DAILY;INTERVAL=1']
|
159
|
+
create_parser(@event)
|
160
|
+
end
|
161
|
+
|
162
|
+
it "should return 31 dates" do
|
163
|
+
@parser.dates(@range).size.should == 32
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
context "recurring every 2 days" do
|
168
|
+
before(:each) do
|
169
|
+
@event.recurrence_rules = ['FREQ=DAILY;INTERVAL=2']
|
170
|
+
create_parser(@event)
|
171
|
+
end
|
172
|
+
|
173
|
+
it "should return 31 dates" do
|
174
|
+
@parser.dates(@range).size.should == 16
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
context "with a one-year range" do
|
180
|
+
before(:each) do
|
181
|
+
@range = (Date.civil(2008, 12, 1)..(Date.civil(2009, 11, 30)))
|
182
|
+
end
|
183
|
+
|
184
|
+
context "recurring every month" do
|
185
|
+
before(:each) do
|
186
|
+
@event.recurrence_rules = ['FREQ=MONTHLY;INTERVAL=1']
|
187
|
+
create_parser(@event)
|
188
|
+
end
|
189
|
+
|
190
|
+
it "should return 12 dates" do
|
191
|
+
@parser.dates(@range).size.should == 12
|
192
|
+
end
|
193
|
+
|
194
|
+
it "should return only dates on the same day of the month" do
|
195
|
+
@parser.dates(@range).map {|d| d.day == @event.start.day}.all?.should be_true
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
context "recurring every two months" do
|
200
|
+
before(:each) do
|
201
|
+
@event.recurrence_rules = ['FREQ=MONTHLY;INTERVAL=2']
|
202
|
+
create_parser(@event)
|
203
|
+
end
|
204
|
+
|
205
|
+
it "should return 6 dates" do
|
206
|
+
@parser.dates(@range).size.should == 6
|
207
|
+
end
|
208
|
+
|
209
|
+
it "should return only dates on the same day of the month" do
|
210
|
+
@parser.dates(@range).map {|d| d.day == @event.start.day}.all?.should be_true
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
context "recurring the first and fifteenth of every month" do
|
215
|
+
before(:each) do
|
216
|
+
@event.recurrence_rules = ['FREQ=MONTHLY;INTERVAL=1;BYMONTHDAY=1,15']
|
217
|
+
create_parser(@event)
|
218
|
+
end
|
219
|
+
|
220
|
+
it "should return 24 dates" do
|
221
|
+
@parser.dates(@range).size.should == 24
|
222
|
+
end
|
223
|
+
|
224
|
+
it "should return dates only on the 1st and 15th of the month" do
|
225
|
+
@parser.dates(@range).map {|d| [1, 15].include?(d.day) }.all?.should be_true
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
context "recurring the first Monday of every month" do
|
230
|
+
before(:each) do
|
231
|
+
@event.recurrence_rules = ['FREQ=MONTHLY;INTERVAL=1;BYDAY=1MO']
|
232
|
+
create_parser(@event)
|
233
|
+
end
|
234
|
+
|
235
|
+
it "should return 12 dates" do
|
236
|
+
@parser.dates(@range).size.should == 12
|
237
|
+
end
|
238
|
+
|
239
|
+
it "should return only Mondays" do
|
240
|
+
# Garfield hates this recurrence rule.
|
241
|
+
@parser.dates(@range).map {|d| d.wday == Runt::Monday}.all?.should be_true
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
context "with a five-year range" do
|
247
|
+
before(:each) do
|
248
|
+
@range = (Date.civil(2008, 12, 1)..(Date.civil(2013, 11, 30)))
|
249
|
+
end
|
250
|
+
|
251
|
+
context "recurring every year" do
|
252
|
+
before do
|
253
|
+
@event.recurrence_rules = ['FREQ=YEARLY;INTERVAL=1']
|
254
|
+
create_parser(@event)
|
255
|
+
@dates = @parser.dates(@range)
|
256
|
+
end
|
257
|
+
|
258
|
+
it "should return 5 dates" do
|
259
|
+
@dates.size.should == 5
|
260
|
+
end
|
261
|
+
|
262
|
+
it "should return the correct date each year" do
|
263
|
+
@dates.map do |d|
|
264
|
+
[:month, :day].map do |method|
|
265
|
+
d.send(method) == @event.start.send(method)
|
266
|
+
end.all?
|
267
|
+
end.all?.should be_true
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
context "recurring every other year" do
|
272
|
+
before do
|
273
|
+
@event.recurrence_rules = ['FREQ=YEARLY;INTERVAL=2']
|
274
|
+
create_parser(@event)
|
275
|
+
@dates = @parser.dates(@range)
|
276
|
+
end
|
277
|
+
|
278
|
+
it "should return 3 dates" do
|
279
|
+
@dates.size.should == 3
|
280
|
+
end
|
281
|
+
|
282
|
+
it "should return the correct date each year" do
|
283
|
+
@dates.map do |d|
|
284
|
+
[:month, :day].map do |method|
|
285
|
+
d.send(method) == @event.start.send(method)
|
286
|
+
end.all?
|
287
|
+
end.all?.should be_true
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
context "recurring every December and June" do
|
292
|
+
before do
|
293
|
+
@event.recurrence_rules = ['FREQ=YEARLY;INTERVAL=1;BYMONTH=6,12']
|
294
|
+
create_parser(@event)
|
295
|
+
@dates = @parser.dates(@range)
|
296
|
+
end
|
297
|
+
|
298
|
+
it "should return 10 dates" do
|
299
|
+
@dates.size.should == 10
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
91
305
|
end
|