recurring 0.3.10 → 0.4.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.
@@ -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: []