symphony-metronome 0.2.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,599 +0,0 @@
1
- # vim: set noet nosta sw=4 ts=4 ft=ragel :
2
-
3
- %%{
4
- #
5
- # Generate the actual code like so:
6
- # ragel -R -T1 -Ls inputfile.rl
7
- #
8
-
9
- machine interval_expression;
10
-
11
- ########################################################################
12
- ### A C T I O N S
13
- ########################################################################
14
-
15
- action set_mark { mark = p }
16
-
17
- action set_valid { event.instance_variable_set( :@valid, true ) }
18
- action set_invalid { event.instance_variable_set( :@valid, false ) }
19
- action recurring { event.instance_variable_set( :@recurring, true ) }
20
-
21
- action start_time {
22
- time = event.send( :extract, mark, p - mark )
23
- event.send( :set_starting, time, :time )
24
- }
25
-
26
- action start_interval {
27
- interval = event.send( :extract, mark, p - mark )
28
- event.send( :set_starting, interval, :interval )
29
- }
30
-
31
- action execute_time {
32
- time = event.send( :extract, mark, p - mark )
33
- event.send( :set_interval, time, :time )
34
- }
35
-
36
- action execute_interval {
37
- interval = event.send( :extract, mark, p - mark )
38
- event.send( :set_interval, interval, :interval )
39
- }
40
-
41
- action execute_multiplier {
42
- multiplier = event.send( :extract, mark, p - mark ).sub( / times/, '' )
43
- event.instance_variable_set( :@multiplier, multiplier.to_i )
44
- }
45
-
46
- action ending_time {
47
- time = event.send( :extract, mark, p - mark )
48
- event.send( :set_ending, time, :time )
49
- }
50
-
51
- action ending_interval {
52
- interval = event.send( :extract, mark, p - mark )
53
- event.send( :set_ending, interval, :interval )
54
- }
55
-
56
-
57
- ########################################################################
58
- ### P R E P O S I T I O N S
59
- ########################################################################
60
-
61
- recur_preposition = ( 'every' | 'each' | 'per' | 'once' ' per'? ) @recurring;
62
- time_preposition = 'at' | 'on';
63
- interval_preposition = 'in';
64
-
65
-
66
- ########################################################################
67
- ### K E Y W O R D S
68
- ########################################################################
69
-
70
- interval_times =
71
- ( 'milli'? 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year' ) 's'?;
72
-
73
- start_identifiers = ( 'start' | 'begin' 'n'? ) 'ing'?;
74
- exec_identifiers = ('run' | 'exec' 'ute'? | 'do' );
75
- ending_identifiers = ( ('for' | 'until' | 'during') | ('end'|'finish'|'stop'|'complet' 'e'?) 'ing'? );
76
-
77
-
78
- ########################################################################
79
- ### T I M E S P E C S
80
- ########################################################################
81
-
82
- # 1st
83
- # 202nd
84
- # 2015th
85
- # ...
86
- #
87
- ordinals = (
88
- ( (digit+ - '1')? '1' 'st' ) |
89
- ( digit+?
90
- ( '1' digit 'th' ) | # all '11s'
91
- ( '2' 'nd' ) |
92
- ( '3' 'rd' ) |
93
- ( [0456789] 'th' )
94
- )
95
- );
96
-
97
- # 2014-05-01
98
- # 2014-05-01 15:00
99
- # 2014-05-01 15:00:30
100
- #
101
- fulldate = digit{4} '-' digit{2} '-' digit{2}
102
- ( space digit{2} ':' digit{2} ( ':' digit{2} )? )?;
103
-
104
- # 10am
105
- # 2:45pm
106
- #
107
- time = digit{1,2} ( ':' digit{2} )? ( 'am' | 'pm' );
108
-
109
- # union of the above
110
- date_or_time = fulldate | time;
111
-
112
- # 20 seconds
113
- # 5 hours
114
- # 1 hour
115
- # 2.5 hours
116
- # an hour
117
- # a minute
118
- # other minute
119
- #
120
- interval = (
121
- (( 'a' 'n'? | [1-9][0-9]* ( '.' [0-9]+ )? ) | 'other' | ordinals ) space
122
- )? interval_times;
123
-
124
-
125
- ########################################################################
126
- ### A C T I O N C H A I N S
127
- ########################################################################
128
-
129
- start_time = date_or_time >set_mark %start_time;
130
- start_interval = interval >set_mark %start_interval;
131
-
132
- start_expression = ( (time_preposition space)? start_time ) |
133
- ( (interval_preposition space)? start_interval );
134
-
135
- execute_time = date_or_time >set_mark %/execute_time;
136
- execute_interval = interval >set_mark %execute_interval;
137
- execute_multiplier = ( digit+ space 'times' )
138
- >set_mark %execute_multiplier @recurring;
139
-
140
- execute_expression = (
141
- # regular dates and intervals
142
- ( time_preposition space execute_time ) |
143
- ( ( interval_preposition | recur_preposition ) space execute_interval )
144
- ) | (
145
- # count + interval (10 times every minute)
146
- execute_multiplier space ( recur_preposition space )? execute_interval
147
- ) |
148
- # count for 'timeboxed' intervals
149
- execute_multiplier;
150
-
151
-
152
- ending_time = date_or_time >set_mark %ending_time;
153
- ending_interval = interval >set_mark %ending_interval;
154
-
155
- ending_expression = ( (time_preposition space)? ending_time ) |
156
- ( (interval_preposition space)? ending_interval );
157
-
158
-
159
- ########################################################################
160
- ### M A C H I N E S
161
- ########################################################################
162
-
163
- Start = (
164
- start: start_identifiers space -> StartTime,
165
- StartTime: start_expression -> final
166
- );
167
-
168
- Interval = (
169
- start:
170
- Decorators: ( exec_identifiers space )? -> ExecuteTime,
171
- ExecuteTime: execute_expression -> final
172
- );
173
-
174
- Ending = (
175
- start: space ending_identifiers space -> EndingTime,
176
- EndingTime: ending_expression -> final
177
- );
178
-
179
-
180
- main := (
181
- ( Start space Interval Ending? ) |
182
- ( Interval ( space Start )? Ending? ) |
183
- ( Interval Ending space Start )
184
- ) %set_valid @!set_invalid;
185
- }%%
186
-
187
-
188
- require 'symphony' unless defined?( Symphony )
189
- require 'symphony/metronome'
190
- require 'symphony/metronome/mixins'
191
-
192
- using Symphony::Metronome::TimeRefinements
193
-
194
-
195
- ### Parse natural English expressions of times and intervals.
196
- ###
197
- ### in 30 minutes
198
- ### once an hour
199
- ### every 15 minutes for 2 days
200
- ### at 2014-05-01
201
- ### at 2014-04-01 14:00:25
202
- ### at 2pm
203
- ### starting at 2pm once a day
204
- ### start in 1 hour from now run every 5 seconds end at 11:15pm
205
- ### every other hour
206
- ### once a day ending in 1 week
207
- ### run once a minute for an hour starting in 6 days
208
- ### run each hour starting at 2010-01-05 09:00:00
209
- ### 10 times a minute for 2 days
210
- ### run 45 times every hour
211
- ### 30 times per day
212
- ### start at 2010-01-02 run 12 times and end on 2010-01-03
213
- ### starting in an hour from now run 6 times a minute for 2 hours
214
- ### beginning a day from now, run 30 times per minute and finish in 2 weeks
215
- ### execute 12 times during the next 2 minutes
216
- ###
217
- class Symphony::Metronome::IntervalExpression
218
- include Comparable,
219
- Symphony::Metronome::TimeFunctions
220
- extend Loggability
221
-
222
- log_to :symphony
223
-
224
- # Ragel accessors are injected as class methods/variables for some reason.
225
- %% write data;
226
-
227
- # Words/phrases in the expression that we'll strip/ignore before parsing.
228
- COMMON_DECORATORS = [ 'and', 'then', /\s+from now/, 'the next' ];
229
-
230
-
231
- ########################################################################
232
- ### C L A S S M E T H O D S
233
- ########################################################################
234
-
235
- ### Parse a schedule expression +exp+.
236
- ###
237
- ### Parsing defaults to Time.now(), but if passed a +time+ object,
238
- ### all contexual times (2pm) are relative to it. If you know when
239
- ### an expression was generated, you can 'reconstitute' an interval
240
- ### object this way.
241
- ###
242
- def self::parse( exp, time=nil )
243
-
244
- # Normalize the expression before parsing
245
- #
246
- exp = exp.downcase.
247
- gsub( /(?:[^[a-z][0-9][\.\-:]\s]+)/, '' ). # . : - a-z 0-9 only
248
- gsub( Regexp.union(COMMON_DECORATORS), '' ). # remove common decorator words
249
- gsub( /\s+/, ' ' ). # collapse whitespace
250
- gsub( /([:\-])+/, '\1' ). # collapse multiple - or : chars
251
- gsub( /\.+$/, '' ) # trailing periods
252
-
253
- event = new( exp, time || Time.now )
254
- data = event.instance_variable_get( :@data )
255
-
256
- # Ragel interface variables
257
- #
258
- key = ''
259
- mark = 0
260
- %% write init;
261
- eof = pe
262
- %% write exec;
263
-
264
- # Attach final time logic and sanity checks.
265
- event.send( :finalize )
266
-
267
- return event
268
- end
269
-
270
-
271
- ########################################################################
272
- ### I N S T A N C E M E T H O D S
273
- ########################################################################
274
-
275
- ### Instantiate a new TimeExpression, provided an +expression+ string
276
- ### that describes when this event will take place in natural english,
277
- ### and a +base+ Time to perform calculations against.
278
- ###
279
- private_class_method :new
280
- def initialize( expression, base ) # :nodoc:
281
- @exp = expression
282
- @data = expression.to_s.unpack( 'c*' )
283
- @base = base
284
-
285
- @valid = false
286
- @recurring = false
287
- @starting = nil
288
- @interval = nil
289
- @multiplier = nil
290
- @ending = nil
291
- end
292
-
293
-
294
- ######
295
- public
296
- ######
297
-
298
- # Is the schedule expression parsable?
299
- attr_reader :valid
300
-
301
- # Does this event repeat?
302
- attr_reader :recurring
303
-
304
- # The valid start time for the schedule (for recurring events)
305
- attr_reader :starting
306
-
307
- # The valid end time for the schedule (for recurring events)
308
- attr_reader :ending
309
-
310
- # The interval to wait before the event should be acted on.
311
- attr_reader :interval
312
-
313
- # An optional interval multipler for expressing counts.
314
- attr_reader :multiplier
315
-
316
-
317
- ### If this interval is on a stack somewhere and ready to
318
- ### fire, is it okay to do so based on the specified
319
- ### expression criteria?
320
- ###
321
- ### Returns +true+ if it should fire, +false+ if it should not
322
- ### but could at a later attempt, and +nil+ if the interval has
323
- ### expired.
324
- ###
325
- def fire?
326
- now = Time.now
327
-
328
- # Interval has expired.
329
- return nil if self.ending && now > self.ending
330
-
331
- # Interval is not yet in its current time window.
332
- return false if self.starting - now > 0
333
-
334
- # Looking good.
335
- return true
336
- end
337
-
338
-
339
- ### Just return the original event expression.
340
- ###
341
- def to_s
342
- return @exp
343
- end
344
-
345
-
346
- ### Inspection string.
347
- ###
348
- def inspect
349
- return ( "<%s:0x%08x valid:%s recur:%s expression:%p " +
350
- "starting:%p interval:%p ending:%p>" ) % [
351
- self.class.name,
352
- self.object_id * 2,
353
- self.valid,
354
- self.recurring,
355
- self.to_s,
356
- self.starting,
357
- self.interval,
358
- self.ending
359
- ]
360
- end
361
-
362
-
363
- ### Comparable interface, order by interval, 'soonest' first.
364
- ###
365
- def <=>( other )
366
- return self.interval <=> other.interval
367
- end
368
-
369
-
370
- #########
371
- protected
372
- #########
373
-
374
- ### Given a +start+ and +ending+ scanner position,
375
- ### return an ascii representation of the data slice.
376
- ###
377
- def extract( start, ending )
378
- slice = @data[ start, ending ]
379
- return '' unless slice
380
- return slice.pack( 'c*' )
381
- end
382
-
383
-
384
- ### Parse and set the starting attribute, given a +time_arg+
385
- ### string and the +type+ of string (interval or exact time)
386
- ###
387
- def set_starting( time_arg, type )
388
- @starting_args ||= []
389
- @starting_args << time_arg
390
-
391
- # If we already have seen a start time, it's possible the parser
392
- # was non-deterministic and this action has been executed multiple
393
- # times. Re-parse the complete date string, overwriting any previous.
394
- time_arg = @starting_args.join( ' ' )
395
-
396
- start = self.get_time( time_arg, type )
397
- @starting = start
398
-
399
- # If start time is expressed as a post-conditional (we've
400
- # already got an end time) we need to recalculate the end
401
- # as an offset from the start. The original parsed ending
402
- # arguments should have already been cached when it was
403
- # previously set.
404
- #
405
- if self.ending && self.recurring
406
- self.set_ending( *@ending_args )
407
- end
408
-
409
- return @starting
410
- end
411
-
412
-
413
- ### Parse and set the interval attribute, given a +time_arg+
414
- ### string and the +type+ of string (interval or exact time)
415
- ###
416
- ### Perform consistency and sanity checks before returning an
417
- ### integer representing the amount of time needed to sleep before
418
- ### firing the event.
419
- ###
420
- def set_interval( time_arg, type )
421
- interval = nil
422
- if self.starting && type == :time
423
- raise Symphony::Metronome::TimeParseError, "That doesn't make sense, just use 'at [datetime]' instead"
424
- else
425
- interval = self.get_time( time_arg, type )
426
- interval = interval - @base
427
- end
428
-
429
- @interval = interval
430
- return @interval
431
- end
432
-
433
-
434
- ### Parse and set the ending attribute, given a +time_arg+
435
- ### string and the +type+ of string (interval or exact time)
436
- ###
437
- ### Perform consistency and sanity checks before returning a
438
- ### Time object.
439
- ###
440
- def set_ending( time_arg, type )
441
- ending = nil
442
-
443
- # Ending dates only make sense for recurring events.
444
- #
445
- if self.recurring
446
- @ending_args = [ time_arg, type ] # squirrel away for post-set starts
447
-
448
- # Make the interval an offset of the start time, instead of now.
449
- #
450
- # This is the contextual difference between:
451
- # every minute until 6 hours from now (ending based on NOW)
452
- # and
453
- # starting in a year run every minute for 1 month (ending based on start time)
454
- #
455
- if self.starting && type == :interval
456
- diff = self.parse_interval( time_arg )
457
- ending = self.starting + diff
458
-
459
- # (offset from now)
460
- #
461
- else
462
- ending = self.get_time( time_arg, type )
463
- end
464
-
465
- # Check the end time is after the start time.
466
- #
467
- if self.starting && ending < self.starting
468
- raise Symphony::Metronome::TimeParseError, "recurring event ends before it begins"
469
- end
470
-
471
- else
472
- self.log.debug "Ignoring ending date, event is not recurring."
473
- end
474
-
475
- @ending = ending
476
- return @ending
477
- end
478
-
479
-
480
- ### Perform finishing logic and final sanity checks before returning
481
- ### a parsed object.
482
- ###
483
- def finalize
484
- raise Symphony::Metronome::TimeParseError, "unable to parse expression" unless self.valid
485
-
486
- # Ensure start time is populated.
487
- #
488
- unless self.starting
489
- if self.recurring
490
- @starting = @base
491
- else
492
- raise Symphony::Metronome::TimeParseError, "non-deterministic expression" if self.interval.nil?
493
- @starting = @base + self.interval
494
- end
495
- end
496
-
497
- # Alter the interval if a multiplier was specified.
498
- #
499
- if self.multiplier
500
- if self.ending
501
-
502
- # Regular 'count' style multipler with end date.
503
- # (run 10 times a minute for 2 days)
504
- # Just divide the current interval by the count.
505
- #
506
- if self.interval
507
- @interval = self.interval.to_f / self.multiplier
508
-
509
- # Timeboxed multiplier (start [date] run 10 times end [date])
510
- # Evenly spread the interval out over the time window.
511
- #
512
- else
513
- diff = self.ending - self.starting
514
- @interval = diff.to_f / self.multiplier
515
- end
516
-
517
- # Regular 'count' style multipler (run 10 times a minute)
518
- # Just divide the current interval by the count.
519
- #
520
- else
521
- raise Symphony::Metronome::TimeParseError, "An end date or interval is required" unless self.interval
522
- @interval = self.interval.to_f / self.multiplier
523
- end
524
- end
525
- end
526
-
527
-
528
- ### Given a +time_arg+ string and a type (:interval or :time),
529
- ### dispatch to the appropriate parser.
530
- ###
531
- def get_time( time_arg, type )
532
- time = nil
533
-
534
- if type == :interval
535
- secs = self.parse_interval( time_arg )
536
- time = @base + secs if secs
537
- end
538
-
539
- if type == :time
540
- time = self.parse_time( time_arg )
541
- end
542
-
543
- raise Symphony::Metronome::TimeParseError, "unable to parse time" if time.nil?
544
- return time
545
- end
546
-
547
-
548
- ### Parse a +time_arg+ string (anything parsable buy Time.parse())
549
- ### into a Time object.
550
- ###
551
- def parse_time( time_arg )
552
- time = Time.parse( time_arg, @base ) rescue nil
553
-
554
- # Generated date is in the past.
555
- #
556
- if time && @base > time
557
-
558
- # Ensure future dates for ambiguous times (2pm)
559
- time = time + 1.day if time_arg.length < 8
560
-
561
- # Still in the past, abandon all hope.
562
- raise Symphony::Metronome::TimeParseError, "attempt to schedule in the past" if @base > time
563
- end
564
-
565
- self.log.debug "Parsed %p (time) to: %p" % [ time_arg, time ]
566
- return time
567
- end
568
-
569
-
570
- ### Parse a +time_arg+ interval string ("30 seconds") into an
571
- ### Integer.
572
- ###
573
- def parse_interval( interval_arg )
574
- duration, span = interval_arg.split( /\s+/ )
575
-
576
- # catch the 'a' or 'an' case (ex: "an hour")
577
- duration = 1 if duration.index( 'a' ) == 0
578
-
579
- # catch the 'other' case, ie: 'every other hour'
580
- duration = 2 if duration == 'other'
581
-
582
- # catch the singular case (ex: "hour")
583
- unless span
584
- span = duration
585
- duration = 1
586
- end
587
-
588
- use_milliseconds = span.sub!( 'milli', '' )
589
- interval = calculate_seconds( duration.to_f, span.to_sym )
590
-
591
- # milliseconds
592
- interval = duration.to_f / 1000 if use_milliseconds
593
-
594
- self.log.debug "Parsed %p (interval) to: %p" % [ interval_arg, interval ]
595
- return interval
596
- end
597
-
598
- end # class TimeExpression
599
-