Skip to content

Instantly share code, notes, and snippets.

@jemmyw
Created November 11, 2011 06:42
Show Gist options
  • Save jemmyw/1357363 to your computer and use it in GitHub Desktop.
Save jemmyw/1357363 to your computer and use it in GitHub Desktop.
Cronline
#
#--
# Copyright (c) 2006-2008, John Mettraux, jmettraux@gmail.com
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#++
#
#
# "made in Japan"
#
# John Mettraux at openwfe.org
#
#
# Modified algorithm to find the next time
# Copyright (c) 2011, Jeremy Wells, jemmyw@gmail.com
#
#
# Jeremy Wells
#
#
# A 'cron line' is a line in the sense of a crontab
# (man 5 crontab) file line.
#
class Cronline
#
# The string used for creating this cronline instance.
#
attr_reader :original
attr_reader :seconds, :minutes, :hours, :days, :months, :weekdays
def initialize (line)
super()
@original = line
items = line.split
unless [ 5, 6 ].include?(items.length)
raise "cron '#{line}' string should hold 5 or 6 items, " + "not #{items.length}"
end
offset = items.length - 5
@seconds = if offset == 1
parse_item(items[0], 0, 59)
else
[ 0 ]
end
@minutes = parse_item(items[0+offset], 0, 59)
@hours = parse_item(items[1+offset], 0, 23)
@days = parse_item(items[2+offset], 1, 31)
@months = parse_item(items[3+offset], 1, 12)
@weekdays = parse_weekdays(items[4+offset])
end
#
# Returns true if the given time matches this cron line.
#
# (the precision is passed as well to determine if it's
# worth checking seconds and minutes)
#
def matches? (time)
#def matches? (time, precision)
time = Time.at(time) unless time.kind_of?(Time)
return false if no_match?(time.sec, @seconds)
#if precision <= 1 and no_match?(time.sec, @seconds)
return false if no_match?(time.min, @minutes)
#if precision <= 60 and no_match?(time.min, @minutes)
return false if no_match?(time.hour, @hours)
return false if no_match?(time.day, @days)
return false if no_match?(time.month, @months)
return false if no_match?(time.wday, @weekdays)
true
end
#
# Returns an array of 6 arrays (seconds, minutes, hours, days,
# months, weekdays).
# This method is used by the cronline unit tests.
#
def to_array
[ @seconds, @minutes, @hours, @days, @months, @weekdays ]
end
#
# Returns the next time that this cron line is supposed to 'fire'
#
def next_time(now = Time.now)
crontime = CronTime.new(to_array, now)
crontime.next_time
end
private
class MutableTime
PARTS = [:sec, :min, :hour, :day, :month, :year]
MINIMUMS = { :sec => 0, :min => 0, :hour => 0, :day => 1, :month => 1, :year => 0 }
MAXIMUMS = { :sec => 59, :min => 59, :hour => 23, :month => 12, :year => 9999 }
DAYS_IN_MONTHS = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ]
attr_accessor *PARTS
def [](index)
self.send(PARTS[index])
end
def []=(index, value)
self.send("#{PARTS[index]}=", value)
end
def initialize(time)
self.sec = time.sec
self.min = time.min
self.hour = time.hour
self.day = time.day
self.month = time.month
self.year = time.year
end
def to_time
Time.mktime(year, month, day, hour, min, sec)
end
MINIMUMS.each do |name,value|
define_method "min_#{name}" do
MINIMUMS[name]
end
end
MAXIMUMS.each do |name, value|
define_method "max_#{name}" do
MAXIMUMS[name]
end
end
PARTS.each do |name|
class_eval %Q{
def valid_#{name}?(value)
value >= min_#{name} && value <= max_#{name}
end
}
end
def valid_year?(value)
true
end
def valid?
PARTS.all? do |part|
send("valid_#{part}?", send(part))
end
end
def max_day
month == 2 ? (leap? ? 29 : 28) : DAYS_IN_MONTHS[month-1]
end
def leap?
year % 4 == 0 && year % 100 != 0 || year % 400 == 0
end
end
class CronTime
# How this gets the next cron time:
#
# Example:
# 0 1 * * * / run at 1am every day
#
# Find the anchor points: indexes 0 (seconds), 1 (minutes), 2 (hours)
#
# Current time: 30-Aug-2011 21:12:19
# s m h d m y
# 19 12 21 30 08 2011
# increment first anchor point to next available number:
# 19 -> ?
# Can't, so set it to the first available number and call increment on the next index
# 19 -> 0
# 00 12 21 30 08 2011
# 12 -> ?
# Can't so set it to the first available number and call increment on the next index
# 12 -> 0
# 00 00 21 30 08 2011
# 21 -> ?
# Can't so set it to the first available number and call increment on the next index
# 21 -> 1
# 00 00 01 30 08 2011
# 30 -> 31
# Now reset all lower indices to first avilable or minimum number
# 01 -> 01
# 00 -> 00
# 00 00 01 31 08 2011
# Finish
# 31-Aug-2011 01:00:00
#
PARTS = [:seconds, :minutes, :hours, :days, :months, :years]
def initialize(cron, time = Time.now)
@time = time
# treat weekdays differently, remove them from the cron list
@weekdays = cron.last
@cron = cron[0..-2]
# any cron part that isn't * is an anchor. There has to be at least one, and cronline
# provides the seconds as the anchor for normal minute cronlines
@anchors = @cron.enum_with_index.map{|cr,index| cr && cr.any? ? index : nil}.compact
@ary = MutableTime.new(time)
end
def next_time
until valid? # This outer loop is really only here for weekdays because they don't have a mutable value
@anchors.each do |anchor|
break if valid?
increment_anchor(anchor)
end
end
@ary.to_time
end
private
def increment(index)
if @anchors.include?(index)
increment_anchor(index)
else
part = MutableTime::PARTS[index]
value = @ary.send(part) + 1
if value > @ary.send("max_#{part}")
@ary[index] = @ary.send("min_#{part}")
increment(index+1)
else
@ary[index] = value
0.upto(index-1){|n| set_to_min(n) } if index > 0
end
end
end
def set_to_min(index)
if @anchors.include?(index)
@ary[index] = get_first_anchor_value(@ary[index], index)
else
@ary[index] = @ary.send("min_#{MutableTime::PARTS[index]}")
end
end
def get_next_anchor_value(value, index)
@cron[index].detect{|v| v > value }
end
def get_first_anchor_value(value, index)
@cron[index].first
end
def increment_anchor(index)
value = @ary[index]
if value = get_next_anchor_value(value, index)
@ary[index] = value
else
@ary[index] = get_first_anchor_value(value, index)
increment(index+1)
end
end
def valid?
time = @ary.to_time
@ary.valid? &&
time > @time &&
(!@weekdays || @weekdays.include?(time.wday)) &&
MutableTime::PARTS.enum_with_index.all? do |part, index|
(@cron[index].blank? || @cron[index].include?(@ary.send(part)))
end
end
end
WDS = [ "sun", "mon", "tue", "wed", "thu", "fri", "sat" ]
#
# used by parse_weekday()
def parse_weekdays (item)
item = item.downcase
WDS.each_with_index do |day, index|
item = item.gsub day, "#{index}"
end
r = parse_item item, 0, 7
return r unless r.is_a?(Array)
r.collect { |e| e == 7 ? 0 : e }.uniq
end
def parse_item (item, min, max)
return nil if item == "*"
return parse_list(item, min, max) if item.index(",")
return parse_range(item, min, max) if item.index("*") or item.index("-")
parse_number(item, min, max)
end
def parse_number(item, min, max)
i = Integer(item)
i = min if i < min
i = max if i > max
[i]
end
def parse_list (item, min, max)
items = item.split(",")
items.inject([]) { |r, i| r.push(parse_range(i, min, max)) }.flatten
end
def parse_range (item, min, max)
i = item.index("-")
j = item.index("/")
return item.to_i if (not i and not j)
inc = 1
inc = Integer(item[j+1..-1]) if j
istart = -1
iend = -1
if i
istart = Integer(item[0..i-1])
if j
iend = Integer(item[i+1..j])
else
iend = Integer(item[i+1..-1])
end
else # case */x
istart = min
iend = max
end
istart = min if istart < min
iend = max if iend > max
result = []
value = istart
if iend < istart
result = ((istart..max).to_a + (min..iend).to_a).sort
else
loop do
result << value
value = value + inc
break if value > iend
end
end
result
end
def no_match? (value, cron_values)
return false if not cron_values
cron_values.each do |v|
return false if value == v # ok, it matches
end
true # no match found
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment