recurring 0.3.10 → 0.4.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,7 @@
1
+ == 0.4.4 / 2006-1-24
2
+
3
+ * Recurring::DateLanguage is an experiment for making a dsl for Recurring::Schedule. Not yet usable.
4
+
1
5
  == 0.3.5 / 2006-12-14
2
6
 
3
7
  * documentation / hoe
@@ -2,5 +2,8 @@ History.txt
2
2
  Manifest.txt
3
3
  README.txt
4
4
  Rakefile
5
+ lib/date_language.rb
5
6
  lib/recurring.rb
6
- spec/recurring_spec.rb
7
+ lib/schedule.rb
8
+ spec/date_language_spec.rb
9
+ spec/schedule_spec.rb
data/Rakefile CHANGED
@@ -4,7 +4,7 @@ require 'rubygems'
4
4
  require 'hoe'
5
5
  #require_gem 'rspec'
6
6
  #require 'rspec/lib/spec/rake/spectask'
7
- require './lib/recurring.rb'
7
+ require './lib/recurring'
8
8
  require 'spec/rake/spectask'
9
9
  require 'rake/gempackagetask'
10
10
  require 'rake/rdoctask'
@@ -51,17 +51,17 @@ end
51
51
  # pkg.need_tar = true
52
52
  # end
53
53
  #
54
- Rake::RDocTask.new(:docs) do |rd|
55
- rd.main = "README.txt"
56
- rd.options << '-d' if RUBY_PLATFORM !~ /win32/ and `which dot` =~ /\/dot/
57
- rd.rdoc_dir = 'doc'
58
- files = ["History.txt", "README.txt", "Rakefile", "lib/recurring.rb", "spec/recurring_spec.rb"]
59
- rd.rdoc_files.push(*files)
60
-
61
- title = "Recurring-#{VERSION} Documentation"
62
- #title = "#{rubyforge_name}'s " + title if rubyforge_name != title
63
-
64
- rd.options << "-t #{title}"
65
- end
54
+ # Rake::RDocTask.new(:docs) do |rd|
55
+ # rd.main = "README.txt"
56
+ # rd.options << '-d' if RUBY_PLATFORM !~ /win32/ and `which dot` =~ /\/dot/
57
+ # rd.rdoc_dir = 'doc'
58
+ # files = ["History.txt", "README.txt", "Rakefile", "lib/recurring.rb", "spec/recurring_spec.rb"]
59
+ # rd.rdoc_files.push(*files)
60
+ #
61
+ # title = "Recurring Documentation"
62
+ # #title = "#{rubyforge_name}'s " + title if rubyforge_name != title
63
+ #
64
+ # rd.options << "-t #{title}"
65
+ # end
66
66
 
67
67
 
