class Symphony::Metronome::IntervalExpression
Parse natural English expressions of times and intervals.
in 30 minutes once an hour every 15 minutes for 2 days at 2014-05-01 at 2014-04-01 14:00:25 at 2pm starting at 2pm once a day start in 1 hour from now run every 5 seconds end at 11:15pm every other hour once a day ending in 1 week run once a minute for an hour starting in 6 days run each hour starting at 2010-01-05 09:00:00 10 times a minute for 2 days run 45 times every hour 30 times per day start at 2010-01-02 run 12 times and end on 2010-01-03 starting in an hour from now run 6 times a minute for 2 hours beginning a day from now, run 30 times per minute and finish in 2 weeks execute 12 times during the next 2 minutes
Constants
- COMMON_DECORATORS
Words/phrases in the expression that we'll strip/ignore before parsing.
Attributes
The valid end time for the schedule (for recurring events)
The interval to wait before the event should be acted on.
An optional interval multipler for expressing counts.
Does this event repeat?
The valid start time for the schedule (for recurring events)
Is the schedule expression parsable?
Public Class Methods
Parse a schedule expression exp
.
Parsing defaults to Time.now(), but if passed a time
object, all contexual times (2pm) are relative to it. If you know when an expression was generated, you can 'reconstitute' an interval object this way.
# File lib/symphony/metronome/intervalexpression.rb, line 1919 def self::parse( exp, time=nil ) # Normalize the expression before parsing # exp = exp.downcase. gsub( /(?:[^[a-z][0-9][\.\-:]\s]+)/, '' ). # . : - a-z 0-9 only gsub( Regexp.union(COMMON_DECORATORS), '' ). # remove common decorator words gsub( /\s+/, ' ' ). # collapse whitespace gsub( /([:\-])+/, '\1' ). # collapse multiple - or : chars gsub( /\.+$/, '' ) # trailing periods event = new( exp, time || Time.now ) data = event.instance_variable_get( :@data ) # Ragel interface variables # key = '' mark = 0 begin p ||= 0 pe ||= data.length cs = interval_expression_start end eof = pe begin testEof = false _klen, _trans, _keys = nil _goto_level = 0 _resume = 10 _eof_trans = 15 _again = 20 _test_eof = 30 _out = 40 while true if _goto_level <= 0 if p == pe _goto_level = _test_eof next end if cs == 0 _goto_level = _out next end end if _goto_level <= _resume _keys = _interval_expression_key_offsets[cs] _trans = _interval_expression_index_offsets[cs] _klen = _interval_expression_single_lengths[cs] _break_match = false begin if _klen > 0 _lower = _keys _upper = _keys + _klen - 1 loop do break if _upper < _lower _mid = _lower + ( (_upper - _lower) >> 1 ) if data[p].ord < _interval_expression_trans_keys[_mid] _upper = _mid - 1 elsif data[p].ord > _interval_expression_trans_keys[_mid] _lower = _mid + 1 else _trans += (_mid - _keys) _break_match = true break end end # loop break if _break_match _keys += _klen _trans += _klen end _klen = _interval_expression_range_lengths[cs] if _klen > 0 _lower = _keys _upper = _keys + (_klen << 1) - 2 loop do break if _upper < _lower _mid = _lower + (((_upper-_lower) >> 1) & ~1) if data[p].ord < _interval_expression_trans_keys[_mid] _upper = _mid - 2 elsif data[p].ord > _interval_expression_trans_keys[_mid+1] _lower = _mid + 2 else _trans += ((_mid - _keys) >> 1) _break_match = true break end end # loop break if _break_match _trans += _klen end end while false cs = _interval_expression_trans_targs[_trans]; if _interval_expression_trans_actions[_trans] != 0 case _interval_expression_trans_actions[_trans] when 2 then begin mark = p end when 1 then begin event.instance_variable_set( :@valid, false ) end when 3 then begin event.instance_variable_set( :@recurring, true ) end when 4 then begin time = event.send( :extract, mark, p - mark ) event.send( :set_starting, time, :time ) end when 5 then begin interval = event.send( :extract, mark, p - mark ) event.send( :set_starting, interval, :interval ) end when 9 then begin interval = event.send( :extract, mark, p - mark ) event.send( :set_interval, interval, :interval ) end when 7 then begin multiplier = event.send( :extract, mark, p - mark ).sub( / times/, '' ) event.instance_variable_set( :@multiplier, multiplier.to_i ) end when 14 then begin time = event.send( :extract, mark, p - mark ) event.send( :set_ending, time, :time ) end when 15 then begin interval = event.send( :extract, mark, p - mark ) event.send( :set_ending, interval, :interval ) end end # action switch end end if _goto_level <= _again if cs == 0 _goto_level = _out next end p += 1 if p != pe _goto_level = _resume next end end if _goto_level <= _test_eof if p == eof begin case ( _interval_expression_eof_actions[cs] ) when 1 then begin event.instance_variable_set( :@valid, false ) end when 10 then begin time = event.send( :extract, mark, p - mark ) event.send( :set_starting, time, :time ) end begin event.instance_variable_set( :@valid, true ) end when 13 then begin interval = event.send( :extract, mark, p - mark ) event.send( :set_starting, interval, :interval ) end begin event.instance_variable_set( :@valid, true ) end when 16 then begin time = event.send( :extract, mark, p - mark ) event.send( :set_interval, time, :time ) end begin event.instance_variable_set( :@valid, true ) end when 8 then begin interval = event.send( :extract, mark, p - mark ) event.send( :set_interval, interval, :interval ) end begin event.instance_variable_set( :@valid, true ) end when 6 then begin multiplier = event.send( :extract, mark, p - mark ).sub( / times/, '' ) event.instance_variable_set( :@multiplier, multiplier.to_i ) end begin event.instance_variable_set( :@valid, true ) end when 11 then begin time = event.send( :extract, mark, p - mark ) event.send( :set_ending, time, :time ) end begin event.instance_variable_set( :@valid, true ) end when 12 then begin interval = event.send( :extract, mark, p - mark ) event.send( :set_ending, interval, :interval ) end begin event.instance_variable_set( :@valid, true ) end end end end end if _goto_level <= _out break end end end # Attach final time logic and sanity checks. event.send( :finalize ) return event end
Public Instance Methods
Comparable interface, order by interval, 'soonest' first.
# File lib/symphony/metronome/intervalexpression.rb, line 2257 def <=>( other ) return self.interval <=> other.interval end
If this interval is on a stack somewhere and ready to fire, is it okay to do so based on the specified expression criteria?
Returns true
if it should fire, false
if it should not but could at a later attempt, and nil
if the interval has expired.
# File lib/symphony/metronome/intervalexpression.rb, line 2217 def fire? now = Time.now # Interval has expired. return nil if self.ending && now > self.ending # Interval is not yet in its current time window. return false if self.starting - now > 0 # Looking good. return true end
Inspection string.
# File lib/symphony/metronome/intervalexpression.rb, line 2240 def inspect return ( "<%s:0x%08x valid:%s recur:%s expression:%p " + "starting:%p interval:%p ending:%p>" ) % [ self.class.name, self.object_id * 2, self.valid, self.recurring, self.to_s, self.starting, self.interval, self.ending ] end
Just return the original event expression.
# File lib/symphony/metronome/intervalexpression.rb, line 2233 def to_s return @exp end
Protected Instance Methods
Given a start
and ending
scanner position, return an ascii representation of the data slice.
# File lib/symphony/metronome/intervalexpression.rb, line 2269 def extract( start, ending ) slice = @data[ start, ending ] return '' unless slice return slice.pack( 'c*' ) end
Perform finishing logic and final sanity checks before returning a parsed object.
# File lib/symphony/metronome/intervalexpression.rb, line 2375 def finalize raise Symphony::Metronome::TimeParseError, "unable to parse expression" unless self.valid # Ensure start time is populated. # unless self.starting if self.recurring @starting = @base else raise Symphony::Metronome::TimeParseError, "non-deterministic expression" if self.interval.nil? @starting = @base + self.interval end end # Alter the interval if a multiplier was specified. # if self.multiplier if self.ending # Regular 'count' style multipler with end date. # (run 10 times a minute for 2 days) # Just divide the current interval by the count. # if self.interval @interval = self.interval.to_f / self.multiplier # Timeboxed multiplier (start [date] run 10 times end [date]) # Evenly spread the interval out over the time window. # else diff = self.ending - self.starting @interval = diff.to_f / self.multiplier end # Regular 'count' style multipler (run 10 times a minute) # Just divide the current interval by the count. # else raise Symphony::Metronome::TimeParseError, "An end date or interval is required" unless self.interval @interval = self.interval.to_f / self.multiplier end end end
Given a time_arg
string and a type (:interval or :time), dispatch to the appropriate parser.
# File lib/symphony/metronome/intervalexpression.rb, line 2423 def get_time( time_arg, type ) time = nil if type == :interval secs = self.parse_interval( time_arg ) time = @base + secs if secs end if type == :time time = self.parse_time( time_arg ) end raise Symphony::Metronome::TimeParseError, "unable to parse time" if time.nil? return time end
Parse a time_arg
interval string (“30 seconds”) into an Integer.
# File lib/symphony/metronome/intervalexpression.rb, line 2465 def parse_interval( interval_arg ) duration, span = interval_arg.split( /\s+/ ) # catch the 'a' or 'an' case (ex: "an hour") duration = 1 if duration.index( 'a' ) == 0 # catch the 'other' case, ie: 'every other hour' duration = 2 if duration == 'other' # catch the singular case (ex: "hour") unless span span = duration duration = 1 end use_milliseconds = span.sub!( 'milli', '' ) interval = calculate_seconds( duration.to_f, span.to_sym ) # milliseconds interval = duration.to_f / 1000 if use_milliseconds self.log.debug "Parsed %p (interval) to: %p" % [ interval_arg, interval ] return interval end
Parse a time_arg
string (anything parsable buy Time.parse()) into a Time object.
# File lib/symphony/metronome/intervalexpression.rb, line 2443 def parse_time( time_arg ) time = Time.parse( time_arg, @base ) rescue nil # Generated date is in the past. # if time && @base > time # Ensure future dates for ambiguous times (2pm) time = time + 1.day if time_arg.length < 8 # Still in the past, abandon all hope. raise Symphony::Metronome::TimeParseError, "attempt to schedule in the past" if @base > time end self.log.debug "Parsed %p (time) to: %p" % [ time_arg, time ] return time end
Parse and set the ending attribute, given a time_arg
string and the type
of string (interval or exact time)
Perform consistency and sanity checks before returning a Time object.
# File lib/symphony/metronome/intervalexpression.rb, line 2332 def set_ending( time_arg, type ) ending = nil # Ending dates only make sense for recurring events. # if self.recurring @ending_args = [ time_arg, type ] # squirrel away for post-set starts # Make the interval an offset of the start time, instead of now. # # This is the contextual difference between: # every minute until 6 hours from now (ending based on NOW) # and # starting in a year run every minute for 1 month (ending based on start time) # if self.starting && type == :interval diff = self.parse_interval( time_arg ) ending = self.starting + diff # (offset from now) # else ending = self.get_time( time_arg, type ) end # Check the end time is after the start time. # if self.starting && ending < self.starting raise Symphony::Metronome::TimeParseError, "recurring event ends before it begins" end else self.log.debug "Ignoring ending date, event is not recurring." end @ending = ending return @ending end
Parse and set the interval attribute, given a time_arg
string and the type
of string (interval or exact time)
Perform consistency and sanity checks before returning an integer representing the amount of time needed to sleep before firing the event.
# File lib/symphony/metronome/intervalexpression.rb, line 2312 def set_interval( time_arg, type ) interval = nil if self.starting && type == :time raise Symphony::Metronome::TimeParseError, "That doesn't make sense, just use 'at [datetime]' instead" else interval = self.get_time( time_arg, type ) interval = interval - @base end @interval = interval return @interval end
Parse and set the starting attribute, given a time_arg
string and the type
of string (interval or exact time)
# File lib/symphony/metronome/intervalexpression.rb, line 2279 def set_starting( time_arg, type ) @starting_args ||= [] @starting_args << time_arg # If we already have seen a start time, it's possible the parser # was non-deterministic and this action has been executed multiple # times. Re-parse the complete date string, overwriting any previous. time_arg = @starting_args.join( ' ' ) start = self.get_time( time_arg, type ) @starting = start # If start time is expressed as a post-conditional (we've # already got an end time) we need to recalculate the end # as an offset from the start. The original parsed ending # arguments should have already been cached when it was # previously set. # if self.ending && self.recurring self.set_ending( *@ending_args ) end return @starting end