class Fugit::Cron
Constants
- FREQUENCY_CACHE
- MAXDAYS
- MAX_ITERATION_COUNT
- SLOTS
- SPECIALS
Attributes
hours[R]
minutes[R]
monthdays[R]
months[R]
original[R]
seconds[R]
timezone[R]
weekdays[R]
zone[R]
Public Class Methods
do_parse(s, opts={})
click to toggle source
# File lib/fugit/cron.rb, line 54 def do_parse(s, opts={}) parse(s, opts) || fail(ArgumentError.new("invalid cron string #{trunc(s)}")) end
new(original)
click to toggle source
# File lib/fugit/cron.rb, line 27 def new(original) parse(original) end
parse(s, opts={})
click to toggle source
# File lib/fugit/cron.rb, line 32 def parse(s, opts={}) return s if s.is_a?(self) return nil unless s.is_a?(String) s0 = s s = s.strip s = if s[0, 1] == '@' ss = s.split(/\s+/, 2) [ SPECIALS[ss[0]] || ss, *ss[1..-1] ].join(' ') else s end #p s; Raabro.pp(Parser.parse(s, debug: 3), colors: true) h = Parser.parse(s) self.allocate.send(:init, s0, h, opts) end
Protected Class Methods
trunc(s)
click to toggle source
# File lib/fugit/cron.rb, line 62 def trunc(s) if s.is_a?(String) r = s.length > 28 ? s[0, 28] + "... len #{s.length}" : s r.inspect else r = s.inspect r.length > 35 ? s[0, 35] + '...' : r end end
Public Instance Methods
==(o)
click to toggle source
# File lib/fugit/cron.rb, line 506 def ==(o) o.is_a?(::Fugit::Cron) && o.to_a == to_a end
Also aliased as: eql?
brute_frequency(year=2017)
click to toggle source
Mostly used as a next_time sanity check. Avoid for “business” use, it's slow.
2017 is a non leap year (though it is preceded by a leap second on 2016-12-31)
Nota bene: cron with seconds are not supported.
# File lib/fugit/cron.rb, line 404 def brute_frequency(year=2017) FREQUENCY_CACHE["#{to_cron_s}|#{year}"] ||= begin deltas = [] t = EtOrbi.make_time("#{year}-01-01") - 1 t0 = nil t1 = nil loop do t1 = next_time(t) deltas << (t1 - t).to_i if t0 t0 ||= t1 break if deltas.any? && t1.year > year break if t1.year - t0.year > 7 t = t1 end Frequency.new(deltas, t1 - t0) end end
day_match?(nt)
click to toggle source
# File lib/fugit/cron.rb, line 215 def day_match?(nt) if @weekdays && @monthdays return weekday_match?(nt) && monthday_match?(nt) \ if @day_and # # extension for fugit, gh-78 return weekday_match?(nt) || monthday_match?(nt) # # From `man 5 crontab` # # Note: The day of a command's execution can be specified # by two fields -- day of month, and day of week. # If both fields are restricted (ie, are not *), the command will be # run when either field matches the current time. # For example, ``30 4 1,15 * 5'' would cause a command to be run # at 4:30 am on the 1st and 15th of each month, plus every Friday. # # as seen in gh-5 and gh-35 end return false unless weekday_match?(nt) return false unless monthday_match?(nt) true end
hash()
click to toggle source
# File lib/fugit/cron.rb, line 512 def hash to_a.hash end
hour_match?(nt)
click to toggle source
# File lib/fugit/cron.rb, line 172 def hour_match?(nt); ( ! @hours) || @hours.include?(nt.hour); end
match?(t)
click to toggle source
# File lib/fugit/cron.rb, line 245 def match?(t) t = Fugit.do_parse_at(t).translate(@timezone) month_match?(t) && day_match?(t) && hour_match?(t) && min_match?(t) && sec_match?(t) end
min_match?(nt)
click to toggle source
# File lib/fugit/cron.rb, line 173 def min_match?(nt); ( ! @minutes) || @minutes.include?(nt.min); end
month_match?(nt)
click to toggle source
# File lib/fugit/cron.rb, line 171 def month_match?(nt); ( ! @months) || @months.include?(nt.month); end
monthday_match?(nt)
click to toggle source
# File lib/fugit/cron.rb, line 204 def monthday_match?(nt) return true if @monthdays.nil? last = (TimeCursor.new(self, nt).inc_month.time - 24 * 3600).day + 1 @monthdays .collect { |d| d < 1 ? last + d : d } .include?(nt.day) end
next(from=::EtOrbi::EoTime.now)
click to toggle source
Returns an ::Enumerable instance that yields each “next time” in succession
# File lib/fugit/cron.rb, line 369 def next(from=::EtOrbi::EoTime.now) CronIterator.new(self, :next_time, from) end
next_time(from=::EtOrbi::EoTime.now)
click to toggle source
See gh-15 and tst/iteration_count.rb
Initially set to 1024 after seeing the worst case for next_time at 167 iterations, I placed it at 2048 after experimenting with gh-18 and noticing some > 1024 for some experiments. 2048 should be ok.
# File lib/fugit/cron.rb, line 262 def next_time(from=::EtOrbi::EoTime.now) from = ::EtOrbi.make_time(from) sfrom = from.strftime('%F|%T') ifrom = from.to_i i = 0 t = TimeCursor.new(self, from.translate(@timezone)) # # the translation occurs in the timezone of # this Fugit::Cron instance zfrom = t.time.strftime('%z|%Z') loop do fail RuntimeError.new( "too many loops for #{@original.inspect} #next_time, breaking, " + "cron expression most likely invalid (Feb 30th like?), " + "please fill an issue at https://git.io/fjJC9" ) if (i += 1) > MAX_ITERATION_COUNT #tt = t.time; #puts " #{tt.strftime('%F %T %:z %A')} #{tt.rweek} #{tt.rweek % 2}" (ifrom == t.to_i) && (t.inc(1); next) month_match?(t) || (t.inc_month; next) day_match?(t) || (t.inc_day; next) hour_match?(t) || (t.inc_hour; next) min_match?(t) || (t.inc_min; next) sec_match?(t) || (t.inc_sec; next) tt = t.time st = tt.strftime('%F|%T') zt = tt.strftime('%z|%Z') # if st == sfrom && zt != zfrom from, sfrom, zfrom, ifrom = tt, st, zt, t.to_i next end # # when transitioning out of DST, this prevents #next_time from # yielding the same literal time twice in a row, see gh-6 break end t.time.translate(from.zone) # # the answer time is in the same timezone as the `from` # starting point end
prev(from=::EtOrbi::EoTime.now)
click to toggle source
Returns an ::Enumerable instance that yields each “previous time” in succession
# File lib/fugit/cron.rb, line 377 def prev(from=::EtOrbi::EoTime.now) CronIterator.new(self, :previous_time, from) end
previous_time(from=::EtOrbi::EoTime.now)
click to toggle source
# File lib/fugit/cron.rb, line 314 def previous_time(from=::EtOrbi::EoTime.now) from = ::EtOrbi.make_time(from) i = 0 t = TimeCursor.new(self, (from - 1).translate(@timezone)) loop do fail RuntimeError.new( "too many loops for #{@original.inspect} #previous_time, breaking, " + "cron expression most likely invalid (Feb 30th like?), " + "please fill an issue at https://git.io/fjJCQ" ) if (i += 1) > MAX_ITERATION_COUNT #tt = t.time; #puts " #{tt.strftime('%F %T %:z %A')} #{tt.rweek} #{tt.rweek % 4}" month_match?(t) || (t.dec_month; next) day_match?(t) || (t.dec_day; next) hour_match?(t) || (t.dec_hour; next) min_match?(t) || (t.dec_min; next) sec_match?(t) || (t.dec_sec; next) break end t.time.translate(from.zone) end
rough_frequency()
click to toggle source
# File lib/fugit/cron.rb, line 434 def rough_frequency slots = SLOTS .collect { |k, v0, v1| a = (k == :days) ? rough_days : instance_variable_get("@#{k}") [ k, v0, v1, a ] } slots.each do |k, v0, _, a| next if a == [ 0 ] break if a != nil return v0 if a == nil end slots.each do |k, v0, v1, a| next unless a && a.length > 1 return (a + [ a.first + v1 ]) .each_cons(2) .collect { |a0, a1| a1 - a0 } .select { |d| d > 0 } # weed out zero deltas .min * v0 end slots.reverse.each do |k, v0, v1, a| return v0 * v1 if a && a.length == 1 end 1 # second end
sec_match?(nt)
click to toggle source
# File lib/fugit/cron.rb, line 174 def sec_match?(nt); ( ! @seconds) || @seconds.include?(nt.sec); end
to_a()
click to toggle source
# File lib/fugit/cron.rb, line 491 def to_a [ @seconds, @minutes, @hours, @monthdays, @months, @weekdays ] end
to_cron_s()
click to toggle source
# File lib/fugit/cron.rb, line 74 def to_cron_s @cron_s ||= [ @seconds == [ 0 ] ? nil : (@seconds || [ '*' ]).join(','), (@minutes || [ '*' ]).join(','), (@hours || [ '*' ]).join(','), (@monthdays || [ '*' ]).join(','), (@months || [ '*' ]).join(','), weekdays_to_cron_s, @timezone ? @timezone.name : nil ].compact.join(' ') end
to_h()
click to toggle source
# File lib/fugit/cron.rb, line 496 def to_h { seconds: @seconds, minutes: @minutes, hours: @hours, monthdays: @monthdays, months: @months, weekdays: @weekdays } end
weekday_hash_match?(nt, hsh)
click to toggle source
# File lib/fugit/cron.rb, line 176 def weekday_hash_match?(nt, hsh) return false unless hsh.is_a?(Integer) phsh, nhsh = nt.wday_in_month # # positive wday, from the beginning of the month # negative wday, from the end of the month, -1 == last (hsh == phsh) || (hsh == nhsh) end
weekday_match?(nt)
click to toggle source
# File lib/fugit/cron.rb, line 194 def weekday_match?(nt) @weekdays.nil? || @weekdays.find { |wd, hom| (nt.wday == wd) && (hom.nil? || weekday_modulo_match?(nt, hom) || weekday_hash_match?(nt, hom)) } end
weekday_modulo_match?(nt, mod)
click to toggle source
# File lib/fugit/cron.rb, line 188 def weekday_modulo_match?(nt, mod) mod.is_a?(Array) && ((nt.rweek % mod[0]) == (mod[1] % mod[0])) end
within(time_range, time_end=nil)
click to toggle source
Returns an array of EtOrbi::EoTime instances that correspond to the occurrences of the cron within the given time range
# File lib/fugit/cron.rb, line 385 def within(time_range, time_end=nil) sta, ned = time_range.is_a?(::Range) ? [ time_range.begin, time_range.end ] : [ ::EtOrbi.make_time(time_range), ::EtOrbi.make_time(time_end) ] CronIterator .new(self, :next_time, sta) .take_while { |eot| eot.to_t < ned } end
Protected Instance Methods
compact_month_days()
click to toggle source
# File lib/fugit/cron.rb, line 519 def compact_month_days return true if @months == nil || @monthdays == nil ms, ds = @months.inject([ [], [] ]) { |a, m| @monthdays.each { |d| next if d > MAXDAYS[m] a[0] << m; a[1] << d } a } @months = ms.uniq @monthdays = ds.uniq @months.any? && @monthdays.any? end
determine_hours(arr, opts)
click to toggle source
# File lib/fugit/cron.rb, line 698 def determine_hours(arr, opts) (@hours = do_determine(:hours, arr, 0, 23, opts)) != false end
determine_minutes(arr, opts)
click to toggle source
# File lib/fugit/cron.rb, line 694 def determine_minutes(arr, opts) (@minutes = do_determine(:minutes, arr, 0, 59, opts)) != false end
determine_monthdays(arr, opts)
click to toggle source
# File lib/fugit/cron.rb, line 702 def determine_monthdays(arr, opts) (@monthdays = do_determine(:monthdays, arr, 1, 31, opts)) != false end
determine_months(arr, opts)
click to toggle source
# File lib/fugit/cron.rb, line 706 def determine_months(arr, opts) (@months = do_determine(:months, arr, 1, 12, opts)) != false end
determine_seconds(arr, opts)
click to toggle source
# File lib/fugit/cron.rb, line 690 def determine_seconds(arr, opts) (@seconds = do_determine(:seconds, arr || [ 0 ], 0, 59, opts)) != false end
determine_timezone(z)
click to toggle source
# File lib/fugit/cron.rb, line 745 def determine_timezone(z) @zone, @timezone = z true end
determine_weekdays(arr, opts)
click to toggle source
# File lib/fugit/cron.rb, line 710 def determine_weekdays(arr, opts) @weekdays = [] arr.each do |a, z, sp, sl, ha, mo| # a to z, seperator, slash, hash, and mod if sp == :tilde a = random(0, 6, a, z, sl, opts) z = nil unless sl end if ha || mo @weekdays << [ a, ha || mo ] elsif sl z ||= (a ? a : 6) a ||= 0 z = z + 7 if a > z (a..z).step(sl < 1 ? 1 : sl) .each { |i| @weekdays << [ (i > 6) ? i - 7 : i ] } elsif z z = z + 7 if a > z (a..z).each { |i| @weekdays << [ (i > 6) ? i - 7 : i ] } elsif a @weekdays << [ a ] #else end end @weekdays.each { |wd| wd[0] = 0 if wd[0] == 7 } # turn sun7 into sun0 @weekdays.uniq! @weekdays.sort! @weekdays = nil if @weekdays.empty? true end
do_determine(key, arr, min, max, opts)
click to toggle source
# File lib/fugit/cron.rb, line 673 def do_determine(key, arr, min, max, opts) null = false r = arr .collect { |v| expand(min, max, opts, v) } .flatten(1) .collect { |e| return false if e == false null = null || e == nil (key == :hours && e == 24) ? 0 : e } return nil if null r.uniq.sort end
expand(min, max, opts, r)
click to toggle source
# File lib/fugit/cron.rb, line 585 def expand(min, max, opts, r) sta, edn, sep, sla = r #return false if sla && sla > max # # let it go, "* */24 * * *" and "* */27 * * *" are okay # gh-86 and gh-103 return false if sla && sla < 1 if sep == :tilde sta = random(min, max, sta, edn, sla, opts) edn = nil unless sla end edn = max if sla && edn.nil? return nil if sta.nil? && edn.nil? && sla.nil? return sta if sta && edn.nil? sla = 1 if sla == nil sta = min if sta == nil edn = max if edn == nil || edn < 0 && sta > 0 range(min, max, sta, edn, sla) end
init(original, h, opts)
click to toggle source
# File lib/fugit/cron.rb, line 562 def init(original, h, opts) return nil unless h @original = original @cron_s = nil # just to be sure @day_and = h[:&] valid = determine_seconds(h[:sec], opts) && determine_minutes(h[:min], opts) && determine_hours(h[:hou], opts) && determine_monthdays(h[:dom], opts) && determine_months(h[:mon], opts) && determine_weekdays(h[:dow], opts) && determine_timezone(h[:tz]) return nil unless valid return nil unless compact_month_days self end
random(min, max, sta, edn, sla, opts)
click to toggle source
# File lib/fugit/cron.rb, line 613 def random(min, max, sta, edn, sla, opts) sta = min if sta.nil? edn = max if edn.nil? random = opts.fetch(:random, true) random = Random if random == true return sta unless random n = edn - sta + 1 n = max - min + 1 + n if n <= 0 n = sla if sla && sla < n r = sta + random.rand(n) r = r + min - max - 1 if r > max r end
range(min, max, sta, edn, sla)
click to toggle source
# File lib/fugit/cron.rb, line 631 def range(min, max, sta, edn, sla) return [ nil ] if sta == min && edn == max && sla == 1 fail ArgumentError.new( 'both start and end must be negative in ' + { min: min, max: max, sta: sta, edn: edn, sla: sla }.inspect ) if (sta < 0 && edn > 0) || (edn < 0 && sta > 0) a = [] omin, omax = min, max min, max = -max, -1 if sta < 0 cur = sta loop do a << cur break if cur == edn cur += 1 if cur > max cur = min edn = edn - max - 1 if edn > max end fail RuntimeError.new( "too many loops for " + { min: omin, max: omax, sta: sta, edn: edn, sla: sla }.inspect + " #range, breaking, " + "please fill an issue at https://git.io/fjJC9" ) if a.length > 2 * omax # there is a #uniq afterwards, hence the 2* for 0-24 and friends end a.each_with_index .select { |e, i| i % sla == 0 } .collect(&:first) .uniq end
rough_days()
click to toggle source
# File lib/fugit/cron.rb, line 535 def rough_days return nil if @weekdays == nil && @monthdays == nil months = (@months || (1..12).to_a) monthdays = months .product(@monthdays || []) .collect { |m, d| d = 31 + d if d < 0 (m - 1) * 30 + d } # rough weekdays = (@weekdays || []) .collect { |d, w| w ? d + (w - 1) * 7 : (0..3).collect { |ww| d + ww * 7 } } .flatten weekdays = months .product(weekdays) .collect { |m, d| (m - 1) * 30 + d } # rough (monthdays + weekdays).sort end
weekdays_to_cron_s()
click to toggle source
# File lib/fugit/cron.rb, line 752 def weekdays_to_cron_s return '*' unless @weekdays @weekdays .collect { |a| if a.length == 1 a[0].to_s elsif a[1].is_a?(Array) a11 = a[1][1] off = (a11 < 0) ? a11.to_s : (a11 > 0) ? "+#{a11}" : '' "#{a[0]}%#{a[1][0]}" + off else a.collect(&:to_s).join('#') end } .join(',') + (@day_and ? '&' : '') end