EdvardM-recurrence 0.1.14

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 (3) hide show
  1. data/README +70 -0
  2. data/spec/recurrence_spec.rb +482 -0
  3. metadata +53 -0
data/README ADDED
@@ -0,0 +1,70 @@
1
+ = Recurrence
2
+
3
+ == Overview
4
+
5
+ Recurrence provides a simple class for handling recurring, time-associated objects. The goal is to create a general-purpose,
6
+ loosely coupled class library to provide common functionality for events occurring at predetermined intervals.
7
+
8
+ Short example:
9
+
10
+ require 'recurrence'
11
+
12
+ first_of_june = [2008, 6, 1]
13
+ r = Recurrence.new(first_of_june, :every_other => :week, :until => '2010-06-01')
14
+
15
+ my_cal = MyCalendar.new_default
16
+ my_cal.each_day { |date|
17
+ time = Time.parse(date) # assuming date can be parsed with Time.parse
18
+ puts 'Yay! today is the day!' if r.recurs_on?(time)
19
+ }
20
+
21
+ == Set-like operations
22
+
23
+ Real power of Recurrence lies in it's support for set-like operations +join+, +intersect+, +diff+ and +complement+. For example,
24
+ given the start date of 2008-06-01, recur something every thursday and friday:
25
+
26
+ require 'recurrence'
27
+
28
+ start_date = '2008-06-01'
29
+ r1 = Recurrence.new(start_date, :every => :thursday)
30
+ r2 = Recurrence.new(start_date, :every => :friday)
31
+ r = r1.join(r2)
32
+
33
+ Another example, a tad contrived perhaps:
34
+
35
+ # Recur something every friday, except if it is last friday of the month:
36
+
37
+ dow = :friday
38
+ r1 = Recurrence.new(:epoch, :every => dow)
39
+ r2 = Recurrence.new(:epoch, :every_last => dow, :of => :month)
40
+
41
+ r = r1.diff(r2)
42
+
43
+ Nested set-like operations are also possible. So, for arbitrary recurrences a and b and any time t, the following should always apply:
44
+
45
+ r1 = (a.join(b)).complement
46
+ r2 = (a.complement).intersect(b.complement)
47
+
48
+ r1.recurs_on?(t) == r2.recurs_on?(t) # De Morgan's law - complement of a union is the same as intersection of the complements
49
+
50
+ See RecurrenceBase::SetOperations for more.
51
+
52
+ == Installation
53
+
54
+ Enter
55
+
56
+ rake gem
57
+
58
+ on the command line in the same directory as this README file, it should produce the gem under the pkg directory.
59
+ Then you should be able to say
60
+
61
+ sudo gem install pkg/recurrence*.gem
62
+
63
+ to install the gem to your local system.
64
+
65
+ KTHXBAI
66
+
67
+ == License
68
+
69
+ MIT (see MIT-LICENSE)
70
+
@@ -0,0 +1,482 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe Recurrence do
4
+ def recur_on(date)
5
+ # TODO: replace this with more heavy-weight custom matcher which results in more informative
6
+ # failure messages
7
+ simple_matcher("recurs on #{date}") { |obj| obj.recurs_on?(date) }
8
+ end
9
+
10
+ describe 'initialization' do
11
+ it "should accept a Time object" do
12
+ Recurrence.new(Date.new(2008, 8, 27), :every => :day)
13
+ end
14
+
15
+ it "should accept Date.new style argument list" do
16
+ Recurrence.new([2008, 8, 27], :every => :day).start_date.should == Date.new(2008, 8, 27)
17
+ end
18
+
19
+ it "should accept a time string" do
20
+ Recurrence.new('2008-08-27', :every => :day).start_date.should == Date.new(2008, 8, 27)
21
+ end
22
+
23
+ it "should accept the symbol epoch" do
24
+ Recurrence.new(:epoch, :every => :day).start_date.should == Date.new(1970, 1, 1)
25
+ end
26
+ end
27
+
28
+ it "should return initialization time" do
29
+ r = Recurrence.new([2008, 8, 27], :every => :day)
30
+ r.start_date.should == Date.new(2008, 8, 27)
31
+ end
32
+
33
+ it "should allow time string argument for recurs_on?" do
34
+ r = Recurrence.new('2008-08-27', :every => :day)
35
+ r.should recur_on('2008-08-30')
36
+ end
37
+
38
+ it "should return starting date of week" do
39
+ r = Recurrence.new([2008, 8, 27], :every => :day)
40
+ r.starting_dow.should == :wednesday
41
+ end
42
+
43
+ it "should return starting date of week in short form" do
44
+ r = Recurrence.new([2008, 8, 27], :every => :day)
45
+ r.starting_dow(:short).should == :wed
46
+ end
47
+
48
+ describe "recurring every <interval>" do
49
+ it "should not recur before initial date" do
50
+ Recurrence.new('2008-08-27', :every => :day).should_not recur_on('2008-08-26')
51
+ end
52
+
53
+ it "should not recur after final date" do
54
+ Recurrence.new('2008-08-27', :every => :day, :until => '2008-10-1').should_not recur_on('2008-10-2')
55
+ end
56
+
57
+ it "should recur daily" do
58
+ r = Recurrence.new([2008, 9, 2], :every => :day)
59
+
60
+ (2..30).each do |day|
61
+ t = Date.new(2008, 9, day)
62
+ r.should recur_on(t)
63
+ end
64
+ end
65
+
66
+ it "should recur ad infinitum if until is not specified (well, 2038-01-19 is the day Time instances go boink unless fixed)" do
67
+ Recurrence.new(:epoch, :every => :day).should recur_on('2038-01-18')
68
+ end
69
+
70
+ it "should recur weekly" do
71
+ r = Recurrence.new(Date.new(2008, 8, 1), :every => :week)
72
+
73
+ (2..7).each do |day|
74
+ r.should_not recur_on(Date.new(2008, 8, day))
75
+ end
76
+ r.should recur_on(Date.new(2008, 8, 8))
77
+ end
78
+
79
+ it "should recur every given weekday" do
80
+ year, mon = 2008, 9
81
+ r = Recurrence.new(Date.new(year, mon, 1), :every => :wednesday)
82
+
83
+ [1, 2, 4, 5, 6, 7, 8, 9, 11].each do |day|
84
+ r.should_not recur_on(Date.new(year, mon, day))
85
+ end
86
+ [3, 10].each do |day|
87
+ r.should recur_on(Date.new(year, mon, day))
88
+ end
89
+ end
90
+
91
+
92
+ it "should recur monthly" do
93
+ r = Recurrence.new(Date.new(2008, 8, 24), :every => :month)
94
+
95
+ (1..23).each do |day|
96
+ r.should_not recur_on(Date.new(2008, 9, day))
97
+ end
98
+ r.should recur_on(Date.new(2008, 9, 24))
99
+ end
100
+
101
+ it "should recur yearly" do
102
+ r = Recurrence.new(Date.new(2008, 8, 13), :every => :year)
103
+
104
+ (1..31).each do |day|
105
+ r.should_not recur_on(Date.new(2009, 1, day))
106
+ end
107
+ r.should recur_on(Date.new(2009, 8, 13))
108
+ end
109
+
110
+ it "should recur every weekend" do
111
+ r = Recurrence.new(:epoch, :every => :weekend)
112
+
113
+ (2..3).each do |day| # saturday, sunday
114
+ r.should recur_on(Date.new(2008, 8, day))
115
+ end
116
+ (4..8).each do |day|
117
+ r.should_not recur_on(Date.new(2008, 8, day))
118
+ end
119
+ end
120
+
121
+ it "should recur every workday" do
122
+ r = Recurrence.new(:epoch, :every => :workday)
123
+
124
+ (2..3).each do |day|
125
+ r.should_not recur_on(Date.new(2008, 8, day))
126
+ end
127
+ (4..8).each do |day|
128
+ r.should recur_on(Date.new(2008, 8, day))
129
+ end
130
+ end
131
+
132
+ it "should raise ArgumentError when given invalid repeat type" do
133
+ lambda { Recurrence.new(Date.new(2008, 8, 22), :foo => :workday) }.should raise_error(ArgumentError)
134
+ end
135
+
136
+ it "should raise ArgumentError when given invalid recurrence type" do
137
+ lambda { Recurrence.new(Date.new(2008, 8, 22), :every => :homersimpson) }.should raise_error(ArgumentError)
138
+ end
139
+
140
+ end
141
+
142
+ describe "recurring every_second <interval>" do
143
+ it "should recur every second day" do
144
+ year, month = 2008, 8
145
+ r = Recurrence.new(Date.new(year, month, 1), :every_second => :day)
146
+
147
+ [1, 3, 5, 7, 9, 23].each { |day| r.should recur_on(Date.new(year, month, day)) }
148
+ [2, 4, 8, 10, 12, 26].each { |day| r.should_not recur_on(Date.new(year, month, day)) }
149
+ end
150
+
151
+ it "should recur every other week" do
152
+ year, month = 2008, 8
153
+ r = Recurrence.new(Date.new(year, month, 2), :every_second => :week)
154
+
155
+ (3..15).each { |day| r.should_not recur_on(Date.new(year, month, day)) }
156
+ [16, 30].each { |day| r.should recur_on(Date.new(year, month, day)) }
157
+ end
158
+
159
+ it "should recur every other month" do
160
+ year, month, day = 2008, 1, 1
161
+ r = Recurrence.new(Date.new(year, month, 1), :every_second => :month)
162
+
163
+ [2, 4, 6].each { |m| r.should_not recur_on(Date.new(year, m, day)) }
164
+ [1, 3, 5].each { |m| r.should recur_on(Date.new(year, m, day)) }
165
+ end
166
+
167
+ it "should recur every other year" do
168
+ month, day = 4, 29
169
+ r = Recurrence.new(Date.new(2000, month, day), :every_second => :year)
170
+
171
+ [2005, 2007, 2009].each { |y| r.should_not recur_on(Date.new(y, month, day)) }
172
+ [2006, 2004, 2010].each { |y| r.should recur_on(Date.new(y, month, day)) }
173
+ end
174
+ end
175
+
176
+ describe "recurring every_third <interval>" do
177
+ it "should recur every third day" do
178
+ year, month = 2008, 8
179
+ r = Recurrence.new(Date.new(year, month, 8), :every_third => :day)
180
+
181
+ [8, 11, 14, 17, 20].each { |day| r.should recur_on(Date.new(year, month, day)) }
182
+ [9, 10, 12, 13, 15, 16, 18, 19].each { |day| r.should_not recur_on(Date.new(year, month, day)) }
183
+ end
184
+
185
+ it "should recur every third week" do
186
+ year, month = 2008, 8
187
+ r = Recurrence.new(Date.new(year, month, 1), :every_third => :week)
188
+
189
+ (2..21).each { |day| r.should_not recur_on(Date.new(year, month, day)) }
190
+ r.should recur_on(Date.new(year, month, 22))
191
+ end
192
+
193
+ it "should recur every third month" do
194
+ year, day = 2008, 1
195
+ r = Recurrence.new(Date.new(year, 1, day), :every_third => :month)
196
+
197
+ [1, 4, 7, 10].each { |m| r.should recur_on(Date.new(year, m, day)) }
198
+ [2, 3, 5, 6, 8, 9].each { |m| r.should_not recur_on(Date.new(year, m, day)) }
199
+ end
200
+
201
+ it "should recur every third year" do
202
+ month, day = 9, 21
203
+ r = Recurrence.new(Date.new(2001, month, day), :every_third => :year)
204
+
205
+ [2004, 2007, 2010].each { |y| r.should recur_on(Date.new(y, month, day)) }
206
+ [2002, 2005, 2006].each { |y| r.should_not recur_on(Date.new(y, month, day)) }
207
+ end
208
+ end
209
+
210
+ describe "recurring every 10th <interval>" do
211
+ it "should recur every 10th day" do
212
+ year, month = 2008, 1
213
+ r = Recurrence.new([year, month, 1], :every_nth => :day, :interval => 10)
214
+
215
+ [11, 21, 31].each do |d|
216
+ r.should recur_on([year, month, d])
217
+ end
218
+
219
+ (2..10).each { |d| r.should_not recur_on([year, month, d]) }
220
+ end
221
+
222
+ it "should recur every 10th month" do
223
+ year, day = 2008, 28
224
+ r = Recurrence.new([year, 1, day], :every_nth => :month, :interval => 10)
225
+
226
+ [1, 11].each { |m| r.should recur_on([year, m, day]) }
227
+
228
+ (2..10).each { |m| r.should_not recur_on([year, m, day]) }
229
+ end
230
+ end
231
+
232
+ describe "set operations" do
233
+ it "should be able to form union (OR) of two recurrences" do
234
+ # 1 2 3 4 5 6 7 8 9 10
235
+ # | | | | | # recur every other day since first
236
+
237
+ # 1 2 3 4 5 6 7 8 9 10
238
+ # | | | | # recur every third day since first
239
+
240
+ # 1 2 3 4 5 6 7 8 9 10
241
+ # | | | | | | | # union of recurrences above
242
+ start_date = '2008-08-01'
243
+ r = Recurrence.new(start_date, :every_second => :day).join(Recurrence.new(start_date, :every_third => :day))
244
+ [1, 3, 4, 5, 7, 9, 10].each { |d| r.should recur_on([2008, 8, d]) }
245
+ end
246
+
247
+ it "should be able to form intersection (AND) of two recurrences" do
248
+ # 1 2 3 4 5 6 7 8 9 10
249
+ # | | | | | # recur every other day since first
250
+
251
+ # 1 2 3 4 5 6 7 8 9 10
252
+ # | | | | # recur every third day since first
253
+
254
+ # 1 2 3 4 5 6 7 8 9 10
255
+ # | | # intersection of recurrences above
256
+ start_date = '2008-08-01'
257
+ r = Recurrence.new(start_date, :every_second => :day).intersect(Recurrence.new(start_date, :every_third => :day))
258
+ [1, 7].each { |d| r.should recur_on(Date.new(2008, 8, d)) }
259
+ end
260
+
261
+ it "should be able to form difference (\) of two recurrences" do
262
+ # 1 2 3 4 5 6 7 8 9 10
263
+ # | | | | | # recur every other day since first
264
+
265
+ # 1 2 3 4 5 6 7 8 9 10
266
+ # | | | | # recur every third day since first
267
+
268
+ # 1 2 3 4 5 6 7 8 9 10
269
+ # | | | # difference of recurrences above
270
+
271
+ start_date = '2008-08-01'
272
+ r = Recurrence.new(start_date, :every_second => :day).diff(Recurrence.new(start_date, :every_third => :day))
273
+ [3, 5, 9].each { |d| r.should recur_on([2008, 8, d]) }
274
+ [1, 2, 4, 6, 7, 8].each { |d| r.should_not recur_on([2008, 8, d]) }
275
+ end
276
+
277
+ it "should be able to form complement (NOT) of a recurrence" do
278
+ r = Recurrence.new('2008-08-01', :every_second => :day).complement
279
+ orig = r.complement
280
+
281
+ [2, 4, 6, 8, 10].each { |day|
282
+ date = Date.new(2008, 8, day)
283
+ r.should recur_on(date)
284
+ orig.should_not recur_on(date)
285
+ }
286
+
287
+ [1, 3, 5, 7, 9].each { |day|
288
+ date = Date.new(2008, 8, day)
289
+ r.should_not recur_on(date)
290
+ orig.should recur_on(date)
291
+ }
292
+ end
293
+
294
+ it "should be able to support nested set-like operations" do
295
+ # 1 2 3 4 5 6 7 8 9 10
296
+ # | | | | | # recur every other day since first
297
+
298
+ # 1 2 3 4 5 6 7 8 9 10
299
+ # | | | | # recur every third day since first
300
+
301
+ # 1 2 3 4 5 6 7 8 9 10
302
+ # ! | | # complement of (A union B) == (NOT A) AND (NOT B)
303
+
304
+ start_date = '2008-01-01'
305
+ a = Recurrence.new(start_date, :every_second => :day)
306
+ b = Recurrence.new(start_date, :every_third => :day)
307
+
308
+ # De Morgan's
309
+ complement_of_union = (a.join(b)).complement
310
+ intersection_of_complements = (a.complement).intersect(b.complement)
311
+
312
+
313
+ [2, 6, 8].each { |d|
314
+ date = [2008, 1, d]
315
+ complement_of_union.should recur_on(date)
316
+ intersection_of_complements.should recur_on(date)
317
+ }
318
+ [1, 3, 4, 5, 7, 9, 10].each { |d|
319
+ date = [2008, 1, d]
320
+ complement_of_union.should_not recur_on(date)
321
+ intersection_of_complements.should_not recur_on(date)
322
+ }
323
+ end
324
+ end
325
+
326
+ describe "recurring every nth of <weekday> of a <period>" do
327
+ it "should recur every first thursday of a month" do
328
+ r = Recurrence.new(:epoch, :every_first => :thursday, :of => :month)
329
+ (1..30).each { |day|
330
+ # 4th day is the first thursday on Sep 2008
331
+ date = [2008, 9, day]
332
+ if day == 4
333
+ r.should recur_on(date)
334
+ else
335
+ r.should_not recur_on(date)
336
+ end
337
+ }
338
+ end
339
+
340
+ it "should recur every second thursday of a month" do
341
+ r = Recurrence.new(:epoch, :every_second => :thursday, :of => :month)
342
+ (1..30).each { |day|
343
+ # 11th day is second thursday on Sep 2008
344
+ date = [2008, 9, day]
345
+ if day == 11
346
+ r.should recur_on(date)
347
+ else
348
+ r.should_not recur_on(date)
349
+ end
350
+ }
351
+ end
352
+
353
+ it "should recur every last thursday of a month" do
354
+ r = Recurrence.new(:epoch, :every_last => :thursday, :of => :month)
355
+ (1..30).each { |day|
356
+ # 25th day is the last thursday on Sep 2008
357
+ date = [2008, 9, day]
358
+ if day == 25
359
+ r.should recur_on(date)
360
+ else
361
+ r.should_not recur_on(date)
362
+ end
363
+ }
364
+ end
365
+ end
366
+
367
+ it "should yield every second day" do
368
+ r = Recurrence.new(:epoch, :every_second => :day)
369
+ days = []
370
+
371
+ r.each {|t|
372
+ break if t.day > 5
373
+ days << t.day
374
+ }
375
+ days.should == [1, 3, 5]
376
+ end
377
+
378
+ describe 'iteration' do
379
+ it "should yield every week" do
380
+ r = Recurrence.new(:epoch, :every => :week)
381
+ weekdays = []
382
+
383
+ r.each {|t|
384
+ weekdays << t.wday
385
+ break if weekdays.length > 3
386
+ }
387
+ weekdays.should == [r.start_date.wday] * 4
388
+ end
389
+
390
+ it "should yield every given weekday" do
391
+ r = Recurrence.new([2008, 9, 1], :every => :sunday)
392
+ count = 0
393
+
394
+ r.each {|t|
395
+ RecurrenceBase::RecurrenceMixin::DAYS[t.wday].should == :sunday
396
+ count += 1
397
+ break if count == 10
398
+ }
399
+ count.should == 10
400
+
401
+ r = Recurrence.new([2008, 9, 1], :every => :wednesday)
402
+ count = 0
403
+
404
+ r.each {|t|
405
+ RecurrenceBase::RecurrenceMixin::DAYS[t.wday].should == :wednesday
406
+ count += 1
407
+ break if count == 10
408
+ }
409
+ count.should == 10
410
+ end
411
+
412
+ it "should yield every month, setting the date to last in month if overlapping" do
413
+ r = Recurrence.new('2008-01-31', :every => :month)
414
+ days_months = []
415
+
416
+ r.each {|t|
417
+ break if days_months.length > 3
418
+ days_months << [t.day, t.mon]
419
+ }
420
+ days_months.should == [[31, 1], [29, 2], [31, 3], [30,4]]
421
+ end
422
+ end
423
+
424
+ describe 'examples' do
425
+ it "should recur every other day, starting from epoch" do
426
+ r = Recurrence.new(:epoch, :every_second => :day)
427
+ r.start_date.should == Date.new(1970, 1, 1)
428
+
429
+ [1, 3, 5].each { |day| r.should recur_on([1970, 1, day]) }
430
+ [2, 4, 6].each { |day| r.should_not recur_on([1970, 1, day]) }
431
+ end
432
+
433
+ it "should recur every 10th day starting from epoch" do
434
+ r = Recurrence.new(:epoch, :every_nth => :day, :interval => 10)
435
+
436
+ [1, 11, 21].each { |day| r.should recur_on([1970, 1, day]) }
437
+ [2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 20].each { |day| r.should_not recur_on([1970, 1, day]) }
438
+ end
439
+
440
+ it "should recur only on the first wednesday of a month starting from today" do
441
+ date = Date.new(2008, 9, 15)
442
+ r = Recurrence.new(date, :every_first => :wednesday, :of => :month)
443
+ date = r.start_date
444
+ hits = []
445
+ 40.times {
446
+ hits << date if r.recurs_on?(date)
447
+ date += 1
448
+ }
449
+ hits.map { |d| d.wday }.should == [3] # wednesday is at index 3
450
+ end
451
+
452
+ it "should recur only on every last thursday of a month, starting from today" do
453
+ r = Recurrence.new(:today, :every_last => :thursday, :of => :month)
454
+ hits = []
455
+ date = r.start_date
456
+ 40.times {
457
+ hits << date if r.recurs_on?(date)
458
+ date += 1
459
+ }
460
+ hits.map { |d| d.wday }.should == [4]
461
+ end
462
+
463
+ it "should recur once a week starting from 1st day" do
464
+ r = Recurrence.new([2008, 10, 7], :every => :week)
465
+ [7, 14, 21].each { |day| r.should recur_on([2008, 10, day]) }
466
+ end
467
+
468
+ it "should recur every wednesday starting from given date" do
469
+ r = Recurrence.new([2008, 10, 7], :every => :wednesday)
470
+ r.should recur_on([2008, 10, 8]) # 8th is wednesday
471
+ end
472
+
473
+ it "should recur every nth day of month" do
474
+ r = Recurrence.new("2008-09-04", :every => :month) # Recur on the 4th day of every month
475
+ [9, 10, 11].each { |mon|
476
+ r.should_not recur_on([2008, mon, 3])
477
+ r.should recur_on([2008, mon, 4])
478
+ r.should_not recur_on([2008, mon, 5])
479
+ }
480
+ end
481
+ end
482
+ end
metadata ADDED
@@ -0,0 +1,53 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: EdvardM-recurrence
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.14
5
+ platform: ruby
6
+ authors:
7
+ - Edvard Majakari
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-04-01 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description:
17
+ email: edvard.majakari@adalia.fi
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README
24
+ files:
25
+ - README
26
+ has_rdoc: "true"
27
+ homepage:
28
+ post_install_message:
29
+ rdoc_options: []
30
+
31
+ require_paths:
32
+ - lib
33
+ required_ruby_version: !ruby/object:Gem::Requirement
34
+ requirements:
35
+ - - ">="
36
+ - !ruby/object:Gem::Version
37
+ version: "0"
38
+ version:
39
+ required_rubygems_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: "0"
44
+ version:
45
+ requirements: []
46
+
47
+ rubyforge_project:
48
+ rubygems_version: 1.2.0
49
+ signing_key:
50
+ specification_version: 2
51
+ summary: Library for periodically recurring things
52
+ test_files:
53
+ - spec/recurrence_spec.rb