texel-recurrence-rule-parser 0.0.4 → 0.0.5
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.
- 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
|