#-- # Copyright (c) 2006-2009, 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. #++ module Rufus # # 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, 24) @days = parse_item(items[2+offset], 1, 31) @months = parse_item(items[3+offset], 1, 12) @weekdays = parse_weekdays(items[4+offset]) #adjust_arrays() 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 unless sub_match? time.sec, @seconds return false unless sub_match? time.min, @minutes return false unless sub_match? time.hour, @hours return false unless sub_match? time.day, @days return false unless sub_match? time.month, @months return false unless sub_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' # # This is raw, 3 secs to iterate over 1 year on my macbook :( brutal. # # This method accepts an optional Time parameter. It's the starting point # for the 'search'. By default, it's Time.now # # Note that the time instance returned will be in the same time zone that # the given start point Time (thus a result in the local time zone will # be passed if no start time is specified (search start time set to # Time.now)) # # >> Rufus::CronLine.new('30 7 * * *').next_time( Time.mktime(2008,10,24,7,29) ) # => Fri Oct 24 07:30:00 -0500 2008 # # >> Rufus::CronLine.new('30 7 * * *').next_time( Time.utc(2008,10,24,7,29) ) # => Fri Oct 24 07:30:00 UTC 2008 # # >> Rufus::CronLine.new('30 7 * * *').next_time( Time.utc(2008,10,24,7,29) ).localtime # => Fri Oct 24 02:30:00 -0500 2008 # # (Thanks to K Liu for the note and the examples) # def next_time time=Time.now time -= time.usec * 1e-6 time += 1 loop do unless date_match? time time += (24 - time.hour) * 3600 - time.min * 60 - time.sec next end unless sub_match? time.hour, @hours time += (60 - time.min) * 60 - time.sec next end unless sub_match? time.min, @minutes time += 60 - time.sec next end unless sub_match? time.sec, @seconds time += 1 next end break end time end private #-- # adjust values to Ruby # #def adjust_arrays() # @hours = @hours.collect { |h| # if h == 24 # 0 # else # h # end # } if @hours # @weekdays = @weekdays.collect { |wd| # wd - 1 # } if @weekdays #end # # dead code, keeping it as a reminder #++ WDS = %w[ 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("-") 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 loop do result << value value = value + inc break if value > iend end result end def sub_match? value, values values.nil? || values.include?(value) end def date_match? date return false unless sub_match? date.day, @days return false unless sub_match? date.month, @months return false unless sub_match? date.wday, @weekdays true end end end