class CronFormat

Attributes

to_expression[R]
to_time[R]

Public Class Methods

new(cron_string, now=Time.now, debug: false) click to toggle source
# File lib/cron_format.rb, line 22
def initialize(cron_string, now=Time.now, debug: false)  
  
  puts 'inside CronFormat'.info if debug
  @cron_string, @now, @debug = cron_string, now, debug
  @to_time = @now
  parse()
  
end

Public Instance Methods

adjust_date(d) click to toggle source

supply a Time object. Modifying the date can be helpful when triggering a day before an actual expression date e.g. the day before the last sunday in March (British summer time).

# File lib/cron_format.rb, line 35
def adjust_date(d)
  
  @to_time = d
  m, h, dd, mm, yy = @to_expression.split
  
  day = dd =~ /^\d+$/ ? d.day : dd
  month = mm =~ /^\d+$/  ? d.month : mm
  year = yy =~ /^\d+$/ ? d.year : yy
  
  @to_expression = [m, h, day, month, year].join(' ')
  
end
next() click to toggle source
# File lib/cron_format.rb, line 48
def next()
  
  nudge() #unless @cron_string =~ %r{/}
  #puts ':to_time : ' + @to_time.inspect
  parse(nudged: true)
end

Private Instance Methods

day_valid?(date) click to toggle source
# File lib/cron_format.rb, line 394
def day_valid?(date)

  year, month, day = date
  last_day = DateTime.parse("%s-%s-%s" % [year, 
                    (month.to_i < 12 ? month.succ : 1), 1]) - 1
  day.to_i <= last_day.day
end
increment_month(d) click to toggle source
# File lib/cron_format.rb, line 402
def increment_month(d)
  
  puts 'inside increment_month' if @debug

  if d[3].to_i <= 11 then
    d[3].succ!
  else 
    d[3] = '1'
    d[4].succ!
  end

  Time.parse(TF % d.reverse)
end
inflate(raw_a) click to toggle source
# File lib/cron_format.rb, line 416
def inflate(raw_a)

  a = Array.new raw_a

  Array.new(a.max_by {|x| x.length}.length).map do |x|
    a.map{|x| x.length <= 1 ? x.first : x.shift}
  end
end
nudge() click to toggle source
# File lib/cron_format.rb, line 57
def nudge()

  t1 = @to_time
  puts ('t1: ' + t1.inspect).debug if @debug
  a  =  @cron_string.split
  
  val = if @cron_string =~ %r{[/,-]} then
    a.reverse.detect{|x| x[/[\/,-]/]}
  else
    a[1..-1].detect{|x| x != '*'}
  end
  
  index, n = 0, 1
  
  puts ('val: ' + val.inspect).debug if @debug

  if val then
    index = a.index(val)

    r = val[/,|\/(\d+)$/,1]      

    n =  if r then
    
      index == 4 ? r.to_i * 7 : 0
      
    else
              
      if val =~ /[,-]/ then
        1
      else
        val.to_i
      end
    end
  end

  puts ('index: ' + index.inspect).debug if @debug
  
  month_proc = lambda {|t1,n|
    a = t1.to_a
    a[4] = a[4] + n <= 12 ? a[4] + n  : a[4] + n - 12
    t = Time.parse(TF % a.values_at(5,4,3,2,1,0))
    t -= DAY until t.month == a[4]
    return t
  }

  day_proc = lambda {|x,n| x + n * DAY}

  units = [
    lambda {|x,n| x + n * MINUTE}, 
    lambda {|x,n| x + n * HOUR}, 
    day_proc, 
    month_proc,
    day_proc
  ]
  
  if @debug then
    puts ('@to_time: ' + @to_time.inspect).debug
    puts ('n: ' + n.inspect).debug
  end
  
  r = units[index].call @to_time, n
  
  puts ('r: ' + r.inspect).debug if @debug
  
  @to_time = if n > 1 then
  
    # given day light savings, ensure the time fragment is preserved
    Time.new(r.year, r.month, r.day, t1.hour, t1.min)
    
  else
    r
  end
  #r += MINUTE  if r == t1

