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