say_when 0.1.0
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 +5 -0
- data/Gemfile +4 -0
- data/Rakefile +1 -0
- data/generators/say_when_migration/say_when_migration_generator.rb +11 -0
- data/generators/say_when_migration/templates/migration.rb +48 -0
- data/lib/generators/.DS_Store +0 -0
- data/lib/generators/say_when/migration/migration_generator.rb +13 -0
- data/lib/generators/say_when/migration/templates/migration.rb +47 -0
- data/lib/say_when/base_job.rb +96 -0
- data/lib/say_when/cron_expression.rb +621 -0
- data/lib/say_when/processor/active_messaging.rb +22 -0
- data/lib/say_when/processor/base.rb +17 -0
- data/lib/say_when/processor/simple.rb +15 -0
- data/lib/say_when/scheduler.rb +129 -0
- data/lib/say_when/storage/active_record/acts.rb +85 -0
- data/lib/say_when/storage/active_record/job.rb +85 -0
- data/lib/say_when/storage/active_record/job_execution.rb +17 -0
- data/lib/say_when/storage/memory/base.rb +34 -0
- data/lib/say_when/storage/memory/job.rb +48 -0
- data/lib/say_when/storage/mongoid/job.rb +15 -0
- data/lib/say_when/tasks.rb +22 -0
- data/lib/say_when/triggers/base.rb +11 -0
- data/lib/say_when/triggers/cron_strategy.rb +22 -0
- data/lib/say_when/triggers/once_strategy.rb +30 -0
- data/lib/say_when/version.rb +3 -0
- data/lib/say_when.rb +28 -0
- data/lib/tasks/say_when.rake +2 -0
- data/say_when.gemspec +26 -0
- data/spec/active_record_spec_helper.rb +11 -0
- data/spec/db/schema.rb +36 -0
- data/spec/db/test.db +0 -0
- data/spec/mongoid_spec_helper.rb +7 -0
- data/spec/say_when/cron_expression_spec.rb +72 -0
- data/spec/say_when/scheduler_spec.rb +76 -0
- data/spec/say_when/storage/active_record/job_spec.rb +84 -0
- data/spec/say_when/storage/memory/job_spec.rb +31 -0
- data/spec/say_when/storage/memory/trigger_spec.rb +54 -0
- data/spec/say_when/storage/mongoid/trigger_spec.rb +57 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +46 -0
- data/spec/support/models.rb +31 -0
- metadata +224 -0
@@ -0,0 +1,621 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
module SayWhen
|
4
|
+
|
5
|
+
# Based on the extended cron capabilties
|
6
|
+
# http://wiki.opensymphony.com/display/QRTZ1/CronTriggers+Tutorial
|
7
|
+
class CronExpression
|
8
|
+
attr_reader :expression
|
9
|
+
attr_accessor :time_zone, :seconds, :minutes, :hours, :days_of_month, :months, :days_of_week, :years
|
10
|
+
|
11
|
+
def initialize(expression, time_zone=nil)
|
12
|
+
if expression.is_a?(Hash)
|
13
|
+
opts = expression
|
14
|
+
|
15
|
+
@expression = if opts[:expression]
|
16
|
+
opts[:expression]
|
17
|
+
else
|
18
|
+
[:days_of_month, :days_of_week].each do |f|
|
19
|
+
opts[f] ||= '?'
|
20
|
+
end
|
21
|
+
|
22
|
+
[:seconds, :minutes, :hours, :days_of_month, :months, :days_of_week, :years].each do |f|
|
23
|
+
opts[f] ||= '*'
|
24
|
+
end
|
25
|
+
|
26
|
+
"#{opts[:seconds]} #{opts[:minutes]} #{opts[:hours]} #{opts[:days_of_month]} #{opts[:months]} #{opts[:days_of_week]} #{opts[:years]}"
|
27
|
+
end
|
28
|
+
|
29
|
+
@time_zone = if opts.has_key?(:time_zone) && !opts[:time_zone].blank?
|
30
|
+
opts[:time_zone]
|
31
|
+
else
|
32
|
+
Time.zone.nil? ? "UTC" : Time.zone.name
|
33
|
+
end
|
34
|
+
|
35
|
+
else
|
36
|
+
@expression = expression
|
37
|
+
@time_zone = if time_zone.blank?
|
38
|
+
Time.zone.nil? ? "UTC" : Time.zone.name
|
39
|
+
else
|
40
|
+
time_zone
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
parse
|
45
|
+
validate
|
46
|
+
end
|
47
|
+
|
48
|
+
def parse
|
49
|
+
return if @expression.blank?
|
50
|
+
vals = @expression.split.collect{|word| word.upcase.gsub(/\s/, '')}
|
51
|
+
@seconds = SecondsCronValue.new(vals[0])
|
52
|
+
@minutes = MinutesCronValue.new(vals[1])
|
53
|
+
@hours = HoursCronValue.new(vals[2])
|
54
|
+
@days_of_month = DaysOfMonthCronValue.new(vals[3])
|
55
|
+
@months = MonthsCronValue.new(vals[4])
|
56
|
+
@days_of_week = DaysOfWeekCronValue.new(vals[5])
|
57
|
+
@years = YearsCronValue.new(vals[6] || "*")
|
58
|
+
end
|
59
|
+
|
60
|
+
def validate
|
61
|
+
return if @expression.blank?
|
62
|
+
raise "days_of_week or days_of_month needs to be ?" if (@days_of_month.is_specified && @days_of_week.is_specified)
|
63
|
+
end
|
64
|
+
|
65
|
+
def to_s
|
66
|
+
"s:#{seconds}m:#{minutes}h:#{hours}dom:#{days_of_month}m:#{months}dow:#{days_of_week}y:#{years}"
|
67
|
+
end
|
68
|
+
|
69
|
+
def will_fire_on?(date)
|
70
|
+
# puts "will fire on? #{date} : #{self.to_s}"
|
71
|
+
[@seconds, @minutes, @hours, @days_of_month, @months, @days_of_week, @years].detect{|part| !part.include?(date)}.nil?
|
72
|
+
end
|
73
|
+
|
74
|
+
def next_fire_at(time=nil)
|
75
|
+
Time.zone = @time_zone
|
76
|
+
after = time.nil? ? Time.zone.now : time.in_time_zone(@time_zone)
|
77
|
+
# after = 1.second.since(after)
|
78
|
+
# puts "next fire at after: #{after.inspect}"
|
79
|
+
|
80
|
+
while (true)
|
81
|
+
[years, months, days_of_month, days_of_week, hours, minutes, seconds].each do |cron_value|
|
82
|
+
# puts "next_fire_at cron val loop: #{cron_value.part}"
|
83
|
+
# puts "before move_to_next: #{after.inspect}"
|
84
|
+
after, changed = move_to_next(cron_value, after)
|
85
|
+
# puts "after move_to_next: #{after.inspect}"
|
86
|
+
return if after.nil?
|
87
|
+
break if changed
|
88
|
+
end
|
89
|
+
|
90
|
+
break if will_fire_on?(after)
|
91
|
+
end
|
92
|
+
# puts "NEXT FIRE AT: #{after}, NOW: #{Time.zone.now}"
|
93
|
+
return after
|
94
|
+
end
|
95
|
+
|
96
|
+
def last_fire_at(time=nil)
|
97
|
+
Time.zone = @time_zone
|
98
|
+
before = time.nil? ? Time.zone.now : time.in_time_zone(@time_zone)
|
99
|
+
# before = 1.second.ago(before)
|
100
|
+
# puts "last fire at before: #{before.inspect}"
|
101
|
+
|
102
|
+
while (true)
|
103
|
+
[years, months, days_of_month, days_of_week, hours, minutes, seconds].each do |cron_value|
|
104
|
+
# puts "last_fire_at cron val loop: #{cron_value.part} for #{before.inspect}"
|
105
|
+
# puts "before move_to_last: #{before.to_s}"
|
106
|
+
before, changed = move_to_last(cron_value, before)
|
107
|
+
# puts "after move_to_last: #{before.to_s}"
|
108
|
+
return if before.nil?
|
109
|
+
break if changed
|
110
|
+
end
|
111
|
+
|
112
|
+
break if will_fire_on?(before)
|
113
|
+
end
|
114
|
+
# puts "NEXT FIRE AT: #{before}, NOW: #{Time.zone.now}"
|
115
|
+
return before
|
116
|
+
end
|
117
|
+
|
118
|
+
protected
|
119
|
+
|
120
|
+
def move_to_next(cron_value, after)
|
121
|
+
unless cron_value.include?(after)
|
122
|
+
after = cron_value.next(after)
|
123
|
+
[after, true]
|
124
|
+
end
|
125
|
+
[after, false]
|
126
|
+
end
|
127
|
+
|
128
|
+
def move_to_last(cron_value, before)
|
129
|
+
unless cron_value.include?(before)
|
130
|
+
before = cron_value.last(before)
|
131
|
+
[before, true]
|
132
|
+
end
|
133
|
+
[before, false]
|
134
|
+
end
|
135
|
+
|
136
|
+
|
137
|
+
end
|
138
|
+
|
139
|
+
class CronValue
|
140
|
+
attr_accessor :part, :min, :max, :expression, :values
|
141
|
+
|
142
|
+
def initialize(p, min, max, exp)
|
143
|
+
self.part = p
|
144
|
+
self.min = min
|
145
|
+
self.max = max
|
146
|
+
self.values = []
|
147
|
+
self.expression = exp
|
148
|
+
parse(exp)
|
149
|
+
end
|
150
|
+
|
151
|
+
def parse(exp)
|
152
|
+
self.values = self.class.parse_number(self.min, self.max, exp.upcase)
|
153
|
+
end
|
154
|
+
|
155
|
+
def to_s
|
156
|
+
"[e:#{self.expression}, v:#{self.values.inspect}]\n"
|
157
|
+
end
|
158
|
+
|
159
|
+
def include?(date)
|
160
|
+
self.values.include?(date.send(part))
|
161
|
+
end
|
162
|
+
|
163
|
+
#works for secs, mins, hours
|
164
|
+
def self.parse_number(min, max, val)
|
165
|
+
values = []
|
166
|
+
case val
|
167
|
+
#check for a '/' for increments
|
168
|
+
when /(\w+)\/(\d+)/ then (( $1 == "*") ? min : $1.to_i).step(max, $2.to_i) {|x| values << x}
|
169
|
+
|
170
|
+
#check for ',' for list of values
|
171
|
+
when /(\d+)(,\d+)+/ then values = val.split(',').collect{|v| v.to_i}.sort
|
172
|
+
|
173
|
+
#check for '-' for range of values
|
174
|
+
when /(\d+)-(\d+)/ then values = (($1.to_i)..($2.to_i)).to_a
|
175
|
+
|
176
|
+
#check for '*' for all values between min and max
|
177
|
+
when /^(\*)$/ then values = (min..max).to_a
|
178
|
+
|
179
|
+
#lastly, should just be a number
|
180
|
+
when /^(\d+)$/ then values << $1.to_i
|
181
|
+
|
182
|
+
#if nothing else, leave values as []
|
183
|
+
else values = []
|
184
|
+
end
|
185
|
+
values
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
class SecondsCronValue < CronValue
|
190
|
+
def initialize(exp)
|
191
|
+
super(:sec, 0, 59, exp)
|
192
|
+
end
|
193
|
+
|
194
|
+
def next(date)
|
195
|
+
# date = date.to_time
|
196
|
+
n = self.values.detect{|v| v > date.sec}
|
197
|
+
if n.blank?
|
198
|
+
1.minute.since(date).change(:sec=>self.values.first)
|
199
|
+
else
|
200
|
+
date.change(:sec=>n)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def last(date)
|
205
|
+
# date = date.to_time
|
206
|
+
n = self.values.reverse.detect{|v| v < date.sec}
|
207
|
+
if n.blank?
|
208
|
+
1.minute.ago(date).change(:sec=>self.values.last)
|
209
|
+
else
|
210
|
+
date.change(:sec=>n)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
|
215
|
+
end
|
216
|
+
|
217
|
+
class MinutesCronValue < CronValue
|
218
|
+
def initialize(exp)
|
219
|
+
super(:min, 0, 59, exp)
|
220
|
+
end
|
221
|
+
|
222
|
+
def next(date)
|
223
|
+
# date = date.to_time
|
224
|
+
n = self.values.detect{|v| v > date.min}
|
225
|
+
if n.blank?
|
226
|
+
1.hour.since(date).change(:min=>self.values.first, :sec=>0)
|
227
|
+
else
|
228
|
+
date.change(:min=>n, :sec=>0)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def last(date)
|
233
|
+
# date = date.to_time
|
234
|
+
n = self.values.reverse.detect{|v| v < date.min}
|
235
|
+
if n.blank?
|
236
|
+
1.hour.ago(date).change(:min=>self.values.last, :sec=>59)
|
237
|
+
else
|
238
|
+
date.change(:min=>n, :sec=>59)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
class HoursCronValue < CronValue
|
244
|
+
def initialize(exp)
|
245
|
+
super(:hour, 0, 24, exp)
|
246
|
+
end
|
247
|
+
|
248
|
+
def next(date)
|
249
|
+
# date = date.to_time
|
250
|
+
# puts "HoursCronValue next: date: #{date.inspect}, hour: #{date.hour}"
|
251
|
+
n = self.values.detect{|v| v > date.hour}
|
252
|
+
if n.blank?
|
253
|
+
1.day.since(date).change(:hour=>self.values.first, :min=>0, :sec=>0)
|
254
|
+
else
|
255
|
+
date.change(:hour=>n, :min=>0, :sec=>0)
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
def last(date)
|
260
|
+
# date = date.to_time
|
261
|
+
n = self.values.reverse.detect{|v| v < date.hour}
|
262
|
+
if n.blank?
|
263
|
+
1.day.ago(date).change(:hour=>self.values.last, :min=>59, :sec=>59)
|
264
|
+
else
|
265
|
+
date.change(:hour=>n, :min=>59, :sec=>59)
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
end
|
270
|
+
|
271
|
+
class DaysOfMonthCronValue < CronValue
|
272
|
+
attr_accessor :is_specified, :is_last, :is_weekday
|
273
|
+
def initialize(exp)
|
274
|
+
self.is_last = false
|
275
|
+
self.is_weekday = false
|
276
|
+
super(:mday, 1, 31, exp)
|
277
|
+
end
|
278
|
+
|
279
|
+
def parse(exp)
|
280
|
+
if self.is_specified = !(self.expression =~ /\?/)
|
281
|
+
case exp
|
282
|
+
when /^(L)$/ then self.is_last = true
|
283
|
+
when /^(W)$/ then self.is_weekday = true
|
284
|
+
when /^(WL|LW)$/ then self.is_last = (self.is_weekday = true)
|
285
|
+
when /^(\d+)W$/ then self.is_weekday = true; self.values << $1.to_i
|
286
|
+
else super(exp)
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
def last(date)
|
292
|
+
result = if !is_specified
|
293
|
+
date
|
294
|
+
elsif is_last
|
295
|
+
eom = date.end_of_month
|
296
|
+
eom = nearest_week_day(eom) if is_weekday
|
297
|
+
if eom > date
|
298
|
+
eom = 1.month.ago(date)
|
299
|
+
eom = nearest_week_day(eom.change(:day=>eom.end_of_month))
|
300
|
+
end
|
301
|
+
eom
|
302
|
+
elsif is_weekday
|
303
|
+
if values.empty?
|
304
|
+
nearest = nearest_week_day(date)
|
305
|
+
if nearest > date
|
306
|
+
nearest = 1.month.ago(date)
|
307
|
+
nearest = nearest_week_day(date.change(:day=>date.end_of_month))
|
308
|
+
end
|
309
|
+
else
|
310
|
+
nearest = nearest_week_day(date.change(:day=>values.first))
|
311
|
+
nearest = nearest_week_day(1.month.ago(date).change(:day=>values.first)) if nearest > date
|
312
|
+
end
|
313
|
+
nearest
|
314
|
+
else
|
315
|
+
# puts "change to the next specified day of the month...#{self.values.inspect} "
|
316
|
+
l = self.values.reverse.detect{|v| v < date.mday}
|
317
|
+
# puts "last should be #{n}"
|
318
|
+
if l.blank?
|
319
|
+
1.month.ago(date).change(:day=>self.values.last)
|
320
|
+
else
|
321
|
+
# puts "change date to have day of month of #{n}"
|
322
|
+
date.change(:day=>l.to_i)
|
323
|
+
end
|
324
|
+
end
|
325
|
+
result = result.change(:hour=>23, :min=>59, :sec=>59)
|
326
|
+
# puts "result after change #{result}"
|
327
|
+
result
|
328
|
+
end
|
329
|
+
|
330
|
+
def next(date)
|
331
|
+
result = if !is_specified
|
332
|
+
date
|
333
|
+
elsif is_last
|
334
|
+
last = date.end_of_month
|
335
|
+
last = nearest_week_day(last) if is_weekday
|
336
|
+
# last = nearest_week_day(1.month.since(date).change(:day=>1)) if last < date
|
337
|
+
if last < date
|
338
|
+
last = 1.month.since(date)
|
339
|
+
last = nearest_week_day(last.change(:day=>last.end_of_month))
|
340
|
+
end
|
341
|
+
last
|
342
|
+
elsif is_weekday
|
343
|
+
if values.empty?
|
344
|
+
nearest = nearest_week_day(date)
|
345
|
+
nearest = nearest_week_day(1.month.since(date).change(:day=>1)) if nearest < date
|
346
|
+
else
|
347
|
+
nearest = nearest_week_day(date.change(:day=>values.first))
|
348
|
+
nearest = nearest_week_day(1.month.since(date).change(:day=>values.first)) if nearest < date
|
349
|
+
end
|
350
|
+
nearest
|
351
|
+
else
|
352
|
+
# puts "change to the next specified day of the month...#{self.values.inspect} "
|
353
|
+
n = self.values.detect{|v| v > date.mday}
|
354
|
+
# puts "next should be #{n}"
|
355
|
+
if n.blank?
|
356
|
+
date.months_since(1).change(:day=>self.values.first)
|
357
|
+
else
|
358
|
+
# puts "change date to have day of month of #{n}"
|
359
|
+
ndate = date.change(:day=>n.to_i)
|
360
|
+
# puts "after change date #{ndate}, #{ndate.class.name}"
|
361
|
+
ndate
|
362
|
+
end
|
363
|
+
end
|
364
|
+
result = result.change(:hour=>0)
|
365
|
+
# puts "result after change #{result}"
|
366
|
+
result
|
367
|
+
end
|
368
|
+
|
369
|
+
def include?(date)
|
370
|
+
return true unless is_specified
|
371
|
+
last = date.clone
|
372
|
+
#must be last weekday of the month
|
373
|
+
if is_last
|
374
|
+
last = last.end_of_month.to_date
|
375
|
+
last = nearest_week_day(last) if is_weekday
|
376
|
+
last == date.to_date
|
377
|
+
elsif is_weekday
|
378
|
+
if values.empty?
|
379
|
+
(1..5).include?(date.wday)
|
380
|
+
else
|
381
|
+
nearest_week_day(date.change(:day=>values.first)) == date
|
382
|
+
end
|
383
|
+
else
|
384
|
+
super(date)
|
385
|
+
end
|
386
|
+
end
|
387
|
+
|
388
|
+
def nearest_week_day(date)
|
389
|
+
if (1..5).include?(date.wday)
|
390
|
+
date
|
391
|
+
elsif date.wday == 6
|
392
|
+
(date.beginning_of_month.to_date == date.to_date) ? 2.days.since(date) : 1.day.ago(date)
|
393
|
+
elsif date.wday == 0
|
394
|
+
(date.end_of_month.to_date == date.to_date) ? date = 2.days.ago(date) : 1.day.since(date)
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
def to_s
|
399
|
+
"[e:#{self.expression}, v:#{self.values.inspect}, is:#{is_specified}, il:#{is_last}, iw:#{is_weekday}]\n"
|
400
|
+
end
|
401
|
+
end
|
402
|
+
|
403
|
+
class MonthsCronValue < CronValue
|
404
|
+
MONTHS = Date::ABBR_MONTHNAMES[1..-1].collect{|a| a.upcase }
|
405
|
+
|
406
|
+
def initialize(exp)
|
407
|
+
super(:month, 1, 12, exp)
|
408
|
+
end
|
409
|
+
|
410
|
+
def parse(exp)
|
411
|
+
if exp =~ /[A-Z]+/
|
412
|
+
MONTHS.each_with_index{|mon, index|
|
413
|
+
exp = exp.gsub(mon, (index+1).to_s)
|
414
|
+
}
|
415
|
+
end
|
416
|
+
super(exp)
|
417
|
+
end
|
418
|
+
|
419
|
+
def last(date)
|
420
|
+
last_month = self.values.reverse.detect{|v| v < date.month}
|
421
|
+
result = if last_month.nil?
|
422
|
+
date.change(:year=>date.year - 1, :month=>self.values.last)
|
423
|
+
else
|
424
|
+
date.change(:month=>last_month)
|
425
|
+
end
|
426
|
+
result.change(:day=>result.end_of_month, :hour=>23, :min=>59, :sec=>59)
|
427
|
+
end
|
428
|
+
|
429
|
+
def next(date)
|
430
|
+
next_month = self.values.detect{|v| v > date.month}
|
431
|
+
result = if next_month.nil?
|
432
|
+
date.change(:year=>date.year + 1, :month=>self.values.first, :day=>1, :hour=>0)
|
433
|
+
else
|
434
|
+
date.change(:month=>next_month, :day=>1, :hour=>0)
|
435
|
+
end
|
436
|
+
result
|
437
|
+
end
|
438
|
+
|
439
|
+
end
|
440
|
+
|
441
|
+
class DaysOfWeekCronValue < CronValue
|
442
|
+
DAYS = Date::ABBR_DAYNAMES.collect{|a| a.upcase }
|
443
|
+
attr_accessor :is_specified, :is_last, :nth_day
|
444
|
+
|
445
|
+
def initialize(exp)
|
446
|
+
self.is_last = false
|
447
|
+
super(:wday, 1, 7, exp)
|
448
|
+
end
|
449
|
+
|
450
|
+
def parse(exp)
|
451
|
+
if self.is_specified = !(self.expression =~ /\?/)
|
452
|
+
if exp =~ /[A-Z]+/
|
453
|
+
DAYS.each_with_index{|day, index|
|
454
|
+
exp = exp.gsub(day, (index+1).to_s)
|
455
|
+
}
|
456
|
+
end
|
457
|
+
case exp
|
458
|
+
when /^L$/ then values << self.max
|
459
|
+
when /^(\d+)L$/ then self.is_last = true; values << $1.to_i
|
460
|
+
when /^(\d+)#(\d+)/ then self.values << $1.to_i; self.nth_day = $2.to_i
|
461
|
+
else super(exp)
|
462
|
+
end
|
463
|
+
end
|
464
|
+
end
|
465
|
+
|
466
|
+
def include?(date)
|
467
|
+
# puts "DaysOfWeekCronValue::include? is_specified:#{is_specified}, date:#{date}"
|
468
|
+
return true unless is_specified
|
469
|
+
if is_last
|
470
|
+
last = last_wday(date, values.first).to_date
|
471
|
+
# puts "checking is_last: date=#{date} == last #{last}"
|
472
|
+
date.to_date == last
|
473
|
+
elsif nth_day
|
474
|
+
date.to_date == nth_wday(self.nth_day, self.values.first, date.month, date.year).to_date
|
475
|
+
else
|
476
|
+
self.values.include?(date.wday+1)
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
def last(date)
|
481
|
+
# puts "DaysOfWeekCronValue::last date:#{date}, is_last:#{is_last}"
|
482
|
+
last_dow = if !is_specified
|
483
|
+
date
|
484
|
+
elsif is_last
|
485
|
+
last = last_wday(date, values.first)
|
486
|
+
# puts "DaysOfWeekCronValue::last after first last_wday: #{date}"
|
487
|
+
if last.to_date > date.to_date
|
488
|
+
last = last_wday(1.month.ago(date).change(:day=>1), values.first)
|
489
|
+
end
|
490
|
+
last
|
491
|
+
elsif nth_day
|
492
|
+
nth = nth_wday(self.nth_day, self.values.first, date.month, date.year)
|
493
|
+
# puts "DaysOfWeekCronValue::last after first nth_wday: #{nth}"
|
494
|
+
if nth.to_date > date.to_date
|
495
|
+
nth = 1.month.ago(date).change(:day=>1)
|
496
|
+
nth = nth_wday(self.nth_day, self.values.first, nth.month, nth.year)
|
497
|
+
end
|
498
|
+
nth
|
499
|
+
else
|
500
|
+
n = self.values.detect{|v| v > date.wday}
|
501
|
+
n = self.values[0] if n.blank?
|
502
|
+
base = (n < (date.wday + 1)) ? 7 : 0
|
503
|
+
days_forward = n + (base - (date.wday + 1))
|
504
|
+
days_forward.days.since(date)
|
505
|
+
end
|
506
|
+
last_dow = last_dow.change(:hour=>23, :min=>59, :sec=>59)
|
507
|
+
# puts "DaysOfWeekCronValue: #{last_dow.class.name}: #{last_dow.to_s}"
|
508
|
+
last_dow
|
509
|
+
end
|
510
|
+
|
511
|
+
def next(date)
|
512
|
+
next_dow = if !is_specified
|
513
|
+
date
|
514
|
+
elsif is_last
|
515
|
+
last = last_wday(date, values.first)
|
516
|
+
# puts "1 last_wday = #{last}"
|
517
|
+
if last.to_date <= date.to_date
|
518
|
+
last = last_wday(1.month.since(date).change(:day=>1), values.first)
|
519
|
+
# puts "2 last_wday = #{last}"
|
520
|
+
end
|
521
|
+
last
|
522
|
+
elsif nth_day
|
523
|
+
nth = nth_wday(self.nth_day, self.values.first, date.month, date.year)
|
524
|
+
if nth.to_date <= date.to_date
|
525
|
+
date = 1.month.since(date)
|
526
|
+
nth = nth_wday(self.nth_day, self.values.first, date.month, date.year)
|
527
|
+
end
|
528
|
+
nth
|
529
|
+
else
|
530
|
+
n = self.values.detect{|v| v > date.wday}
|
531
|
+
n = self.values[0] if n.blank?
|
532
|
+
base = (n < (date.wday + 1)) ? 7 : 0
|
533
|
+
days_forward = n + (base - (date.wday + 1))
|
534
|
+
days_forward.days.since(date)
|
535
|
+
end
|
536
|
+
next_dow.change(:hour=>0)
|
537
|
+
end
|
538
|
+
|
539
|
+
# 1 last_wday = 2008-05-30
|
540
|
+
# latest_wday date=Sun Jun 01 10:00:00 -0400 2008, wday=5
|
541
|
+
# 2 last_wday = 2008-06-28 '
|
542
|
+
# **** should be 06-27
|
543
|
+
def last_wday(date, aWday)
|
544
|
+
# puts "last_wday date=#{date.to_time}, wday=#{wday}"
|
545
|
+
# sleep(1)
|
546
|
+
wday = aWday - 1
|
547
|
+
eom = date.end_of_month
|
548
|
+
if eom.wday == wday
|
549
|
+
eom
|
550
|
+
elsif eom.wday > wday
|
551
|
+
(eom.wday - wday).days.ago(eom)
|
552
|
+
else
|
553
|
+
((7 - wday) + eom.wday).days.ago(eom)
|
554
|
+
end
|
555
|
+
end
|
556
|
+
|
557
|
+
# compliments of the ruby way
|
558
|
+
def nth_wday(n, aWday, month, year)
|
559
|
+
wday = aWday - 1
|
560
|
+
if (!n.between? 1,5) or
|
561
|
+
(!wday.between? 0,6) or
|
562
|
+
(!month.between? 1,12)
|
563
|
+
raise ArgumentError
|
564
|
+
end
|
565
|
+
t = Time.zone.local year, month, 1
|
566
|
+
# puts "t = #{t}"
|
567
|
+
first = t.wday
|
568
|
+
if first == wday
|
569
|
+
fwd = 1
|
570
|
+
elsif first < wday
|
571
|
+
fwd = wday - first + 1
|
572
|
+
elsif first > wday
|
573
|
+
fwd = (wday+7) - first + 1
|
574
|
+
end
|
575
|
+
target = fwd + (n-1)*7
|
576
|
+
begin
|
577
|
+
t2 = Time.zone.local year, month, target
|
578
|
+
# puts "t2 = #{t2}"
|
579
|
+
rescue ArgumentError
|
580
|
+
return nil
|
581
|
+
end
|
582
|
+
if t2.mday == target
|
583
|
+
t2
|
584
|
+
else
|
585
|
+
nil
|
586
|
+
end
|
587
|
+
end
|
588
|
+
|
589
|
+
|
590
|
+
def to_s
|
591
|
+
"[e:#{self.expression}, v:#{self.values.inspect}, is:#{is_specified}, il:#{is_last}, nd:#{nth_day}]\n"
|
592
|
+
end
|
593
|
+
|
594
|
+
end
|
595
|
+
|
596
|
+
class YearsCronValue < CronValue
|
597
|
+
def initialize(exp)
|
598
|
+
super(:year, 1970, 2099, exp)
|
599
|
+
end
|
600
|
+
|
601
|
+
def next(date)
|
602
|
+
next_year = self.values.detect{|v| v > date.year}
|
603
|
+
if next_year.nil?
|
604
|
+
return nil
|
605
|
+
else
|
606
|
+
date.change(:year=>next_year, :month=>1, :day=>1, :hour=>0)
|
607
|
+
end
|
608
|
+
end
|
609
|
+
|
610
|
+
def last(date)
|
611
|
+
last_year = self.values.reverse.detect{|v| v < date.year}
|
612
|
+
if last_year.nil?
|
613
|
+
return nil
|
614
|
+
else
|
615
|
+
date.change(:year=>last_year, :month=>12, :day=>31, :hour=>23, :min=>59, :sec=>59)
|
616
|
+
end
|
617
|
+
end
|
618
|
+
|
619
|
+
end
|
620
|
+
|
621
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'activemessaging/message_sender'
|
2
|
+
|
3
|
+
module SayWhen
|
4
|
+
module Processor
|
5
|
+
|
6
|
+
class ActiveMessaging < SayWhen::Processor::Base
|
7
|
+
|
8
|
+
include ::ActiveMessaging::MessageSender
|
9
|
+
|
10
|
+
def initialize(scheduler)
|
11
|
+
super(scheduler)
|
12
|
+
end
|
13
|
+
|
14
|
+
# send the job to the other end, then in the a13g processor, call the execute method
|
15
|
+
def process(job)
|
16
|
+
message = {:job_id=>job.id}.to_yaml
|
17
|
+
publish :say_when, message
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module SayWhen
|
2
|
+
module Processor
|
3
|
+
|
4
|
+
class Base
|
5
|
+
attr_accessor :scheduler
|
6
|
+
|
7
|
+
def initialize(scheduler)
|
8
|
+
@scheduler = scheduler
|
9
|
+
end
|
10
|
+
|
11
|
+
def process(job)
|
12
|
+
raise NotImplementedError.new('You need to implement process(job)')
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|