end
parse(nudged: false) click to toggle source
# File lib/cron_format.rb, line 133
def parse(nudged: false)
  
  puts ('0. @to_time: ' + @to_time.inspect).debug if @debug

  raw_a = @cron_string.split
  raw_a << '*' if raw_a.length <= 5 # add the year?
  mins, hours, day, month, wday, year = raw_a[0..5]
  
  if day[/\d+/] and month[/\d+/] and year == '*' then
    @to_time += DAY until @to_time.day == day.to_i and \
                                              @to_time.month == month.to_i
  end
  
  puts ('1. @to_time: ' + @to_time.inspect).debug if @debug
  
  #
  
  if @debug then
    puts ('1.5 @to_time: ' + @to_time.inspect).debug 
    puts ('hours: ' + hours.inspect).debug
    puts ('mins: ' + mins.inspect).debug
  end
  
  if mins[/^\d+$/] and hours[/^\d+$/] then

    if @to_time.to_date != @now.to_date then
      @to_time = Time.local(@to_time.year, @to_time.month, @to_time.day)
    end
    
    until (@to_time.min == mins.to_i and @to_time.hour == hours.to_i) \
        or (@to_time - 1).isdst != @to_time.isdst do
      
      puts ('1.7 @to_time: ' + @to_time.inspect).debug if @debug
      @to_time += MINUTE
    end
    @to_time -= MINUTE
  else
    
    if mins[/^\d+$/] then
      @to_time += MINUTE until @to_time.min == mins.to_i 
      @to_time -= MINUTE
    end
    
    @to_time += HOUR until @to_time.hour == hours.to_i if hours[/^\d+$/]
  end

  puts ('2. @to_time: ' + @to_time.inspect).debug if @debug    
  
  if wday[/^[0-6]$/] and @to_time.wday != wday.to_i then
    @to_time += DAY until @to_time.wday == wday.to_i
  end
  
  dayceiling = raw_a[2][/-(\d+)$/,1]
 
  if dayceiling and dayceiling.to_i <= @to_time.day then

    dt2 = @to_time.to_datetime
    next_month = dt2.next_month.month
    dt2 += 1 until dt2.month == next_month

    @to_time = dt2.to_time 
  end
  
  puts ('3. @to_time: ' + @to_time.inspect).debug if @debug

  units = @to_time.to_a.values_at(1..4) + [nil, @to_time.year]

  procs = {
      min: lambda {|x, interval| x += (interval * MINUTE).to_i},
     hour: lambda {|x, interval| x += (interval * HOUR).to_i},
      day: lambda {|x, interval| x += (interval * DAY).to_i}, 
    month: lambda {|x, interval| 
       date = x.to_a.values_at(1..5)
       interval.times { date[3].succ! }
       Time.parse(TF % date.reverse)},
     week: lambda {|x, interval| x += (interval * WEEK).to_i}, 
     year: lambda {|x, interval| 
       date = x.to_a.values_at(1..5)
       interval.times { date[4].succ! }
       Time.parse(TF % date.reverse)}
  }

  dt = units.map do |start|
    # convert * to time unit
    lambda do |x| v2 = x.sub('*', start.to_s)
      # split any times
      multiples = v2.split(/,/)
      range = multiples.map do |time|
        s1, s2 = time.split(/-/)
        s2 ? (s1..s2).to_a : s1
      end
      range.flatten
    end

  end
  
  # take any repeater out of the unit value
  raw_units, repeaters = [], []
  
  raw_a.each do |x| 
    v1, v2 = x.split('/')
    raw_units << v1
    repeaters << v2
  end
  
  
  if raw_a[4] != '*' then
    r = /(sun|mon|tues|wed|thurs|fri|satur|sun)(day)?|tue|thu|sat/i    

    to_i = lambda {|x|
      a = Date::DAYNAMES
      a.index a.grep(/#{x}/i).first
    }
    raw_a[4].gsub!(r,&to_i)
    raw_units[4].gsub!(r,&to_i) 
  end
  
  @to_expression = raw_a[0..4].join ' '  

  raw_date = raw_units.map.with_index {|x,i| dt[i].call(x) }

  # expand the repeater

  ceil = {min: MINUTE, hour: 23, day: 31, month: 12}.values    

  if repeaters.any? then
    repeaters.each_with_index do |x,i|

      next if i == 4

      if x and not raw_a[i][/^\*/] then
        raw_date[i] = raw_date[i].map {|y|            
          (y.to_i...ceil[i]).step(x.to_i).to_a.map(&:to_s)
        }.flatten
      else  
        raw_date[i]
      end 
    end
  end  

  dates = inflate(raw_date)

  puts ('dates: ' + dates.inspect).debug if @debug

  a = dates.map do |date|

    d = date.map{|x| x ? x.clone : nil}
    wday, year = d.pop(2)
    d << year

    next unless day_valid? d.reverse.take 3
    t = Time.parse(TF % d.reverse)      
    # if there is a defined weekday, increment a day at
    #                          a time to match that weekday
    if wday and wday != t.wday then
      
      t = Time.parse(TF % d.reverse)        

      if repeaters[4] then
        t += repeaters[4].to_i * WEEK while t < @to_time
      else
        d[2], d[3] = @to_time.to_a.values_at(3,4).map(&:to_s)         
        t += DAY until t.wday == wday.to_i        
      end
      
    end
    
    # increment the month, day, hour, and minute for
    #              expressions which match '* * * *' consecutively
    i = 3

    while t < @to_time and i >= 0 and raw_a[i][/\*/]

      d[i] = @to_time.to_a[i+1].to_s
      t = Time.parse(TF % d.reverse)
      i -= 1
    end

    # starting from the biggest unit, attempt to increment that
    #                       unit where it is equal to '*'

    if @debug then
      puts ('t: ' + t.inspect).debug
      puts ('@to_time: ' + @to_time.inspect).debug
    end
    
    if t < @to_time then

      if t.month < @to_time.month and raw_a[4] == '*' then

        # increment the year
        d[4].succ!
        t = Time.parse(TF % d.reverse)
        puts 't: ' + t.inspect if @debug

        if repeaters[4] then

          d[4].succ!
          t = Time.parse(TF % d.reverse)
        end
      elsif t.day < @to_time.day and raw_a[3] == '*' then

        t = increment_month d
      elsif  (t.hour < @to_time.hour or (t.hour == @to_time.hour \
        and t.min < @to_time.min and raw_a[1] != '*') ) \
          and raw_a[2] == '*' then

        puts 'incrementing the day' if @debug
        # increment the day
        t += DAY * ((@to_time.day - d[2].to_i) + 1)
      elsif t.min < @to_time.min and raw_a[1] == '*' then

        # increment the hour
        t += HOUR * ((@to_time.hour - d[1].to_i) + 1)
      elsif raw_a[0][0] == '*' then

        i = 0

        # increment the minute
        t += MINUTE * ((@to_time.min - d[0].to_i) + 1)
        t = procs.values[i].call(t, repeaters[i].to_i) if repeaters[i]
      elsif raw_a[3] == '*' then

        t = increment_month d        
      end   

    end

    # after the units have been incremented we need to
    #                   increment the weekday again if need be
    if wday then

      if raw_date[2].length > 1 then

        t += DAY until t.wday == wday.to_i and raw_date[2].include? t.day.to_s
      else
        t += DAY until t.wday == wday.to_i
      end

    end

    # finally, if the date is still less than the current time we can
    #      increment the date using any repeating intervals
    if (t < @to_time or (!nudged and t == @to_time)) and repeaters.any? then

      repeaters.each_with_index do |x,i|

        if x then
          t = procs.values[i].call(t, x.to_i)
        end
      end
    end

    t     
  end
  
  puts ('a: ' + a.inspect).debug if @debug

  @to_time = a.compact.min
end