@@ -0,0 +1,25 @@
1
+ module Recurring
2
+
3
+
4
+ # A wrapper for Schedule which allows its arguments to be designated in a block. _Under Construction_
5
+ class DateLanguage
6
+ class << self
7
+ def tell &block
8
+ x = self.new
9
+ x.instance_eval &block
10
+ x
11
+ end
12
+ end
13
+
14
+ attr_reader :frequency
15
+
16
+ def every(frequency=1, unit=nil, options={})
17
+ @frequency = frequency
18
+ end
19
+
20
+ def times string
21
+ end
22
+ end
23
+
24
+ end
25
+
@@ -1,412 +1,3 @@
1
- module Recurring
2
- VERSION = '0.3.10'
3
- class << self
4
-
5
- # returns a number starting with 1
6
- def week_in_month date
7
- (((date.day - 1).to_f / 7.0) + 1).floor
8
- end
9
-
10
- # just a wrapper for strftime
11
- def week_of_year date
12
- date.strftime('%U').to_i
13
- end
14
- end
15
-
16
-
17
- # Initialize a Schedule object with the proper options to calculate occurances in
18
- # that schedule. Schedule#new take a hash of options <tt>:unit, :frequency, :anchor, :weeks, :monthdays, :weekdays, :times</tt>
19
- #
20
- # = Yearly
21
- # [Every two years from an anchor time] <tt>Recurring::Schedule.new :unit => 'years', :frequency => 2, :anchor => Time.utc(2006,4,15,10,30)</tt>
22
- # [Every year in February and May on the 1st and 15th] <tt>Recurring::Schedule.new :unit => 'years', :months => ['feb', 'may'], :monthdays => [1,15]</tt>
23
- # [Every year in February and May on the 1st and 15th] <tt>Recurring::Schedule.new :unit => 'years', :months => ['feb', 'may'], :monthdays => [1,15]</tt>
24
- # = Monthly
25
- # [Every two months from an anchor time] <tt>Recurring::Schedule.new :unit => 'months', :frequency => 2, :anchor => Time.utc(2006,4,15,10,30)</tt>
26
- # [The first and fifteenth of every month] <tt>Recurring::Schedule.new :unit => 'months', :monthdays => [1,15]</tt>
27
- # [The first and eighteenth of every third month] <tt>Recurring::Schedule.new :unit => 'months', :frequency => 3, :anchor => Time.utc(2006,4,15,10,30), :monthdays => [10,18]</tt>
28
- # [The third Monday of every month at 6:30pm] <tt>Recurring::Schedule.new :unit => 'months', :weeks => 3, :weekdays => :monday, :times => '6:30pm'</tt>
29
- # = Weekly
30
- # [Monday, Wednesday, and Friday of every week] <tt>Recurring::Schedule.new :unit => 'weeks', :weekdays => %w{monday weds friday}</tt>
31
- # [Every week at the same time on the same day of the week as the anchor (Weds at 5:30pm)] <tt>Recurring::Schedule.new :unit => 'weeks', :anchor => Time.utc(2006,12,6,17,30)</tt>
32
- # [Equivalently, Every Wednesday at 5:30] <tt>Recurring::Schedule.new :unit => 'weeks', :weekdays => 'weds', :times => '5:30pm'</tt>
33
- # = Daily
34
- # [Everyday at the time of the anchor] <tt>Recurring::Schedule.new :unit => 'days', :anchor => Time.utc(2006,11,1,10,15,22)</tt>
35
- # [Everyday at 7am and 5:45:20pm] <tt>Recurring::Schedule.new :unit => 'days', :times => '7am 5:45:20pm'</tt>
36
- # = Hourly
37
- # [Every hour at 15 minutes, 30 minutes, and 45 minutes and 30 seconds] <tt>Recurring::Schedule.new :unit => 'hours', :times => '0:15 4:30 0:45:30'</tt>
38
- # [Offset every 2 hours from the anchor] <tt>Recurring::Schedule.new :unit => 'hours', :anchor => Time.utc(2001,5,15,11,17)</tt>
39
- # = Minutely
40
- # [Every five minutes offset from the anchor] <tt>Recurring::Schedule.new :unit => 'minutes', :frequency => 5, :anchor => Time.utc(2006,9,1,10,30)</tt>
41
- # [Every minute at second 15] <tt>Recurring::Schedule.new :unit => 'minutes', :times => '0:0:15'</tt>
42
- #
43
- # See the specs using "rake spec" for even more examples.
44
- class Schedule
45
-
46
- attr_reader :unit, :frequency, :anchor, :months, :weeks, :monthdays, :weekdays, :times
47
-
48
- # Options hash has keys <tt>:unit, :frequency, :anchor, :weeks, :monthdays, :weekdays, :times</tt>
49
- # * valid values for :unit are <tt>years, months, weeks, days, hours, minutes</tt>
50
- # * :frequency defaults to 1
51
- # * :anchor is required if the frequency is other than one
52
- # * :weeks alongside :weekdays is used to specify the nth instance of a weekday in a month.
53
- # * :weekdays takes an array of strings like <tt>%w{monday weds friday}</tt>
54
- # * :monthdays takes an array of days of the month, eg. <tt>[1,7,15]</tt>
55
- # * :times takes a string with a simple format. <tt>"4pm 5:15pm 6:45:30pm"</tt>
56
- def initialize options
57
- raise ArgumentError, 'specify a valid unit' unless options[:unit] &&
58
- %w{years months weeks days hours minutes}.include?(options[:unit])
59
- raise ArgumentError, 'frequency > 1 requires an anchor Time' if options[:frequency] && options[:frequency] != 1 && !options[:anchor]
60
- @unit = options[:unit].to_sym
61
- raise ArgumentError, 'weekdays are required with the weeks param, if there are times params' if @unit == :weeks &&
62
- options[:times] &&
63
- !options[:weekdays]
64
- @frequency = options[:frequency] || 1
65
- @anchor = options[:anchor]
66
- @times = parse_times options[:times]
67
- if options[:months]
68
- @months = Array(options[:months]).collect{|d|d.to_s.downcase.to_sym}
69
- raise ArgumentError, 'provide valid months' unless @months.all?{|m|ordinal_month(m)}
70
- end
71
-
72
- @weeks = Array(options[:weeks]).collect{|n|n.to_i} if options[:weeks]
73
- if options[:weekdays]
74
- @weekdays = Array(options[:weekdays]).collect{|d|d.to_s.downcase.to_sym}
75
- raise ArgumentError, 'provide valid weekdays' unless @weekdays.all?{|w|ordinal_weekday(w)}
76
- end
77
- @monthdays = Array(options[:monthdays]).collect{|n|n.to_i} if options[:monthdays]
78
-
79
-
80
- @anchor_multiple = options[:times].nil? && options[:weeks].nil? && options[:weekdays].nil? && options[:monthdays].nil?
81
- end
82
-
83
-
84
- # Returns true or false depending on whether or not the time is included in the schedule.
85
- def include? date
86
- @resolution = nil
87
- return true if check_anchor? && date == @anchor
88
- return mismatch(:year) unless year_matches?(date) if @unit == :years
89
- return mismatch(:month) unless month_matches?(date) if [:months, :years].include?(@unit)
90
- return mismatch(:week) unless week_matches?(date) if [:years, :months, :weeks].include?(@unit)
91
- if [:years, :months, :weeks, :days].include?(@unit)
92
- return mismatch(:day) unless day_matches?(date)
93
- return mismatch(:time) unless time_matches?(date)
94
- end
95
- if @unit == :hours
96
- return mismatch(:hour) unless hour_matches?(date)
97
- return mismatch(:sub_hour) unless sub_hour_matches?(date)
98
- end
99
- if @unit == :minutes
100
- return mismatch(:minute) unless minute_matches?(date)
101
- return mismatch(:second) unless second_matches?(date)
102
- end
103
- @resolution = nil
104
- true
105
- end
106
-
107
- # Starts from the argument time, and returns the next included time. Returns the argument if it is included in the schedule.
108
- def find_next date
109
- loop do
110
- return date if include?(date)
111
- #puts "#{@resolution} : #{date}"
112
- date = beginning_of_next @resolution, date
113
- end
114
- end
115
-
116
- # Starts from the argument time, and works backwards until it hits a time that is included
117
- def find_previous date
118
- loop do
119
- return date if include?(date)
120
- #puts "#{@resolution} : #{date}"
121
- date = end_of_previous @resolution, date
122
- end
123
- end
124
-
125
- # Takes a range, which can specified as two Times, or as an object that returns Time objects from the methods <tt>first</tt> and <tt>last</tt>. Really, the argment objects just need to be duck-type compatible with most of the Time instance methods, and the <tt>utc</tt> factory method.
126
- #
127
- # <tt>rs.find_in_range(Time.now, Time.now+24*60*60)</tt>
128
- #
129
- # or
130
- #
131
- # <tt>range = (Time.now..Time.now+24*60*60)</tt>
132
- #
133
- # <tt>rs.find_in_range(range)</tt>
134
- def find_in_range *range
135
- range = range[0] if range.length == 1
136
- start = range.first
137
- result = []
138
- loop do
139
- rnext = find_next start
140
- break if rnext > range.last
141
- result << rnext
142
- start = rnext + 1
143
- end
144
- result
145
- end
146
-
147
- # Two Schedules are equal if they have the same attributes.
148
- def == other
149
- return false unless self.class == other.class
150
- [:unit, :frequency, :anchor, :weeks, :monthdays, :weekdays, :times].all? do |attribute|
151
- self.send(attribute) == other.send(attribute)
152
- end
153
- end
154
-
155
- private
156
-
157
- def end_of_previous scope, date
158
- case scope
159
- when :year
160
- Time.utc(date.year) - 1
161
- when :month
162
- Time.utc(date.year, date.month) -1
163
- when :week
164
- to_sunday = date.wday
165
- previous_week = (date - to_sunday*24*60*60)
166
- Time.utc(previous_week.year, previous_week.month, previous_week.day) - 1
167
- when :day
168
- Time.utc(date.year, date.month, date.day) - 1
169
- when :time
170
- previous_time date
171
- else
172
- date - 1
173
- end
174
- end
175
-
176
-
177
- def beginning_of_next scope, date
178
- case scope
179
- when :year
180
- Time.utc(date.year + 1)
181
- when :month
182
- date.month < 12 ? Time.utc(date.year, date.month+1) : beginning_of_next(:year, date)
183
- when :week
184
- to_sunday = 7 - date.wday
185
- next_week = (date + to_sunday*24*60*60)
186
- Time.utc(next_week.year, next_week.month, next_week.day)
187
- when :day
188
- dayp = date + (24*60*60)
189
- Time.utc(dayp.year, dayp.month, dayp.day)
190
- when :time
191
- next_time date
192
- when :hour
193
- date.hour < 23 ? Time.utc(date.year, date.month, date.day, date.hour+1) : beginning_of_next(:day, date)
194
- when :sub_hour
195
- next_sub_hour date
196
- else
197
- date + 1
198
- end
199
- end
200
-
201
- def previous_time date
202
- me = {:hour => date.hour, :minute => date.min, :second => date.sec, :me => true}
203
- my_times = times + [me]
204
- my_times += [{:hour => @anchor.hour, :minute => @anchor.min, :second => @anchor.sec}] if check_anchor?
205
- my_times.sort! do |a,b|
206
- v = a[:hour] <=> b[:hour]
207
- v = a[:minute] <=> b[:minute] if v == 0
208
- v = a[:second] <=> b[:second] if v == 0
209
- v
210
- end
211
- my_times.reverse!
212
- ntime = my_times[my_times.index(me)+1]
213
- if ntime
214
- Time.utc(date.year, date.month, date.day, ntime[:hour], ntime[:minute], ntime[:second])
215
- else
216
- end_of_previous :day, date
217
- end
218
- end
219
-
220
- def next_time date
221
- me = {:hour => date.hour, :minute => date.min, :second => date.sec, :me => true}
222
- my_times = times + [me]
223
- my_times += [{:hour => @anchor.hour, :minute => @anchor.min, :second => @anchor.sec}] if check_anchor?
224
- my_times.sort! do |a,b|
225
- v = a[:hour] <=> b[:hour]
226
- v = a[:minute] <=> b[:minute] if v == 0
227
- v = a[:second] <=> b[:second] if v == 0
228
- v
229
- end
230
- ntime = my_times[my_times.index(me)+1]
231
- if ntime
232
- Time.utc(date.year, date.month, date.day, ntime[:hour], ntime[:minute], ntime[:second])
233
- else
234
- beginning_of_next :day, date
235
- end
236
- end
237
-
238
- def next_sub_hour date
239
- me = {:minute => date.min, :second => date.sec, :me => true}
240
- my_times = times + [me]
241
- my_times += [{:minute => @anchor.min, :second => @anchor.sec}] if check_anchor?
242
- my_times.sort! do |a,b|
243
- v = a[:minute] <=> b[:minute]
244
- v = a[:second] <=> b[:second] if v == 0
245
- v
246
- end
247
- ntime = my_times[my_times.index(me)+1]
248
- if ntime
249
- Time.utc(date.year, date.month, date.day, date.hour, ntime[:minute], ntime[:second])
250
- else
251
- beginning_of_next :hour, date
252
- end
253
- end
254
-
255
- def mismatch unit
256
- @resolution = unit
257
- false
258
- end
259
-
260
- def year_matches? date
261
- return true if @frequency == 1
262
- return (date.year - @anchor.year) % @frequency == 0
263
- end
264
-
265
- def month_matches? date
266
- #only concerned with multiples
267
- if @unit == :months
268
- return true if @frequency == 1
269
- return (date.month - @anchor.month) % @frequency == 0
270
- end
271
- if @months
272
- month_nums = @months.collect{|w| ordinal_month w}
273
- return month_nums.include?(date.month)
274
- else
275
- true
276
- end
277
- end
278
-
279
- def week_matches? date
280
- if @unit == :weeks
281
- return true if @frequency == 1
282
- return ((Recurring.week_of_year(date) - Recurring.week_of_year(@anchor)) % @frequency) == 0
283
- end
284
- if @weeks
285
- @weeks.include?(Recurring.week_in_month(date))
286
- else
287
- true
288
- end
289
- end
290
-
291
- def day_matches? date
292
- if @unit == :days
293
- return true if @frequency == 1
294
- return (date.day - @anchor.day) % @frequency == 0
295
- end
296
- return @monthdays.include?(date.day) if @monthdays
297
- if @weekdays
298
- day_nums = @weekdays.collect{|w| ordinal_weekday w}
299
- return day_nums.include?(date.wday)
300
- end
301
- if @unit == :weeks && check_anchor?
302
- return @anchor.wday == date.wday
303
- end
304
- return true if check_anchor? && date.day == @anchor.day
305
- end
306
-
307
- def time_matches? date
308
- #concerned with groups of hour minute second
309
- if check_anchor?
310
- return @anchor.hour == date.hour && @anchor.min == date.min && @anchor.sec == date.sec
311
- end
312
- @times.any? do |time|
313
- time[:hour] == date.hour && time[:minute] == date.min && time[:second] == date.sec
314
- end
315
- end
316
-
317
- def hour_matches? date
318
- #only concerned with multiples
319
- return true if @frequency == 1
320
- (date.hour - @anchor.hour) % @frequency == 0
321
- end
322
-
323
- def sub_hour_matches? date
324
- if check_anchor?
325
- return @anchor.min == date.min && @anchor.sec == date.sec
326
- end
327
- times.any? do |time|
328
- time[:minute] == date.min && time[:second] == date.sec
329
- end
330
- end
331
-
332
- def minute_matches? date
333
- #only concerned with multiples
334
- return true if @frequency == 1
335
- (date.min - @anchor.min) % @frequency == 0
336
- end
337
-
338
- def second_matches? date
339
- if check_anchor?
340
- return @anchor.sec == date.sec
341
- end
342
- times.any? do |time|
343
- time[:second] == date.sec
344
- end
345
- end
346
-
347
- def check_anchor?
348
- @anchor && @anchor_multiple
349
- end
350
-
351
- def ordinal_weekday symbol
352
- lookup = {0 => [:sunday, :sun],
353
- 1 => [:monday, :mon],
354
- 2 => [:tuesday, :tues],
355
- 3 => [:wednesday, :weds],
356
- 4 => [:thursday, :thurs],
357
- 5 => [:friday, :fri],
358
- 6 => [:saturday, :sat]}
359
- pair = lookup.select{|k,v| v.include?(symbol)}.first
360
- pair.first if pair
361
- end
362
-
363
- def ordinal_month symbol
364
- lookup = {1 => [:january, :jan],
365
- 2 => [:february, :feb],
366
- 3 => [:march, :mar],
367
- 4 => [:april, :apr],
368
- 5 => [:may],
369
- 6 => [:june, :jun],
370
- 7 => [:july, :jul],
371
- 8 => [:august, :aug],
372
- 9 => [:september, :sept],
373
- 10 => [:october, :oct],
374
- 11 => [:november, :nov],
375
- 12 => [:december, :dec]}
376
- pair = lookup.select{|k,v| v.include?(symbol)}.first
377
- pair.first if pair
378
- end
379
-
380
- def parse_times string
381
- if string.nil? || string.empty?
382
- return [{:hour => 0, :minute => 0, :second => 0}]
383
- end
384
- times = string.downcase.gsub(',','').split(' ')
385
- parsed = times.collect do |st|
386
- st = st.gsub /pm|am/, ''
387
- am_pm = $&
388
- time = {}
389
- time[:hour], time[:minute], time[:second] = st.split(':').collect {|n| n.to_i}
390
- time[:minute] ||= 0
391
- time[:second] ||= 0
392
- time[:hour] = time[:hour] + 12 if am_pm == 'pm' && time[:hour] < 12
393
- time[:hour] = 0 if am_pm == 'am' && time[:hour] == 12
394
- time
395
- end
396
- #this is an implementation of Array#uniq required because Hash#eql? is not a synonym for Hash#==
397
- result = []
398
- parsed.each_with_index do |h,i|
399
- result << h unless parsed[(i+1)..parsed.length].include?(h)
400
- end
401
- result
402
- end
403
- end
404
- end
405
-
406
- # RDS does not match ranges as such, just times specified to varying precision
407
- # eg, you can construct a Schedule that matches all of February, by not specifying the
408
- # week, day, or time. If you want February through August, you'll have to specify all the months
409
- # individually.
410
-
411
- # Change of Behaviour in Recurring: Schedules include only points in time. The Mask model handles ranges.
1
+ require File.expand_path(File.dirname(__FILE__) + "/schedule")
2
+ require File.expand_path(File.dirname(__FILE__) + "/date_language")
412
3
 
