class CronParser

Parses cron expressions and computes the next occurence of the “job”

Constants

SUBELEMENT_REGEX
SYMBOLS

Public Class Methods

new(source,time_source = Time) click to toggle source
# File lib/cron_parser.rb, line 54
def initialize(source,time_source = Time)
  @source = source
  @time_source = time_source
  validate_source
end

Public Instance Methods

last(now = @time_source.now) click to toggle source

returns the last occurence before the given date

# File lib/cron_parser.rb, line 86
def last(now = @time_source.now)
  t = InternalTime.new(now,@time_source)

  unless time_specs[:month][0].include?(t.month)
    nudge_month(t, :last)
    t.day = 32
  end

  if t.day == 32 || !interpolate_weekdays(t.year, t.month)[0].include?(t.day)
    nudge_date(t, :last)
    t.hour = 24
  end

  unless time_specs[:hour][0].include?(t.hour)
    nudge_hour(t, :last)
    t.min = 60
  end

  # always nudge the minute
  nudge_minute(t, :last)
  t.to_time
end
next(now = @time_source.now) click to toggle source

returns the next occurence after the given date

# File lib/cron_parser.rb, line 62
def next(now = @time_source.now)
  t = InternalTime.new(now,@time_source)

  unless time_specs[:month][0].include?(t.month)
    nudge_month(t)
    t.day = 0
  end

  unless interpolate_weekdays(t.year, t.month)[0].include?(t.day)
    nudge_date(t)
    t.hour = -1
  end

  unless time_specs[:hour][0].include?(t.hour)
    nudge_hour(t)
    t.min = -1
  end

  # always nudge the minute
  nudge_minute(t)
  t.to_time
end
parse_element(elem, allowed_range) click to toggle source
# File lib/cron_parser.rb, line 111
def parse_element(elem, allowed_range)
  values = elem.split(',').map do |subel|
    if subel =~ /^\*/
      step = subel.length > 1 ? subel[2..-1].to_i : 1
      stepped_range(allowed_range, step)
    else
      if SUBELEMENT_REGEX === subel
        if $5 # with range
          stepped_range($1.to_i..$3.to_i, $5.to_i)
        elsif $3 # range without step
          stepped_range($1.to_i..$3.to_i, 1)
        else # just a numeric
          [$1.to_i]
        end
      else
        raise "Bad Vixie-style specification #{subel}"
      end
    end
  end.flatten.sort

  [Set.new(values), values]
end

Protected Instance Methods

date_valid?(t, dir = :next) click to toggle source
# File lib/cron_parser.rb, line 173
def date_valid?(t, dir = :next)
  interpolate_weekdays(t.year, t.month)[0].include?(t.day)
end
find_best_next(current, allowed, dir) click to toggle source

returns the smallest element from allowed which is greater than current returns nil if no matching value was found

# File lib/cron_parser.rb, line 235
def find_best_next(current, allowed, dir)
  if dir == :next
    allowed.sort.find { |val| val > current }
  else
    allowed.sort.reverse.find { |val| val < current }
  end
end
interpolate_weekdays(year, month) click to toggle source

returns a list of days which do both match time_spec and time_spec

# File lib/cron_parser.rb, line 138
def interpolate_weekdays(year, month)
  @_interpolate_weekdays_cache ||= {}
  @_interpolate_weekdays_cache["#{year}-#{month}"] ||= interpolate_weekdays_without_cache(year, month)
end
interpolate_weekdays_without_cache(year, month) click to toggle source
# File lib/cron_parser.rb, line 143
def interpolate_weekdays_without_cache(year, month)
  t = Date.new(year, month, 1)
  valid_mday = time_specs[:dom][0]
  valid_wday = time_specs[:dow][0]

  result = []
  while t.month == month
    result << t.mday if valid_mday.include?(t.mday) && valid_wday.include?(t.wday)
    t = t.succ
  end

  [Set.new(result), result]
end
nudge_date(t, dir = :next, can_nudge_month = true) click to toggle source
# File lib/cron_parser.rb, line 177
def nudge_date(t, dir = :next, can_nudge_month = true)
  spec = interpolate_weekdays(t.year, t.month)[1]
  next_value = find_best_next(t.day, spec, dir)
  t.day = next_value || (dir == :next ? spec.first : spec.last)

  nudge_month(t, dir) if next_value.nil? && can_nudge_month
end
nudge_hour(t, dir = :next) click to toggle source
# File lib/cron_parser.rb, line 185
def nudge_hour(t, dir = :next)
  spec = time_specs[:hour][1]
  next_value = find_best_next(t.hour, spec, dir)
  t.hour = next_value || (dir == :next ? spec.first : spec.last)

  nudge_date(t, dir) if next_value.nil?
end
nudge_minute(t, dir = :next) click to toggle source
# File lib/cron_parser.rb, line 193
def nudge_minute(t, dir = :next)
  spec = time_specs[:minute][1]
  next_value = find_best_next(t.min, spec, dir)
  t.min = next_value || (dir == :next ? spec.first : spec.last)

  nudge_hour(t, dir) if next_value.nil?
end
nudge_month(t, dir = :next) click to toggle source
# File lib/cron_parser.rb, line 161
def nudge_month(t, dir = :next)
  spec = time_specs[:month][1]
  next_value = find_best_next(t.month, spec, dir)
  t.month = next_value || (dir == :next ? spec.first : spec.last)

  nudge_year(t, dir) if next_value.nil?

  # we changed the month, so its likely that the date is incorrect now
  valid_days = interpolate_weekdays(t.year, t.month)[1]
  t.day = dir == :next ? valid_days.first : valid_days.last
end
nudge_year(t, dir = :next) click to toggle source
# File lib/cron_parser.rb, line 157
def nudge_year(t, dir = :next)
  t.year = t.year + (dir == :next ? 1 : -1)
end
stepped_range(rng, step = 1) click to toggle source
# File lib/cron_parser.rb, line 222
def stepped_range(rng, step = 1)
  len = rng.last - rng.first

  num = len.div(step)
  result = (0..num).map { |i| rng.first + step * i }

  result.pop if result[-1] == rng.last and rng.exclude_end?
  result
end
substitute_parse_symbols(str) click to toggle source
# File lib/cron_parser.rb, line 215
def substitute_parse_symbols(str)
  SYMBOLS.inject(str.downcase) do |s, (symbol, replacement)|
    s.gsub(symbol, replacement)
  end
end
time_specs() click to toggle source
# File lib/cron_parser.rb, line 201
def time_specs
  @time_specs ||= begin
    # tokens now contains the 5 fields
    tokens = substitute_parse_symbols(@source).split(/\s+/)
    {
      :minute => parse_element(tokens[0], 0..59), #minute
      :hour   => parse_element(tokens[1], 0..23), #hour
      :dom    => parse_element(tokens[2], 1..31), #DOM
      :month  => parse_element(tokens[3], 1..12), #mon
      :dow    => parse_element(tokens[4], 0..6)  #DOW
    }
  end
end
validate_source() click to toggle source
# File lib/cron_parser.rb, line 243
def validate_source
  unless @source.respond_to?(:split)
    raise ArgumentError, 'not a valid cronline'
  end
  source_length = @source.split(/\s+/).length
  unless source_length >= 5 && source_length <= 6
    raise ArgumentError, 'not a valid cronline'
  end
end