lib/rufus/sc/cronline.rb in rufus-scheduler-2.0.19 vs lib/rufus/sc/cronline.rb in rufus-scheduler-2.0.20

- old
+ new

@@ -31,10 +31,13 @@ # A 'cron line' is a line in the sense of a crontab # (man 5 crontab) file line. # class CronLine + DAY_S = 24 * 3600 + WEEK_S = 7 * DAY_S + # The string used for creating this cronline instance. # attr_reader :original attr_reader :seconds @@ -72,11 +75,11 @@ [ @seconds, @minutes, @hours, @months ].each do |es| raise ArgumentError.new( "invalid cronline: '#{line}'" - ) if es && es.find { |e| ! e.is_a?(Integer) } + ) if es && es.find { |e| ! e.is_a?(Fixnum) } end end # Returns true if the given time matches this cron line. # @@ -123,29 +126,25 @@ def next_time(now=Time.now) time = @timezone ? @timezone.utc_to_local(now.getutc) : now time = time - time.usec * 1e-6 + 1 - # little adjustment before starting + # small adjustment before starting loop do unless date_match?(time) - time += (24 - time.hour) * 3600 - time.min * 60 - time.sec - next + 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 + time += (60 - time.min) * 60 - time.sec; next end unless sub_match?(time, :min, @minutes) - time += 60 - time.sec - next + time += 60 - time.sec; next end unless sub_match?(time, :sec, @seconds) - time += 1 - next + time += 1; next end break end @@ -155,10 +154,32 @@ end time end + # Returns the previous the cronline matched. It's like next_time, but + # for the past. + # + def previous_time(now=Time.now) + + # looks back by slices of two hours, + # + # finds for '* * * * sun', '* * 13 * *' and '0 12 13 * *' + # starting 1970, 1, 1 in 1.8 to 2 seconds (says Rspec) + + start = current = now - 2 * 3600 + result = nil + + loop do + nex = next_time(current) + return (result ? result : previous_time(start)) if nex > now + result = current = nex + end + + # never reached + 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 @@ -188,19 +209,22 @@ weekdays = nil monthdays = nil items.each do |it| - if it.match(/#[12345]$/) + if m = it.match(/^(.+)#(l|-?[12345])$/) raise ArgumentError.new( "ranges are not supported for monthdays (#{it})" - ) if it.index('-') + ) if m[1].index('-') - (monthdays ||= []) << it + expr = it.gsub(/#l/, '#-1') + (monthdays ||= []) << expr + else + expr = it.dup WEEKDAYS.each_with_index { |a, i| expr.gsub!(/#{a}/, i.to_s) } raise ArgumentError.new( "invalid weekday expression (#{it})" @@ -219,113 +243,112 @@ end def parse_item(item, min, max) return nil if item == '*' - return [ 'L' ] if item == 'L' - return parse_list(item, min, max) if item.index(',') - return parse_range(item, min, max) if item.match(/[*-\/]/) - i = item.to_i + r = item.split(',').map { |i| parse_range(i.strip, min, max) }.flatten - i = min if i < min - i = max if i > max - - [ i ] - end - - def parse_list(item, min, max) - - l = item.split(',').collect { |i| parse_range(i, min, max) }.flatten - raise ArgumentError.new( "found duplicates in #{item.inspect}" - ) if l.uniq.size < l.size + ) if r.uniq.size < r.size - l + r end + RANGE_REGEX = /^(\*|\d{1,2})(?:-(\d{1,2}))?(?:\/(\d{1,2}))?$/ + def parse_range(item, min, max) - dash = item.index('-') - slash = item.index('/') + return %w[ L ] if item == 'L' - return parse_item(item, min, max) if (not slash) and (not dash) + m = item.match(RANGE_REGEX) raise ArgumentError.new( - "'L' (end of month) is not accepted in ranges, " + - "#{item.inspect} is not valid" - ) if item.index('L') + "cannot parse #{item.inspect}" + ) unless m - inc = slash ? item[slash + 1..-1].to_i : 1 + sta = m[1] + sta = sta == '*' ? min : sta.to_i - istart = -1 - iend = -1 + edn = m[2] + edn = edn ? edn.to_i : sta + edn = max if m[1] == '*' - if dash + inc = m[3] + inc = inc ? inc.to_i : 1 - istart = item[0..dash - 1].to_i - iend = (slash ? item[dash + 1..slash - 1] : item[dash + 1..-1]).to_i + raise ArgumentError.new( + "#{item.inspect} is not in range #{min}..#{max}" + ) if sta < min or edn > max - else # case */x + r = [] + val = sta - 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 + v = val + v = 0 if max == 24 && v == 24 + r << v + break if inc == 1 && val == edn + val += inc + break if inc > 1 && val > edn + val = min if val > max end - result + r.uniq end - def sub_match?(time, accessor, values=:none) + def sub_match?(time, accessor, values) - value, values = - if values == :none - [ time, accessor ] - else - [ time.send(accessor), values ] - end + value = time.send(accessor) return true if values.nil? - return true if values.include?('L') && (time + 24 * 3600).day == 1 + return true if values.include?('L') && (time + DAY_S).day == 1 + return true if value == 0 && accessor == :hour && values.include?(24) + values.include?(value) end + def monthday_match?(date, values) + + return true if values.nil? + + today_values = monthdays(date) + + (today_values & values).any? + 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) - return false unless sub_match?(CronLine.monthday(date), @monthdays) + return false unless monthday_match?(date, @monthdays) true end - DAY_IN_SECONDS = 7 * 24 * 3600 + def monthdays(date) - def self.monthday(date) + pos = 1 + d = date.dup - count = 1 - date2 = date.dup + loop do + d = d - WEEK_S + break if d.month != date.month + pos = pos + 1 + end + neg = -1 + d = date.dup + loop do - date2 = date2 - DAY_IN_SECONDS - break if date2.month != date.month - count = count + 1 + d = d + WEEK_S + break if d.month != date.month + neg = neg - 1 end - "#{WEEKDAYS[date.wday]}##{count}" + [ "#{WEEKDAYS[date.wday]}##{pos}", "#{WEEKDAYS[date.wday]}##{neg}" ] end end end