recurs 0.0.1
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/.gitignore +4 -0
- data/Gemfile +9 -0
- data/README +17 -0
- data/Rakefile +2 -0
- data/lib/recurs.rb +245 -0
- data/lib/recurs/consts.rb +40 -0
- data/lib/recurs/rules.rb +92 -0
- data/lib/recurs/version.rb +3 -0
- data/lib/util/module.rb +67 -0
- data/recurs.gemspec +22 -0
- data/spec/models/event_spec.rb +66 -0
- data/spec/models/recurrence_spec.rb +128 -0
- data/spec/models/rules_spec.rb +40 -0
- data/spec/spec_helper.rb +4 -0
- metadata +95 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# Recurs
|
2
|
+
Is a small Recurring Event parsing library. It will take symbolized input and output icalendar standard recurring events,
|
3
|
+
see the specs for examples of the api
|
4
|
+
|
5
|
+
This gem has been tested as a standalone in ruby 1.9, or using Rails 3
|
6
|
+
|
7
|
+
It also allows the developer to specifiy specialized Recurrence schemes by using the power of Ruby closures
|
8
|
+
|
9
|
+
## Rails (ActiveRecord)
|
10
|
+
|
11
|
+
class Event < AR
|
12
|
+
acts_as_recurring
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
## TODO
|
17
|
+
Finish the instance functionality
|
data/Rakefile
ADDED
data/lib/recurs.rb
ADDED
@@ -0,0 +1,245 @@
|
|
1
|
+
require 'util/module' unless defined? Rails
|
2
|
+
require 'recurs/consts'
|
3
|
+
require 'recurs/rules'
|
4
|
+
require 'ri_cal'
|
5
|
+
module Recurs
|
6
|
+
include RiCal
|
7
|
+
# Your code goes here...
|
8
|
+
|
9
|
+
module Parser
|
10
|
+
|
11
|
+
def self.included(base)
|
12
|
+
base.send :extend, ClassMethods
|
13
|
+
end
|
14
|
+
|
15
|
+
# class << self is NOT the same as including ( seen above ) which makes methods available to CLASSES that include the module
|
16
|
+
#, it ONLY makes the methods available to the MODULE itself
|
17
|
+
class << self
|
18
|
+
def rrule(repeats=nil, args={})
|
19
|
+
args[:rule] = :r
|
20
|
+
rule(repeats, args)
|
21
|
+
end
|
22
|
+
|
23
|
+
def exrule(repeats=nil, args={})
|
24
|
+
args[:rule] = :e
|
25
|
+
rule(repeats, args)
|
26
|
+
end
|
27
|
+
|
28
|
+
def rdate(dates=nil, args={})
|
29
|
+
args[:rule] = :r
|
30
|
+
date(dates, args)
|
31
|
+
end
|
32
|
+
|
33
|
+
def exdate(dates=nil, args={})
|
34
|
+
args[:rule] = :e
|
35
|
+
date(dates, args)
|
36
|
+
end
|
37
|
+
|
38
|
+
def date(dates=nil, args={})
|
39
|
+
args[:rule] == :r ? r = "RDATE" : r = "EXDATE"
|
40
|
+
if dates.is_a? Hash
|
41
|
+
f_dates = dates.flatten
|
42
|
+
if (f_dates[0] == :period) || (f_dates[0] == :range)
|
43
|
+
r += ";VALUE=PERIOD:"
|
44
|
+
i = 0
|
45
|
+
l = f_dates[1].length
|
46
|
+
e = 1
|
47
|
+
f_dates[1].each { |d|
|
48
|
+
r += RiCal::FastDateTime.from_date_time(d.to_datetime).ical_str
|
49
|
+
r += '/' if i == 0
|
50
|
+
r += ',' if (e < l) && (i != 0)
|
51
|
+
i += 1
|
52
|
+
e += 1
|
53
|
+
}
|
54
|
+
elsif f_dates[0] == :dates
|
55
|
+
r += ":"
|
56
|
+
l = f_dates[1].length
|
57
|
+
e = 1
|
58
|
+
f_dates[1].each {|d|
|
59
|
+
r += "#{RiCal::FastDateTime.from_date_time(d.to_datetime).ical_str}"
|
60
|
+
r += "," if (e < l)
|
61
|
+
e += 1
|
62
|
+
}
|
63
|
+
end
|
64
|
+
elsif dates.is_a?(Date) || dates.is_a?(DateTime)
|
65
|
+
r += ":#{RiCal::FastDateTime.from_date_time(dates.to_datetime).ical_str}"
|
66
|
+
end
|
67
|
+
r
|
68
|
+
end
|
69
|
+
|
70
|
+
protected
|
71
|
+
@@rule = nil
|
72
|
+
|
73
|
+
# The BYSECOND attr could be eg: 14 or multiple: 14, 45 between 0 and 59 ( i assume )
|
74
|
+
# The BYMINUTE attr could be eg: 14 or multiple: 14, 45 between 0 and 59 ( i assume )
|
75
|
+
# The BYSECOND attr could be eg: 14 or multiple: 14, 22 between 0 and 23 ( i assume )
|
76
|
+
# The BYDAY attribute allows you to specify exactly which days (SA, SU, MO, TU, WE, TH, FR)
|
77
|
+
# The BYMONTH is any month value between 1 .. 12
|
78
|
+
# the BYMONTHDAY is any value between 1 and 31
|
79
|
+
# WKST is the week starting on BYDAY eg SU,MO,TU,WE,TH
|
80
|
+
|
81
|
+
#args.each {|a| args[a[0]] = a[1].to_s.upcase if (a[1].is_a? String) || (a[1].is_a? Symbol)}
|
82
|
+
|
83
|
+
|
84
|
+
def rule(repeats, args = {})
|
85
|
+
args[:rule] == :r ? @@rule = "RRULE" : @@rule = "EXRULE"
|
86
|
+
@@rule += ":FREQ=#{repeats.to_s.upcase}"
|
87
|
+
interval(args)
|
88
|
+
args.each { |ar|
|
89
|
+
unless [:rule, :by_set_pos, :by_week_at, :count, :occurrences, :until, :ends_at, :repeats_every, :interval].include? ar[0]
|
90
|
+
@@rule += ";#{ar[0].to_s.gsub('_', '').upcase}=#{by_unit(ar[0], ar[1])}"
|
91
|
+
end
|
92
|
+
}
|
93
|
+
@@rule += ";BYSETPOS=#{args[:by_set_pos]}" if args[:by_set_pos]
|
94
|
+
@@rule += ";WKST=#{args[:by_week_st]}" if args[:by_week_st]
|
95
|
+
ending(args)
|
96
|
+
@@rule
|
97
|
+
end
|
98
|
+
|
99
|
+
def by_unit(measure, units)
|
100
|
+
ms = {:by_second => [60, get_num, BY_N_SECONDS],
|
101
|
+
:by_minute => [60, get_num, BY_N_MINUTES],
|
102
|
+
:by_hour => [23, get_num, BY_N_HOURS],
|
103
|
+
:by_day => [7, get_day_num, BY_DAYS],
|
104
|
+
:by_month_day => [31, get_day_num, BY_N_MONTH_DAYS],
|
105
|
+
:by_year_day => [366, get_day_num, BY_N_YEAR_DAYS],
|
106
|
+
:by_week => [52, get_num, BY_N_WEEKS],
|
107
|
+
:by_month => [12, get_month_num, BY_N_MONTHS]}
|
108
|
+
|
109
|
+
@measure = ms[measure]
|
110
|
+
|
111
|
+
r = ""
|
112
|
+
d = get_unit_nums(units)
|
113
|
+
comp = d.uniq.compact
|
114
|
+
c = comp.count
|
115
|
+
z = 0
|
116
|
+
comp.each { |i|
|
117
|
+
z += 1
|
118
|
+
r += @measure[2][i].to_s
|
119
|
+
r += "," unless z == c
|
120
|
+
}
|
121
|
+
r
|
122
|
+
end
|
123
|
+
|
124
|
+
def get_unit_nums(units)
|
125
|
+
r = []
|
126
|
+
# consts DAYS, BY_DAY
|
127
|
+
if units.is_a? Array
|
128
|
+
units.each { |d|
|
129
|
+
r << @measure[1].call(d)
|
130
|
+
}
|
131
|
+
else
|
132
|
+
r << @measure[1].call(units)
|
133
|
+
end
|
134
|
+
r
|
135
|
+
end
|
136
|
+
|
137
|
+
def get_num
|
138
|
+
->(num){
|
139
|
+
num.to_i.modulo(@measure[0]) unless num.is_a? Symbol;
|
140
|
+
}
|
141
|
+
end
|
142
|
+
|
143
|
+
def get_day_num
|
144
|
+
->(day){
|
145
|
+
if DAYS.include?(day.to_s.capitalize)
|
146
|
+
DAYS.find_index(day.to_s.capitalize)
|
147
|
+
elsif BY_DAYS.include?(day.to_s.upcase)
|
148
|
+
BY_DAYS.find_index(day.to_s.upcase)
|
149
|
+
else
|
150
|
+
day.to_i.modulo(7) unless day.is_a? Symbol
|
151
|
+
end;
|
152
|
+
}
|
153
|
+
end
|
154
|
+
|
155
|
+
def get_month_num
|
156
|
+
->(mon){
|
157
|
+
if MONTHS.include?(mon.to_s.capitalize)
|
158
|
+
MONTHS.find_index(mon.to_s.capitalize)
|
159
|
+
elsif BY_N_MONTHS.include?(mon.to_s.upcase)
|
160
|
+
BY_N_MONTHS.find_index(mon.to_s.upcase)
|
161
|
+
else
|
162
|
+
mon.to_i.modulo(12) unless mon.is_a? Symbol
|
163
|
+
end;
|
164
|
+
}
|
165
|
+
end
|
166
|
+
|
167
|
+
def interval(args)
|
168
|
+
if args[:repeats_every]
|
169
|
+
@@rule += ";INTERVAL=#{args[:repeats_every]}"
|
170
|
+
elsif args[:interval]
|
171
|
+
@@rule += ";INTERVAL=#{args[:interval]}"
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def ending(args)
|
176
|
+
if args[:count]
|
177
|
+
@@rule += ";COUNT=#{args[:count]}"
|
178
|
+
elsif args[:occurrences]
|
179
|
+
@@rule += ";COUNT=#{args[:occurrences]}"
|
180
|
+
elsif args[:until]
|
181
|
+
@@rule += ";UNTIL=#{RiCal::FastDateTime.from_date_time(args[:until].to_datetime).ical_str}"
|
182
|
+
elsif args[:ends_at]
|
183
|
+
@@rule += ";UNTIL=#{RiCal::FastDateTime.from_date_time(args[:ends_at].to_datetime).ical_str}"
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
end
|
188
|
+
|
189
|
+
module ClassMethods
|
190
|
+
def acts_as_recurring
|
191
|
+
send :include, InstanceMethods
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
module InstanceMethods
|
196
|
+
def initialize
|
197
|
+
@rrules ||= []
|
198
|
+
@exrules ||= []
|
199
|
+
@rdates ||= []
|
200
|
+
@exdates ||= []
|
201
|
+
super
|
202
|
+
end
|
203
|
+
|
204
|
+
def recurs
|
205
|
+
r = @rrules
|
206
|
+
r.concat @exrules
|
207
|
+
r.concat @rdates
|
208
|
+
r.concat @exdates
|
209
|
+
r.join
|
210
|
+
end
|
211
|
+
|
212
|
+
def add_rrule(repeats, args = {})
|
213
|
+
rrule = Parser.rrule(repeats, args)
|
214
|
+
@rrules << rrule
|
215
|
+
rrule
|
216
|
+
end
|
217
|
+
|
218
|
+
def add_exrule(repeats, args = {})
|
219
|
+
exrule = Parser.exrule(repeats, args)
|
220
|
+
@exrules << exrule
|
221
|
+
exrule
|
222
|
+
end
|
223
|
+
|
224
|
+
def add_rdate(args = {})
|
225
|
+
rdate = Parser.rdate(args)
|
226
|
+
@rdates << rdate
|
227
|
+
rdate
|
228
|
+
end
|
229
|
+
|
230
|
+
def add_exdate(args = {})
|
231
|
+
exdate = Parser.exdate(args)
|
232
|
+
@exdates << exdate
|
233
|
+
exdate
|
234
|
+
end
|
235
|
+
|
236
|
+
protected
|
237
|
+
attr_accessor :rrules, :exrules, :rdates, :exdates
|
238
|
+
end
|
239
|
+
|
240
|
+
|
241
|
+
end
|
242
|
+
|
243
|
+
|
244
|
+
end
|
245
|
+
ActiveRecord::Base.send(:include, Recurs::Parser) if defined? Rails
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Recurs
|
2
|
+
#module Consts
|
3
|
+
# 0 1 2 3 4 5 6
|
4
|
+
DAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
|
5
|
+
MONTHS = ['January', "February", 'March', 'April', 'May', 'June',
|
6
|
+
'July', 'August', 'September', 'October', 'November', 'December']
|
7
|
+
BY_N_SECONDS = (0..59).to_a
|
8
|
+
BY_N_MINUTES = (0..59).to_a
|
9
|
+
BY_N_HOURS = (0..23).to_a
|
10
|
+
BY_N_DAYS = (0..6).to_a
|
11
|
+
BY_N_MONTH_DAYS = (1..31).to_a
|
12
|
+
BY_N_YEAR_DAYS = (1..366).to_a
|
13
|
+
BY_DAYS = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA']
|
14
|
+
BY_N_WEEKS = (1..54).to_a
|
15
|
+
BY_N_MONTHS = (1..12).to_a
|
16
|
+
BY_MONTHS = ['jan', 'feb', 'mar', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']
|
17
|
+
|
18
|
+
#SYM_DAYS = [:sunday, :monday, :tuesday, :wednesday, :thursday, :friday, :saturday]
|
19
|
+
SYM_INTEGERS = [:zero, :one, :two, :three, :four, :five, :six, :seven, :eight, :nine, :ten]
|
20
|
+
SYM_TEENS = [:eleven, :twelve, :thirteen, :fourteen, :fifteen, :sixteen, :seventeen, :eighteen, :nineteen]
|
21
|
+
SYM_INTEGER_GROUP = SYM_INTEGERS+SYM_TEENS
|
22
|
+
|
23
|
+
# SYM_TENS[(34 - 34.modulo(10))/10]
|
24
|
+
SYM_TENS = [:ten, :twenty, :thirty, :fourty, :fifty, :sixty, :seventy, :eighty, :ninety, :hundred]
|
25
|
+
|
26
|
+
def get_integer_from_sym(sym)
|
27
|
+
SYM_INTEGER_GROUP(sym)
|
28
|
+
end
|
29
|
+
|
30
|
+
def build_num_sym(int)
|
31
|
+
if int > 19
|
32
|
+
r = int.modulo(10)
|
33
|
+
t = int - r
|
34
|
+
[SYM_TENS(t), SYM_INTEGERS[r]]
|
35
|
+
else
|
36
|
+
SYM_INTEGER_GROUP[int]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
#end
|
40
|
+
end
|
data/lib/recurs/rules.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
module Recurs
|
2
|
+
module Rules
|
3
|
+
mattr_accessor :repeat_procs, :schema
|
4
|
+
@@repeat_procs = {
|
5
|
+
|
6
|
+
=begin
|
7
|
+
'Daily' => ->(args = {}){
|
8
|
+
args[:repeats] = 'DAILY'
|
9
|
+
rrule(args);
|
10
|
+
}, # 0
|
11
|
+
=end
|
12
|
+
|
13
|
+
#=begin
|
14
|
+
'Daily' => ->(set=false, args = {}){
|
15
|
+
unless set
|
16
|
+
@recurrence_template = ['standard','Days']
|
17
|
+
else
|
18
|
+
Parser.rrule(:daily, args)
|
19
|
+
end;
|
20
|
+
}, # 0
|
21
|
+
#=end
|
22
|
+
|
23
|
+
'Every Weekday ( Mon - Fri )' => ->(set=false, args = {}){
|
24
|
+
unless set
|
25
|
+
@recurrence_template = 'set_points'
|
26
|
+
else
|
27
|
+
args[:by_day] = [1,2,3,4,5]
|
28
|
+
Parser.rrule(:weekly, args)
|
29
|
+
#
|
30
|
+
# .
|
31
|
+
end;
|
32
|
+
}, # 1
|
33
|
+
|
34
|
+
'Every Mon, Wed, Fri' => ->(set=false, args = {}){
|
35
|
+
unless set
|
36
|
+
@recurrence_template = 'set_points'
|
37
|
+
else
|
38
|
+
args[:by_day] = [1,3,5]
|
39
|
+
Parser.rrule(:weekly, args)
|
40
|
+
end;
|
41
|
+
}, # 2
|
42
|
+
|
43
|
+
'Every Tues, Thurs' => ->(set=false, args = {}){
|
44
|
+
unless set
|
45
|
+
@recurrence_template = 'set_points'
|
46
|
+
else
|
47
|
+
args[:by_day] = [2,4]
|
48
|
+
Parser.rrule(:weekly, args)
|
49
|
+
end;
|
50
|
+
}, # 3
|
51
|
+
|
52
|
+
'Every Weekend' => ->(set=false, args = {}){
|
53
|
+
unless set
|
54
|
+
@recurrence_template = 'set_points'
|
55
|
+
else
|
56
|
+
args[:by_day] = [0,6]
|
57
|
+
Parser.rrule(:weekly, args)
|
58
|
+
end;
|
59
|
+
}, # 4
|
60
|
+
|
61
|
+
'Weekly' => ->(set=false, args = {}){
|
62
|
+
unless set
|
63
|
+
@recurrence_template = ['weekly', 'Weeks']
|
64
|
+
else
|
65
|
+
Parser.rrule(:weekly, args)
|
66
|
+
end;
|
67
|
+
}, # 5
|
68
|
+
|
69
|
+
'Monthly' => ->(set=false, args = {}){
|
70
|
+
unless set
|
71
|
+
@recurrence_template = ['monthly', 'Months']
|
72
|
+
else
|
73
|
+
Parser.rrule(:monthly, args)
|
74
|
+
end;
|
75
|
+
}, # 6
|
76
|
+
|
77
|
+
'Yearly' => ->(set=false, args = {}){
|
78
|
+
unless set
|
79
|
+
@recurrence_template = ['standard', 'Years']
|
80
|
+
else
|
81
|
+
Parser.rrule(:yearly, args)
|
82
|
+
end;
|
83
|
+
} # 7
|
84
|
+
|
85
|
+
}
|
86
|
+
class << self
|
87
|
+
def schemes
|
88
|
+
@@schemas = @@repeat_procs.keys if @@repeat_procs.is_a? Hash
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
data/lib/util/module.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
# these utility methods are taken from Ruby On Rails ActiveSupport module
|
2
|
+
|
3
|
+
class Array
|
4
|
+
# Extracts options from a set of arguments. Removes and returns the last
|
5
|
+
# element in the array if it's a hash, otherwise returns a blank hash.
|
6
|
+
#
|
7
|
+
# def options(*args)
|
8
|
+
# args.extract_options!
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# options(1, 2) # => {}
|
12
|
+
# options(1, 2, :a => :b) # => {:a=>:b}
|
13
|
+
def extract_options!
|
14
|
+
if last.is_a?(Hash) && last.extractable_options?
|
15
|
+
pop
|
16
|
+
else
|
17
|
+
{}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class Module
|
23
|
+
def mattr_reader(*syms)
|
24
|
+
syms.each do |sym|
|
25
|
+
next if sym.is_a?(Hash)
|
26
|
+
class_eval(<<-EOS, __FILE__, __LINE__)
|
27
|
+
unless defined? @@#{sym}
|
28
|
+
@@#{sym} = nil
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.#{sym}
|
32
|
+
@@#{sym}
|
33
|
+
end
|
34
|
+
|
35
|
+
def #{sym}
|
36
|
+
@@#{sym}
|
37
|
+
end
|
38
|
+
EOS
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def mattr_writer(*syms)
|
43
|
+
options = syms.extract_options!
|
44
|
+
syms.each do |sym|
|
45
|
+
class_eval(<<-EOS, __FILE__, __LINE__)
|
46
|
+
unless defined? @@#{sym}
|
47
|
+
@@#{sym} = nil
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.#{sym}=(obj)
|
51
|
+
@@#{sym} = obj
|
52
|
+
end
|
53
|
+
|
54
|
+
#{"
|
55
|
+
def #{sym}=(obj)
|
56
|
+
@@#{sym} = obj
|
57
|
+
end
|
58
|
+
" unless options[:instance_writer] == false }
|
59
|
+
EOS
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def mattr_accessor(*syms)
|
64
|
+
mattr_reader(*syms)
|
65
|
+
mattr_writer(*syms)
|
66
|
+
end
|
67
|
+
end
|
data/recurs.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "recurs/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "recurs"
|
7
|
+
s.version = Recurs::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Steve Caney Martin"]
|
10
|
+
s.email = ["steve@shakewell.co.uk"]
|
11
|
+
s.homepage = "http://rubygems.org/gems/recurs"
|
12
|
+
s.summary = %q{A recurrence generator for ical format}
|
13
|
+
s.description = %q{Specifiy you're recurrence pattern in symbols and strings and get an ical format recurrence string}
|
14
|
+
|
15
|
+
s.rubyforge_project = "recurs"
|
16
|
+
s.add_dependency('ri_cal', '>= 0.8.7')
|
17
|
+
|
18
|
+
s.files = `git ls-files`.split("\n")
|
19
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
20
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
21
|
+
s.require_paths = ["lib"]
|
22
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class Event;end
|
4
|
+
Event.send(:include, Recurs::Parser)
|
5
|
+
class Event
|
6
|
+
acts_as_recurring
|
7
|
+
end
|
8
|
+
|
9
|
+
describe Event do
|
10
|
+
before :each do
|
11
|
+
@event = Event.new
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should act as recurring" do
|
15
|
+
#@event.rrules
|
16
|
+
#@event.exrules
|
17
|
+
#@event.rdates
|
18
|
+
#@event.exdates
|
19
|
+
@event.recurs
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should add an rrule" do
|
23
|
+
@event.add_rrule(:daily).should == "RRULE:FREQ=DAILY"
|
24
|
+
@event.recurs.should == "RRULE:FREQ=DAILY"
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should add an exrule" do
|
28
|
+
@event.add_exrule(:daily).should == "EXRULE:FREQ=DAILY"
|
29
|
+
@event.recurs.should == "EXRULE:FREQ=DAILY"
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should add an rdate" do
|
33
|
+
@event.add_rdate(Date.today).should == "RDATE:#{RiCal::FastDateTime.from_date_time(Date.today.to_datetime).ical_str}"
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should add an rdate range" do
|
37
|
+
@event.add_rdate(:period => [Date.today, (Date.today+2)]).should == "RDATE;VALUE=PERIOD:#{RiCal::FastDateTime.from_date_time(Date.today.to_datetime).ical_str}/#{RiCal::FastDateTime.from_date_time((Date.today+2).to_datetime).ical_str}"
|
38
|
+
end
|
39
|
+
it "should add a list of rdates" do
|
40
|
+
@event.add_rdate(:dates => [Date.today, (Date.today+2)]).should == "RDATE:#{RiCal::FastDateTime.from_date_time(Date.today.to_datetime).ical_str},#{RiCal::FastDateTime.from_date_time((Date.today+2).to_datetime).ical_str}"
|
41
|
+
end
|
42
|
+
it "should add an exdate" do
|
43
|
+
@event.add_exdate(Date.today).should == "EXDATE:#{RiCal::FastDateTime.from_date_time(Date.today.to_datetime).ical_str}"
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
=begin
|
49
|
+
An event has recurrence
|
50
|
+
|
51
|
+
@event.recurs #"DTSTART;TZID=US-Eastern:19970902T0900
|
52
|
+
RRULE:FREQ=DAILY;COUNT=10
|
53
|
+
EXRULE:"
|
54
|
+
|
55
|
+
TODO: Ascertain how ri_cal parses complex rules
|
56
|
+
|
57
|
+
While building the recurrence the event must use private instance attributes from the recurrrence module:
|
58
|
+
|
59
|
+
The recurrence module must be mixed into the model transparently, either by using an acts_as
|
60
|
+
or by inheriting from the module
|
61
|
+
|
62
|
+
class Event < AR
|
63
|
+
acts_as_recurring_event
|
64
|
+
end
|
65
|
+
|
66
|
+
=end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Recurs do
|
4
|
+
|
5
|
+
end
|
6
|
+
|
7
|
+
describe Recurs::Parser do
|
8
|
+
#pending "add some examples to (or delete) #{__FILE__}"
|
9
|
+
|
10
|
+
it "should respond to class/module methods:" do
|
11
|
+
Recurs::Parser.rrule
|
12
|
+
Recurs::Parser.exrule
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should get basic rrule" do
|
16
|
+
Recurs::Parser.rrule.should == "RRULE:FREQ="
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should get basic exrule" do
|
20
|
+
Recurs::Parser.exrule.should == "EXRULE:FREQ="
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should build a basic daily rule" do
|
24
|
+
Recurs::Parser.rrule(:daily).should == "RRULE:FREQ=DAILY"
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should create a daily recurrence with a two day interval" do
|
28
|
+
Recurs::Parser.rrule(:daily, :interval => 2).should == "RRULE:FREQ=DAILY;INTERVAL=2"
|
29
|
+
Recurs::Parser.rrule(:daily, :repeats_every => 2).should == "RRULE:FREQ=DAILY;INTERVAL=2"
|
30
|
+
Recurs::Parser.rrule(:daily, :repeats_every => 2, :interval => 2).should == "RRULE:FREQ=DAILY;INTERVAL=2"
|
31
|
+
end
|
32
|
+
#=begin
|
33
|
+
it "should create a daily recurrence with a two day interval for ten occurrences" do
|
34
|
+
Recurs::Parser.rrule(:daily, :interval => 2, :count => 10).should == "RRULE:FREQ=DAILY;INTERVAL=2;COUNT=10"
|
35
|
+
Recurs::Parser.rrule(:daily, :interval => 2, :occurrences => 10).should == "RRULE:FREQ=DAILY;INTERVAL=2;COUNT=10"
|
36
|
+
Recurs::Parser.rrule(:daily, :interval => 2, :count => 10, :occurrences => 10).should == "RRULE:FREQ=DAILY;INTERVAL=2;COUNT=10"
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should create a valid ending, 'count' OR 'until' NOT both, PREFER 'count'" do
|
40
|
+
Recurs::Parser.rrule(:daily, :interval => 2, :count => 10).should == "RRULE:FREQ=DAILY;INTERVAL=2;COUNT=10"
|
41
|
+
Recurs::Parser.rrule(:daily, :interval => 2, :ends_at => Date.today).should == "RRULE:FREQ=DAILY;INTERVAL=2;UNTIL=#{RiCal::FastDateTime.from_date_time(Date.today.to_datetime).ical_str}"
|
42
|
+
Recurs::Parser.rrule(:daily, :interval => 2, :until => Date.today).should == "RRULE:FREQ=DAILY;INTERVAL=2;UNTIL=#{RiCal::FastDateTime.from_date_time(Date.today.to_datetime).ical_str}"
|
43
|
+
Recurs::Parser.rrule(:daily, :interval => 2, :until => Date.today, :count => 10).should == "RRULE:FREQ=DAILY;INTERVAL=2;COUNT=10"
|
44
|
+
end
|
45
|
+
#=end
|
46
|
+
it "should create a weekly occurrence" do
|
47
|
+
Recurs::Parser.rrule(:weekly).should == "RRULE:FREQ=WEEKLY"
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should build a basic daily rule with a count" do
|
51
|
+
Recurs::Parser.rrule(:daily, :count => 10).should == "RRULE:FREQ=DAILY;COUNT=10"
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should create a weekly occurrence on monday and thursday" do
|
55
|
+
Recurs::Parser.rrule(:weekly, :by_day => [1, 4]).should == "RRULE:FREQ=WEEKLY;BYDAY=MO,TH"
|
56
|
+
Recurs::Parser.rrule(:weekly, :by_day => [1, '4']).should == "RRULE:FREQ=WEEKLY;BYDAY=MO,TH"
|
57
|
+
Recurs::Parser.rrule(:weekly, :by_day => ['MO', 'TH']).should == "RRULE:FREQ=WEEKLY;BYDAY=MO,TH"
|
58
|
+
Recurs::Parser.rrule(:weekly, :by_day => ['Monday', 'THURSDAY']).should == "RRULE:FREQ=WEEKLY;BYDAY=MO,TH"
|
59
|
+
Recurs::Parser.rrule(:weekly, :by_day => [1, 'TH']).should == "RRULE:FREQ=WEEKLY;BYDAY=MO,TH"
|
60
|
+
Recurs::Parser.rrule(:weekly, :by_day => [2, 'TH']).should_not == "RRULE:FREQ=WEEKLY;BYDAY=MO,TH"
|
61
|
+
|
62
|
+
#Unique
|
63
|
+
Recurs::Parser.rrule(:weekly, :by_day => [1, 1, 4, 'TH', 'Thursday', 'MO']).should == "RRULE:FREQ=WEEKLY;BYDAY=MO,TH"
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should create a weekly occurrence for three weeks" do
|
67
|
+
Recurs::Parser.rrule(:weekly, :count => 3).should == "RRULE:FREQ=WEEKLY;COUNT=3"
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should create a weekly occurrence for three weeks on fridays" do
|
71
|
+
Recurs::Parser.rrule(:weekly, :count => 3, :by_day => 'FR').should == "RRULE:FREQ=WEEKLY;BYDAY=FR;COUNT=3"
|
72
|
+
Recurs::Parser.rrule(:weekly, :count => 3, :by_day => :friday).should == "RRULE:FREQ=WEEKLY;BYDAY=FR;COUNT=3"
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should create a weekly occurrence for three weeks on fridays and sundays" do
|
76
|
+
Recurs::Parser.rrule(:weekly, :count => 3, :by_day => ['FR', 0]).should == "RRULE:FREQ=WEEKLY;BYDAY=FR,SU;COUNT=3"
|
77
|
+
Recurs::Parser.rrule(:weekly, :count => 3, :by_day => [:friday, 0]).should == "RRULE:FREQ=WEEKLY;BYDAY=FR,SU;COUNT=3"
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should bypass badly input days and use good values" do
|
81
|
+
Recurs::Parser.rrule(:weekly, :count => 3, :by_day => ['fer', 0]).should == "RRULE:FREQ=WEEKLY;BYDAY=SU;COUNT=3"
|
82
|
+
Recurs::Parser.rrule(:weekly, :count => 3, :by_day => [:fer, 0]).should == "RRULE:FREQ=WEEKLY;BYDAY=SU;COUNT=3"
|
83
|
+
Recurs::Parser.rrule(:weekly, :count => 3, :by_day => [:fesefra, :sunday]).should == "RRULE:FREQ=WEEKLY;BYDAY=SU;COUNT=3"
|
84
|
+
end
|
85
|
+
|
86
|
+
it "should occur every month" do
|
87
|
+
Recurs::Parser.rrule(:monthly).should == "RRULE:FREQ=MONTHLY"
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should occur every year" do
|
91
|
+
Recurs::Parser.rrule(:yearly).should == "RRULE:FREQ=YEARLY"
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should occur every january of each year" do
|
95
|
+
Recurs::Parser.rrule(:yearly, :by_month => :january).should == "RRULE:FREQ=YEARLY;BYMONTH=1"
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should occur on every thursday of every janruary of each year" do
|
99
|
+
Recurs::Parser.rrule(:yearly, :by_month => :january, :by_day => :thursday).should == "RRULE:FREQ=YEARLY;BYMONTH=1;BYDAY=TH"
|
100
|
+
end
|
101
|
+
|
102
|
+
it "should have independent instance rules" do
|
103
|
+
Recurs::Parser.rrule(:yearly, :by_month => :january).should == "RRULE:FREQ=YEARLY;BYMONTH=1"
|
104
|
+
Recurs::Parser.rrule(:weekly, :count => 3, :by_day => ['fer', 0]).should == "RRULE:FREQ=WEEKLY;BYDAY=SU;COUNT=3"
|
105
|
+
Recurs::Parser.rrule(:yearly, :by_month => :january).should == "RRULE:FREQ=YEARLY;BYMONTH=1"
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
=begin
|
112
|
+
# A recurrence instance should implement a couple of attributes ( recurs, rrules, rdates, exrules )
|
113
|
+
|
114
|
+
Recurs::Parser.recurs is a composite method combining all rrules and exrules
|
115
|
+
Recurs::Parser.rrules is an array of rrules similarly exrules is the same
|
116
|
+
|
117
|
+
it "instance should respond to instance methods:" do
|
118
|
+
Recurs::Parser = Parser.new
|
119
|
+
Recurs::Parser.recurs
|
120
|
+
Recurs::Parser.rrules
|
121
|
+
end
|
122
|
+
|
123
|
+
# The Parser Class should implement singular versions ( rrule, exrule ), these methods generate atomic rules
|
124
|
+
|
125
|
+
The Parser class implements the rrule and exrule methods => alias_method_chain perhaps
|
126
|
+
( these are actually just aliases of the same method but with predefined flags )
|
127
|
+
|
128
|
+
=end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
describe Recurs::Rules do
|
3
|
+
it "should respond to class/module variables" do
|
4
|
+
Recurs::Rules.repeat_procs
|
5
|
+
Recurs::Rules.schemes
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should build Daily" do
|
9
|
+
Recurs::Rules.repeat_procs['Daily'].call(true, :count => 10).should == "RRULE:FREQ=DAILY;COUNT=10"
|
10
|
+
end
|
11
|
+
#=begin
|
12
|
+
it 'Every Weekday ( Mon - Fri )' do
|
13
|
+
Recurs::Rules.repeat_procs['Every Weekday ( Mon - Fri )'].call(true, :count => 10).should == "RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR;COUNT=10"
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'Every Mon, Wed, Fri' do
|
17
|
+
Recurs::Rules.repeat_procs['Every Mon, Wed, Fri'].call(true, :count => 10).should == "RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=10"
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'Every Tues, Thurs' do
|
21
|
+
Recurs::Rules.repeat_procs['Every Tues, Thurs'].call(true, :count => 10).should == "RRULE:FREQ=WEEKLY;BYDAY=TU,TH;COUNT=10"
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'Every Weekend' do
|
25
|
+
Recurs::Rules.repeat_procs['Every Weekend'].call(true, :count => 10).should == "RRULE:FREQ=WEEKLY;BYDAY=SU,SA;COUNT=10"
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'Weekly' do
|
29
|
+
Recurs::Rules.repeat_procs['Weekly'].call(true, :count => 10).should == "RRULE:FREQ=WEEKLY;COUNT=10"
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'Monthly' do
|
33
|
+
Recurs::Rules.repeat_procs['Monthly'].call(true, :count => 10).should == "RRULE:FREQ=MONTHLY;COUNT=10"
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'Yearly' do
|
37
|
+
Recurs::Rules.repeat_procs['Yearly'].call(true, :count => 10).should == "RRULE:FREQ=YEARLY;COUNT=10"
|
38
|
+
end
|
39
|
+
#=end
|
40
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: recurs
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
version: 0.0.1
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Steve Caney Martin
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-12-14 00:00:00 +00:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: ri_cal
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 0
|
30
|
+
- 8
|
31
|
+
- 7
|
32
|
+
version: 0.8.7
|
33
|
+
type: :runtime
|
34
|
+
version_requirements: *id001
|
35
|
+
description: Specifiy you're recurrence pattern in symbols and strings and get an ical format recurrence string
|
36
|
+
email:
|
37
|
+
- steve@shakewell.co.uk
|
38
|
+
executables: []
|
39
|
+
|
40
|
+
extensions: []
|
41
|
+
|
42
|
+
extra_rdoc_files: []
|
43
|
+
|
44
|
+
files:
|
45
|
+
- .gitignore
|
46
|
+
- Gemfile
|
47
|
+
- README
|
48
|
+
- Rakefile
|
49
|
+
- lib/recurs.rb
|
50
|
+
- lib/recurs/consts.rb
|
51
|
+
- lib/recurs/rules.rb
|
52
|
+
- lib/recurs/version.rb
|
53
|
+
- lib/util/module.rb
|
54
|
+
- recurs.gemspec
|
55
|
+
- spec/models/event_spec.rb
|
56
|
+
- spec/models/recurrence_spec.rb
|
57
|
+
- spec/models/rules_spec.rb
|
58
|
+
- spec/spec_helper.rb
|
59
|
+
has_rdoc: true
|
60
|
+
homepage: http://rubygems.org/gems/recurs
|
61
|
+
licenses: []
|
62
|
+
|
63
|
+
post_install_message:
|
64
|
+
rdoc_options: []
|
65
|
+
|
66
|
+
require_paths:
|
67
|
+
- lib
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
69
|
+
none: false
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
segments:
|
74
|
+
- 0
|
75
|
+
version: "0"
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
77
|
+
none: false
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
segments:
|
82
|
+
- 0
|
83
|
+
version: "0"
|
84
|
+
requirements: []
|
85
|
+
|
86
|
+
rubyforge_project: recurs
|
87
|
+
rubygems_version: 1.3.7
|
88
|
+
signing_key:
|
89
|
+
specification_version: 3
|
90
|
+
summary: A recurrence generator for ical format
|
91
|
+
test_files:
|
92
|
+
- spec/models/event_spec.rb
|
93
|
+
- spec/models/recurrence_spec.rb
|
94
|
+
- spec/models/rules_spec.rb
|
95
|
+
- spec/spec_helper.rb
|