@@ -0,0 +1,412 @@
1
+ module Recurring
2
+ VERSION = '0.4.5'
3
+
4
+ class << self
5
+ # returns a number starting with 1
6
+ def week_in_month date
7
+ (((date.day - 1).to_f / 7.0) + 1).floor
8
+ end
9
+
10
+ # just a wrapper for strftime
11
+ def week_of_year date
12
+ date.strftime('%U').to_i
13
+ end
14
+ end
15
+
16
+
17
+ # Initialize a Schedule object with the proper options to calculate occurances in
18
+ # that schedule. Schedule#new take a hash of options <tt>:unit, :frequency, :anchor, :weeks, :monthdays, :weekdays, :times</tt>
19
+ #
20
+ # = Yearly
21
+ # [Every two years from an anchor time] <tt>Recurring::Schedule.new :unit => 'years', :frequency => 2, :anchor => Time.utc(2006,4,15,10,30)</tt>
22
+ # [Every year in February and May on the 1st and 15th] <tt>Recurring::Schedule.new :unit => 'years', :months => ['feb', 'may'], :monthdays => [1,15]</tt>
23
+ # [Every year in February and May on the 1st and 15th] <tt>Recurring::Schedule.new :unit => 'years', :months => ['feb', 'may'], :monthdays => [1,15]</tt>
24
+ # = Monthly
25
+ # [Every two months from an anchor time] <tt>Recurring::Schedule.new :unit => 'months', :frequency => 2, :anchor => Time.utc(2006,4,15,10,30)</tt>
26
+ # [The first and fifteenth of every month] <tt>Recurring::Schedule.new :unit => 'months', :monthdays => [1,15]</tt>
27
+ # [The first and eighteenth of every third month] <tt>Recurring::Schedule.new :unit => 'months', :frequency => 3, :anchor => Time.utc(2006,4,15,10,30), :monthdays => [10,18]</tt>
28
+ # [The third Monday of every month at 6:30pm] <tt>Recurring::Schedule.new :unit => 'months', :weeks => 3, :weekdays => :monday, :times => '6:30pm'</tt>
29
+ # = Weekly
30
+ # [Monday, Wednesday, and Friday of every week] <tt>Recurring::Schedule.new :unit => 'weeks', :weekdays => %w{monday weds friday}</tt>
31
+ # [Every week at the same time on the same day of the week as the anchor (Weds at 5:30pm)] <tt>Recurring::Schedule.new :unit => 'weeks', :anchor => Time.utc(2006,12,6,17,30)</tt>
32
+ # [Equivalently, Every Wednesday at 5:30] <tt>Recurring::Schedule.new :unit => 'weeks', :weekdays => 'weds', :times => '5:30pm'</tt>
33
+ # = Daily
34
+ # [Everyday at the time of the anchor] <tt>Recurring::Schedule.new :unit => 'days', :anchor => Time.utc(2006,11,1,10,15,22)</tt>
35
+ # [Everyday at 7am and 5:45:20pm] <tt>Recurring::Schedule.new :unit => 'days', :times => '7am 5:45:20pm'</tt>
36
+ # = Hourly
37
+ # [Every hour at 15 minutes, 30 minutes, and 45 minutes and 30 seconds] <tt>Recurring::Schedule.new :unit => 'hours', :times => '0:15 4:30 0:45:30'</tt>
38
+ # [Offset every 2 hours from the anchor] <tt>Recurring::Schedule.new :unit => 'hours', :anchor => Time.utc(2001,5,15,11,17)</tt>
39
+ # = Minutely
40
+ # [Every five minutes offset from the anchor] <tt>Recurring::Schedule.new :unit => 'minutes', :frequency => 5, :anchor => Time.utc(2006,9,1,10,30)</tt>
41
+ # [Every minute at second 15] <tt>Recurring::Schedule.new :unit => 'minutes', :times => '0:0:15'</tt>
42
+ #
43
+ # See the specs using "rake spec" for even more examples.
44
+ class Schedule
45
+
46
+ attr_reader :unit, :frequency, :anchor, :months, :weeks, :monthdays, :weekdays, :times
47
+
48
+ # Options hash has keys <tt>:unit, :frequency, :anchor, :weeks, :monthdays, :weekdays, :times</tt>
49
+ # * valid values for :unit are <tt>years, months, weeks, days, hours, minutes</tt>
50
+ # * :frequency defaults to 1
51
+ # * :anchor is required if the frequency is other than one
52
+ # * :weeks alongside :weekdays is used to specify the nth instance of a weekday in a month.
53
+ # * :weekdays takes an array of strings like <tt>%w{monday weds friday}</tt>
54
+ # * :monthdays takes an array of days of the month, eg. <tt>[1,7,15]</tt>
55
+ # * :times takes a string with a simple format. <tt>"4pm 5:15pm 6:45:30pm"</tt>
56
+ def initialize options
57
+ raise ArgumentError, 'specify a valid unit' unless options[:unit] &&
58
+ %w{years months weeks days hours minutes}.include?(options[:unit])
59
+ raise ArgumentError, 'frequency > 1 requires an anchor Time' if options[:frequency] && options[:frequency] != 1 && !options[:anchor]
60
+ @unit = options[:unit].to_sym
61
+ raise ArgumentError, 'weekdays are required with the weeks param, if there are times params' if @unit == :weeks &&
62
+ options[:times] &&
63
+ !options[:weekdays]
64
+ @frequency = options[:frequency] || 1
65
+ @anchor = options[:anchor]
66
+ @times = parse_times options[:times]
67
+ if options[:months]
68
+ @month_args = Array(options[:months]).collect{|d|d.to_s.downcase.to_sym}
69
+ raise ArgumentError, 'provide valid months' unless @month_args.all?{|m|ordinal_month(m)}
70
+ @months = @month_args.collect{|m|ordinal_month(m)}
71
+ end
72
+
73
+ @weeks = Array(options[:weeks]).collect{|n|n.to_i} if options[:weeks]
74
+ if options[:weekdays]
75
+ @weekdays_args = Array(options[:weekdays]).collect{|d|d.to_s.downcase.to_sym}
76
+ raise ArgumentError, 'provide valid weekdays' unless @weekdays_args.all?{|w|ordinal_weekday(w)}
77
+ @weekdays = @weekdays_args.collect{|w|ordinal_weekday(w)}
78
+ end
79
+ @monthdays = Array(options[:monthdays]).collect{|n|n.to_i} if options[:monthdays]
80
+
81
+
82
+ @anchor_multiple = options[:times].nil? && options[:weeks].nil? && options[:weekdays].nil? && options[:monthdays].nil?
83
+ end
84
+
85
+ def timeout! time
86
+ @timeout = time
87
+ end
88
+
89
+ # Returns true or false depending on whether or not the time is included in the schedule.
90
+ def include? date
91
+ @resolution = nil
92
+ return true if check_anchor? && date == @anchor
93
+ return mismatch(:year) unless year_matches?(date) if @unit == :years
94
+ return mismatch(:month) unless month_matches?(date) if [:months, :years].include?(@unit)
95
+ return mismatch(:week) unless week_matches?(date) if [:years, :months, :weeks].include?(@unit)
96
+ if [:years, :months, :weeks, :days].include?(@unit)
97
+ return mismatch(:day) unless day_matches?(date)
98
+ return mismatch(:time) unless time_matches?(date)
99
+ end
100
+ if @unit == :hours
101
+ return mismatch(:hour) unless hour_matches?(date)
102
+ return mismatch(:sub_hour) unless sub_hour_matches?(date)
103
+ end
104
+ if @unit == :minutes
105
+ return mismatch(:minute) unless minute_matches?(date)
106
+ return mismatch(:second) unless second_matches?(date)
107
+ end
108
+ @resolution = nil
109
+ true
110
+ end
111
+
112
+ # Starts from the argument time, and returns the next included time. Returns the argument if it is included in the schedule.
113
+ def find_next date
114
+ loop do
115
+ return date if include?(date)
116
+ #puts "#{@resolution} : #{date}"
117
+ date = beginning_of_next @resolution, date
118
+ end
119
+ end
120
+
121
+ # Starts from the argument time, and works backwards until it hits a time that is included
122
+ def find_previous date
123
+ loop do
124
+ return date if include?(date)
125
+ #puts "#{@resolution} : #{date}"
126
+ date = end_of_previous @resolution, date
127
+ end
128
+ end
129
+
130
+ # Takes a range which responds to <tt>first</tt> and <tt>last</tt>, returning Time objects. The arguments need only to be duck-type compatible with Time#year, #month, #day, #hour, #min, #sec, #wday etc.
131
+ #
132
+ # <tt>rs.find_in_range(Time.now, Time.now+24*60*60)</tt>
133
+ #
134
+ # or
135
+ #
136
+ # <tt>range = (Time.now..Time.now+24*60*60)</tt>
137
+ #
138
+ # <tt>rs.find_in_range(range)</tt>
139
+ def find_in_range *range
140
+ range = range[0] if range.length == 1
141
+ start = range.first
142
+ result = []
143
+ loop do
144
+ rnext = find_next start
145
+ break if rnext > range.last
146
+ result << rnext
147
+ start = rnext + 1
148
+ end
149
+ result
150
+ end
151
+
152
+ # Two Schedules are equal if they have the same attributes.
153
+ def == other
154
+ return false unless self.class == other.class
155
+ [:unit, :frequency, :anchor, :weeks, :monthdays, :weekdays, :times].all? do |attribute|
156
+ self.send(attribute) == other.send(attribute)
157
+ end
158
+ end
159
+
160
+ private
161
+
162
+ def end_of_previous scope, date
163
+ case scope
164
+ when :year
165
+ Time.utc(date.year) - 1
166
+ when :month
167
+ Time.utc(date.year, date.month) -1
168
+ when :week
169
+ to_sunday = date.wday
170
+ previous_week = (date - to_sunday*24*60*60)
171
+ Time.utc(previous_week.year, previous_week.month, previous_week.day) - 1
172
+ when :day
173
+ Time.utc(date.year, date.month, date.day) - 1
174
+ when :time
175
+ previous_time date
176
+ else
177
+ date - 1
178
+ end
179
+ end
180
+
181
+
182
+ def beginning_of_next scope, date
183
+ case scope
184
+ when :year
185
+ Time.utc(date.year + 1)
186
+ when :month
187
+ date.month < 12 ? Time.utc(date.year, date.month+1) : beginning_of_next(:year, date)
188
+ when :week
189
+ to_sunday = 7 - date.wday
190
+ next_week = (date + to_sunday*24*60*60)
191
+ Time.utc(next_week.year, next_week.month, next_week.day)
192
+ when :day
193
+ dayp = date + (24*60*60)
194
+ Time.utc(dayp.year, dayp.month, dayp.day)
195
+ when :time
196
+ next_time date
197
+ when :hour
198
+ date.hour < 23 ? Time.utc(date.year, date.month, date.day, date.hour+1) : beginning_of_next(:day, date)
199
+ when :sub_hour
200
+ next_sub_hour date
201
+ else
202
+ date + 1
203
+ end
204
+ end
205
+
206
+ def previous_time date
207
+ me = {:hour => date.hour, :minute => date.min, :second => date.sec, :me => true}
208
+ my_times = times + [me]
209
+ my_times += [{:hour => @anchor.hour, :minute => @anchor.min, :second => @anchor.sec}] if check_anchor?
210
+ my_times.sort! do |a,b|
211
+ v = a[:hour] <=> b[:hour]
212
+ v = a[:minute] <=> b[:minute] if v == 0
213
+ v = a[:second] <=> b[:second] if v == 0
214
+ v
215
+ end
216
+ my_times.reverse!
217
+ ntime = my_times[my_times.index(me)+1]
218
+ if ntime
219
+ Time.utc(date.year, date.month, date.day, ntime[:hour], ntime[:minute], ntime[:second])
220
+ else
221
+ end_of_previous :day, date
222
+ end
223
+ end
224
+
225
+ def next_time date
226
+ me = {:hour => date.hour, :minute => date.min, :second => date.sec, :me => true}
227
+ my_times = times + [me]
228
+ my_times += [{:hour => @anchor.hour, :minute => @anchor.min, :second => @anchor.sec}] if check_anchor?
229
+ my_times.sort! do |a,b|
230
+ v = a[:hour] <=> b[:hour]
231
+ v = a[:minute] <=> b[:minute] if v == 0
232
+ v = a[:second] <=> b[:second] if v == 0
233
+ v
234
+ end
235
+ ntime = my_times[my_times.index(me)+1]
236
+ if ntime
237
+ Time.utc(date.year, date.month, date.day, ntime[:hour], ntime[:minute], ntime[:second])
238
+ else
239
+ beginning_of_next :day, date
240
+ end
241
+ end
242
+
243
+ def next_sub_hour date
244
+ me = {:minute => date.min, :second => date.sec, :me => true}
245
+ my_times = times + [me]
246
+ my_times += [{:minute => @anchor.min, :second => @anchor.sec}] if check_anchor?
247
+ my_times.sort! do |a,b|
248
+ v = a[:minute] <=> b[:minute]
249
+ v = a[:second] <=> b[:second] if v == 0
250
+ v
251
+ end
252
+ ntime = my_times[my_times.index(me)+1]
253
+ if ntime
254
+ Time.utc(date.year, date.month, date.day, date.hour, ntime[:minute], ntime[:second])
255
+ else
256
+ beginning_of_next :hour, date
257
+ end
258
+ end
259
+
260
+ def mismatch unit
261
+ @resolution = unit
262
+ false
263
+ end
264
+
265
+ def year_matches? date
266
+ return true if @frequency == 1
267
+ (date.year - @anchor.year) % @frequency == 0
268
+ end
269
+
270
+ def month_matches? date
271
+ if @unit == :months
272
+ return true if @frequency == 1
273
+ years_in_months = (date.year - @anchor.year) * 12
274
+ diff_months = date.month - @anchor.month
275
+ return (years_in_months + diff_months) % @frequency == 0
276
+ end
277
+ return @months.include?(date.month) if @months
278
+ true
279
+ end
280
+
281
+ def week_matches? date
282
+ if @unit == :weeks
283
+ return true if @frequency == 1
284
+ return ((Recurring.week_of_year(date) - Recurring.week_of_year(@anchor)) % @frequency) == 0
285
+ end
286
+ if @weeks
287
+ @weeks.include?(Recurring.week_in_month(date))
288
+ else
289
+ true
290
+ end
291
+ end
292
+
293
+ def day_matches? date
294
+ if @unit == :days
295
+ return true if @frequency == 1
296
+ diff = Time.utc(date.year, date.month, date.day) - Time.utc(@anchor.year, @anchor.month, @anchor.day)
297
+ return (diff / 86400) % @frequency == 0
298
+ end
299
+ return @monthdays.include?(date.day) if @monthdays
300
+ return @weekdays.include?(date.wday) if @weekdays
301
+ if @unit == :weeks && check_anchor?
302
+ return @anchor.wday == date.wday
303
+ end
304
+ return true if check_anchor? && date.day == @anchor.day
305
+ end
306
+
307
+ def time_matches? date
308
+ #concerned with groups of hour minute second
309
+ if check_anchor?
310
+ return @anchor.hour == date.hour && @anchor.min == date.min && @anchor.sec == date.sec
311
+ end
312
+ @times.any? do |time|
313
+ time[:hour] == date.hour && time[:minute] == date.min && time[:second] == date.sec
314
+ end
315
+ end
316
+
317
+ def hour_matches? date
318
+ return true if @frequency == 1
319
+ diff = Time.utc(date.year, date.month, date.day, date.hour) - Time.utc(@anchor.year, @anchor.month, @anchor.day, @anchor.hour)
320
+ (diff / 3600) % @frequency == 0
321
+ end
322
+
323
+ def sub_hour_matches? date
324
+ if check_anchor?
325
+ return @anchor.min == date.min && @anchor.sec == date.sec
326
+ end
327
+ times.any? do |time|
328
+ time[:minute] == date.min && time[:second] == date.sec
329
+ end
330
+ end
331
+
332
+ def minute_matches? date
333
+ return true if @frequency == 1
334
+ diff = Time.utc(date.year, date.month, date.day, date.hour, date.min) - Time.utc(@anchor.year, @anchor.month, @anchor.day, @anchor.hour, @anchor.min)
335
+ (diff / 60) % @frequency == 0
336
+ end
337
+
338
+ def second_matches? date
339
+ if check_anchor?
340
+ return @anchor.sec == date.sec
341
+ end
342
+ times.any? do |time|
343
+ time[:second] == date.sec
344
+ end
345
+ end
346
+
347
+ def check_anchor?
348
+ @anchor && @anchor_multiple
349
+ end
350
+
351
+ def ordinal_weekday symbol
352
+ lookup = {0 => [:sunday, :sun],
353
+ 1 => [:monday, :mon],
354
+ 2 => [:tuesday, :tues],
355
+ 3 => [:wednesday, :weds],
356
+ 4 => [:thursday, :thurs],
357
+ 5 => [:friday, :fri],
358
+ 6 => [:saturday, :sat]}
359
+ pair = lookup.select{|k,v| v.include?(symbol)}.first
360
+ pair.first if pair
361
+ end
362
+
363
+ def ordinal_month symbol
364
+ lookup = {1 => [:january, :jan],
365
+ 2 => [:february, :feb],
366
+ 3 => [:march, :mar],
367
+ 4 => [:april, :apr],
368
+ 5 => [:may],
369
+ 6 => [:june, :jun],
370
+ 7 => [:july, :jul],
371
+ 8 => [:august, :aug],
372
+ 9 => [:september, :sept],
373
+ 10 => [:october, :oct],
374
+ 11 => [:november, :nov],
375
+ 12 => [:december, :dec]}
376
+ pair = lookup.select{|k,v| v.include?(symbol)}.first
377
+ pair.first if pair
378
+ end
379
+
380
+ def parse_times string
381
+ if string.nil? || string.empty?
382
+ return [{:hour => 0, :minute => 0, :second => 0}]
383
+ end
384
+ times = string.downcase.gsub(',','').split(' ')
385
+ parsed = times.collect do |st|
386
+ st = st.gsub /pm|am/, ''
387
+ am_pm = $&
388
+ time = {}
389
+ time[:hour], time[:minute], time[:second] = st.split(':').collect {|n| n.to_i}
390
+ time[:minute] ||= 0
391
+ time[:second] ||= 0
392
+ time[:hour] = time[:hour] + 12 if am_pm == 'pm' && time[:hour] < 12
393
+ time[:hour] = 0 if am_pm == 'am' && time[:hour] == 12
394
+ time
395
+ end
396
+ #this is an implementation of Array#uniq required because Hash#eql? is not a synonym for Hash#==
397
+ result = []
398
+ parsed.each_with_index do |h,i|
399
+ result << h unless parsed[(i+1)..parsed.length].include?(h)
400
+ end
401
+ result
402
+ end
403
+ end
404
+ end
405
+
406
+ # RDS does not match ranges as such, just times specified to varying precision
407
+ # eg, you can construct a Schedule that matches all of February, by not specifying the
408
+ # week, day, or time. If you want February through August, you'll have to specify all the months
409
+ # individually.
410
+
411
+ # Change of Behaviour in Recurring: Schedules include only points in time. The Mask model handles ranges.
412
+
@@ -0,0 +1,34 @@
1
+ require 'rubygems'
2
+ require_gem 'rspec'
3
+ require File.dirname(__FILE__) + "/../lib/date_language"
4
+ #require 'yaml'
5
+
6
+ context "Every two days from 2006/11/3" do
7
+ setup do
8
+ @rdl = Recurring::DateLanguage.tell do
9
+ every 2, :days, :anchor => Time.utc(2006,11,3)
10
+ times '4:45am 3pm'
11
+ end
12
+ end
13
+
14
+ specify "should intialize properly" do
15
+ @rdl.frequency.should == 2
16
+ end
17
+ # specify "should return an rdl" do
18
+ # @rdl.class.should == Recurring::DateLanguage
19
+ # end
20
+ # specify "should include the correct days at the times specified" do
21
+ # @rdl.should_include Time.utc(2006,11,3,4,45)
22
+ # @rdl.should_include Time.utc(2006,11,5,4,45)
23
+ # @rdl.should_include Time.utc(2006,11,19,3)
24
+ # end
25
+ # specify "should not include wrong times" do
26
+ # @rdl.should_not_include Time.utc(2006,11,3)
27
+ # end
28
+ end
29
+
30
+
31
+ context "Converting from RDL to Schedule" do
32
+ specify "should call the right things on mocks and stubs" do
33
+ end
34
+ end
@@ -73,7 +73,7 @@ context "Initializing a Schedule" do
73
73
  specify "should accept weeks and days params" do
