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.
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