say_when 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/.gitignore +5 -0
  2. data/Gemfile +4 -0
  3. data/Rakefile +1 -0
  4. data/generators/say_when_migration/say_when_migration_generator.rb +11 -0
  5. data/generators/say_when_migration/templates/migration.rb +48 -0
  6. data/lib/generators/.DS_Store +0 -0
  7. data/lib/generators/say_when/migration/migration_generator.rb +13 -0
  8. data/lib/generators/say_when/migration/templates/migration.rb +47 -0
  9. data/lib/say_when/base_job.rb +96 -0
  10. data/lib/say_when/cron_expression.rb +621 -0
  11. data/lib/say_when/processor/active_messaging.rb +22 -0
  12. data/lib/say_when/processor/base.rb +17 -0
  13. data/lib/say_when/processor/simple.rb +15 -0
  14. data/lib/say_when/scheduler.rb +129 -0
  15. data/lib/say_when/storage/active_record/acts.rb +85 -0
  16. data/lib/say_when/storage/active_record/job.rb +85 -0
  17. data/lib/say_when/storage/active_record/job_execution.rb +17 -0
  18. data/lib/say_when/storage/memory/base.rb +34 -0
  19. data/lib/say_when/storage/memory/job.rb +48 -0
  20. data/lib/say_when/storage/mongoid/job.rb +15 -0
  21. data/lib/say_when/tasks.rb +22 -0
  22. data/lib/say_when/triggers/base.rb +11 -0
  23. data/lib/say_when/triggers/cron_strategy.rb +22 -0
  24. data/lib/say_when/triggers/once_strategy.rb +30 -0
  25. data/lib/say_when/version.rb +3 -0
  26. data/lib/say_when.rb +28 -0
  27. data/lib/tasks/say_when.rake +2 -0
  28. data/say_when.gemspec +26 -0
  29. data/spec/active_record_spec_helper.rb +11 -0
  30. data/spec/db/schema.rb +36 -0
  31. data/spec/db/test.db +0 -0
  32. data/spec/mongoid_spec_helper.rb +7 -0
  33. data/spec/say_when/cron_expression_spec.rb +72 -0
  34. data/spec/say_when/scheduler_spec.rb +76 -0
  35. data/spec/say_when/storage/active_record/job_spec.rb +84 -0
  36. data/spec/say_when/storage/memory/job_spec.rb +31 -0
  37. data/spec/say_when/storage/memory/trigger_spec.rb +54 -0
  38. data/spec/say_when/storage/mongoid/trigger_spec.rb +57 -0
  39. data/spec/spec.opts +4 -0
  40. data/spec/spec_helper.rb +46 -0
  41. data/spec/support/models.rb +31 -0
  42. 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
@@ -0,0 +1,15 @@
1
+ module SayWhen
2
+ module Processor
3
+
4
+ class Simple < SayWhen::Processor::Base
5
+ def initialize(scheduler)
6
+ super(scheduler)
7
+ end
8
+
9
+ def process(job)
10
+ job.execute
11
+ end
12
+ end
13
+
14
+ end
15
+ end