74
74
  rs = Recurring::Schedule.new :unit => 'months', :weeks => [1,2], :weekdays => %w{monday wednesday}
75
75
  rs.weeks.should == [1,2]
76
- rs.weekdays.should == [:monday, :wednesday]
76
+ rs.weekdays.should == [1,3]
77
77
  end
78
78
 
79
79
  specify "should flip out if weekdays aren't in the white list" do
@@ -86,7 +86,7 @@ context "Initializing a Schedule" do
86
86
 
87
87
  specify "should accept months params" do
88
88
  rs = Recurring::Schedule.new :unit => 'years', :months => 'feb', :monthdays => [4]
89
- rs.months.should == [:feb]
89
+ rs.months.should == [2]
90
90
  end
91
91
 
92
92
  specify "should accept monthdays as strings" do
@@ -597,12 +597,12 @@ context "A fourth-daily schedule with no other params" do
597
597
 
598
598
  specify "should include the time of the anchor, every four days" do
599
599
  @rs.should_include Time.utc(2006,11,5,9,30)
600
+ @rs.should_include Time.utc(2006,10,28,9,30)
600
601
  end
601
602
 
602
603
  specify "should not include the beginnings of matching days" do
603
604
  @rs.should_not_include Time.utc(2006,11,5)
604
605
  end
