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