class Fugit::Cron
def ==(o)
def ==(o) o.is_a?(::Fugit::Cron) && o.to_a == to_a end
def brute_frequency(year=2017)
Nota bene: cron with seconds are not supported.
a leap second on 2016-12-31)
2017 is a non leap year (though it is preceded by
Avoid for "business" use, it's slow.
Mostly used as a #next_time sanity check.
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
def compact(key)
def compact(key) arr = instance_variable_get(key) return instance_variable_set(key, nil) if arr.include?(nil) # reductio ad astrum arr.uniq! arr.sort! end
def day_match?(nt)
def day_match?(nt) return weekday_match?(nt) || monthday_match?(nt) \ if @weekdays && @monthdays return false unless weekday_match?(nt) return false unless monthday_match?(nt) true end
def determine_hours(arr)
def determine_hours(arr) @hours = arr .inject([]) { |a, h| a.concat(expand(0, 23, h)) } .collect { |h| h == 24 ? 0 : h } compact(:@hours) end
def determine_minutes(arr)
def determine_minutes(arr) @minutes = arr.inject([]) { |a, m| a.concat(expand(0, 59, m)) } compact(:@minutes) end
def determine_monthdays(arr)
def determine_monthdays(arr) @monthdays = arr.inject([]) { |a, d| a.concat(expand(1, 31, d)) } compact(:@monthdays) end
def determine_months(arr)
def determine_months(arr) @months = arr.inject([]) { |a, m| a.concat(expand(1, 12, m)) } compact(:@months) end
def determine_seconds(arr)
def determine_seconds(arr) @seconds = (arr || [ 0 ]).inject([]) { |a, s| a.concat(expand(0, 59, s)) } compact(:@seconds) end
def determine_timezone(z)
def determine_timezone(z) @zone, @timezone = z end
def determine_weekdays(arr)
def determine_weekdays(arr) @weekdays = [] arr.each do |a, z, s, h| # a to z, slash and hash if h @weekdays << [ a, h ] elsif s ((a || 0)..(z || (a ? a : 6))).step(s < 1 ? 1 : s) .each { |i| @weekdays << [ i ] } elsif z (a..z).each { |i| @weekdays << [ 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 = nil if @weekdays.empty? end
def do_parse(s)
def do_parse(s) parse(s) || fail(ArgumentError.new("invalid cron string #{s.inspect}")) end
def expand(min, max, r)
def expand(min, max, r) sta, edn, sla = r sla = nil if sla == 1 # don't get fooled by /1 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 sta, edn = edn, sta if sta > edn (sta..edn).step(sla).to_a end
def hash
def hash to_a.hash end
def hour_match?(nt); ( ! @hours) || @hours.include?(nt.hour); end
def hour_match?(nt); ( ! @hours) || @hours.include?(nt.hour); end
def init(original, h)
def init(original, h) @original = original @cron_s = nil # just to be sure determine_seconds(h[:sec]) determine_minutes(h[:min]) determine_hours(h[:hou]) determine_monthdays(h[:dom]) determine_months(h[:mon]) determine_weekdays(h[:dow]) determine_timezone(h[:tz]) self end
def match?(t)
def match?(t) t = Fugit.do_parse_at(t) month_match?(t) && day_match?(t) && hour_match?(t) && min_match?(t) && sec_match?(t) end
def min_match?(nt); ( ! @minutes) || @minutes.include?(nt.min); end
def min_match?(nt); ( ! @minutes) || @minutes.include?(nt.min); end
def month_match?(nt); ( ! @months) || @months.include?(nt.month); end
def month_match?(nt); ( ! @months) || @months.include?(nt.month); end
def monthday_match?(nt)
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
def new(original)
def new(original) parse(original) end
def next_time(from=::EtOrbi::EoTime.now)
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 loop do fail RuntimeError.new( "too many loops for #{@original.inspect} #next_time, breaking, " + "please fill an issue at https://git.io/fjJC9" ) if (i += 1) > MAX_ITERATION_COUNT (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) st = t.time.strftime('%F|%T') (from, sfrom, ifrom = t.time, st, t.to_i; next) if st == sfrom # # 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
def parse(s)
def parse(s) return s if s.is_a?(self) s = SPECIALS[s] || s return nil unless s.is_a?(String) Raabro.pp(Parser.parse(s, debug: 3), colors: true) h = Parser.parse(s) return nil unless h self.allocate.send(:init, s, h) end
def previous_time(from=::EtOrbi::EoTime.now)
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, " + "please fill an issue at https://git.io/fjJCQ" ) if (i += 1) > MAX_ITERATION_COUNT 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
def rough_days
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
def rough_frequency
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 } .min * v0 end slots.reverse.each do |k, v0, v1, a| return v0 * v1 if a && a.length == 1 end 1 # second end
def sec_match?(nt); ( ! @seconds) || @seconds.include?(nt.sec); end
def sec_match?(nt); ( ! @seconds) || @seconds.include?(nt.sec); end
def to_a
def to_a [ @seconds, @minutes, @hours, @monthdays, @months, @weekdays ] end
def to_cron_s
def to_cron_s @cron_s ||= begin [ @seconds == [ 0 ] ? nil : (@seconds || [ '*' ]).join(','), (@minutes || [ '*' ]).join(','), (@hours || [ '*' ]).join(','), (@monthdays || [ '*' ]).join(','), (@months || [ '*' ]).join(','), (@weekdays || [ [ '*' ] ]).map { |d| d.compact.join('#') }.join(','), @timezone ? @timezone.name : nil ].compact.join(' ') end end
def to_h
def to_h { seconds: @seconds, minutes: @minutes, hours: @hours, monthdays: @monthdays, months: @months, weekdays: @weekdays } end
def weekday_match?(nt)
def weekday_match?(nt) return true if @weekdays.nil? wd, hsh = @weekdays.find { |d, _| d == nt.wday } return false unless wd return true if hsh.nil? phsh, nhsh = nt.wday_in_month if hsh > 0 hsh == phsh # positive wday, from the beginning of the month else hsh == nhsh # negative wday, from the end of the month, -1 == last end end