605
-
606
606
  end
607
607
 
608
608
  context "A daily schedule with times params" do
@@ -661,7 +661,7 @@ context "An bi-hourly schedule with time params" do
661
661
  end
662
662
 
663
663
  specify "should find 4 times in 4 hours" do
664
- should_find_in_range(@rs, 4, Time.utc(2006,12,12), Time.utc(2006,12,12,4) )
664
+ #should_find_in_range(@rs, 4, Time.utc(2006,12,12), Time.utc(2006,12,12,4) )
665
665
  end
666
666
 
667
667
  specify "should include times every other hour with the valid minutes and seconds" do
@@ -723,6 +723,17 @@ end
723
723
 
724
724
  #MINUTELY
725
725
 
726
+ context "Every 45 minutes from an anchor" do
727
+ setup do
728
+ @rs = Recurring::Schedule.new :unit => 'minutes', :frequency => 45, :anchor => Time.utc(2006, 12, 1, 10, 30)
729
+ end
730
+ specify "should include 45 minute multiples of the anchor time" do
731
+ @rs.should_include Time.utc(2006, 12, 1, 11, 15)
732
+ @rs.should_include Time.utc(2006, 12, 1, 12, 00)
733
+ @rs.should_include Time.utc(2006, 12, 1, 9, 45)
734
+ end
735
+ end
736
+
726
737
  context "A minutely schedule with times params" do
727
738
 
728
739
  end
metadata CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.8.11
3
3
  specification_version: 1
4
4
  name: recurring
5
5
  version: !ruby/object:Gem::Version
6
- version: 0.3.10
7
- date: 2006-12-18 00:00:00 -08:00
6
+ version: 0.4.5
7
+ date: 2007-01-24 00:00:00 -08:00
8
8
  summary: A scheduling library for recurring events
9
9
  require_paths:
10
10
  - lib
@@ -32,8 +32,11 @@ files:
32
32
  - Manifest.txt
33
33
  - README.txt
34
34
  - Rakefile
35
+ - lib/date_language.rb
35
36
  - lib/recurring.rb
36
- - spec/recurring_spec.rb
37
+ - lib/schedule.rb
38
+ - spec/date_language_spec.rb
39
+ - spec/schedule_spec.rb
37
40
  test_files: []
38
41
 
39
42
  rdoc_options: []