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/scheduled/cron_parser.rb, line 78 def initialize(source,time_source = Time) @source = interpret_vixieisms(source) @time_source = time_source validate_source end
Public Instance Methods
interpret_vixieisms(spec)
click to toggle source
# File lib/scheduled/cron_parser.rb, line 84 def interpret_vixieisms(spec) case spec when '@reboot' raise ArgumentError, "Can't predict last/next run of @reboot" when '@yearly', '@annually' '0 0 1 1 *' when '@monthly' '0 0 1 * *' when '@weekly' '0 0 * * 0' when '@daily', '@midnight' '0 0 * * *' when '@hourly' '0 * * * *' else spec end end
last(now = @time_source.now, num=1)
click to toggle source
returns the last occurence before the given date
# File lib/scheduled/cron_parser.rb, line 134 def last(now = @time_source.now, num=1) 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 = t.to_time if num > 1 recursive_calculate(:last,t,num) else t end end
next(now = @time_source.now, num = 1)
click to toggle source
returns the next occurence after the given date
# File lib/scheduled/cron_parser.rb, line 105 def next(now = @time_source.now, num = 1) 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 = t.to_time if num > 1 recursive_calculate(:next,t,num) else t end end
parse_element(elem, allowed_range)
click to toggle source
# File lib/scheduled/cron_parser.rb, line 164 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 ArgumentError, "Bad Vixie-style specification #{subel}" end end end.flatten.sort [Set.new(values), values, elem] end
Protected Instance Methods
date_valid?(t, dir = :next)
click to toggle source
# File lib/scheduled/cron_parser.rb, line 243 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/scheduled/cron_parser.rb, line 305 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
interpolate_weekdays_without_cache(year, month)
click to toggle source
# File lib/scheduled/cron_parser.rb, line 204 def interpolate_weekdays_without_cache(year, month) t = Date.new(year, month, 1) valid_mday, _, mday_field = time_specs[:dom] valid_wday, _, wday_field = time_specs[:dow] # Careful, if both DOW and DOM fields are non-wildcard, # then we only need to match *one* for cron to run the job: if not (mday_field == '*' and wday_field == '*') valid_mday = [] if mday_field == '*' valid_wday = [] if wday_field == '*' end # Careful: crontabs may use either 0 or 7 for Sunday: valid_wday << 0 if valid_wday.include?(7) 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/scheduled/cron_parser.rb, line 247 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/scheduled/cron_parser.rb, line 255 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/scheduled/cron_parser.rb, line 263 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/scheduled/cron_parser.rb, line 231 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/scheduled/cron_parser.rb, line 227 def nudge_year(t, dir = :next) t.year = t.year + (dir == :next ? 1 : -1) end
recursive_calculate(meth,time,num)
click to toggle source
# File lib/scheduled/cron_parser.rb, line 190 def recursive_calculate(meth,time,num) array = [time] num.-(1).times do |num| array << self.send(meth, array.last) end array end
stepped_range(rng, step = 1)
click to toggle source
# File lib/scheduled/cron_parser.rb, line 292 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/scheduled/cron_parser.rb, line 285 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/scheduled/cron_parser.rb, line 271 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/scheduled/cron_parser.rb, line 313 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