class Cron2English::Parser

Constants

ORDINATIONS

Public Class Methods

new() click to toggle source
# File lib/cron2english/all.rb, line 37
def initialize
  @dow = nil
  @month = nil
  @dow2num = {}
  @month2num = {}
  @num2dow = {}
  @num2month = {}
end

Public Instance Methods

parse(str) click to toggle source
# File lib/cron2english/all.rb, line 46
def parse(str)
  str = str.strip

  if str =~ /^@(\w+)$/ and AT_WORDS[$1.downcase]
    process_vixie($1)
  else
    bits = str.split(/[ \t]+/)
    if bits.size == 5
      process_trad(*bits)
    else
      give_up(str)
    end
  end
end

Private Instance Methods

bits_to_english(bits) click to toggle source
# File lib/cron2english/all.rb, line 122
  def bits_to_english(bits)
    # This is the deep ugly scary guts of this program.
    # The older and eldritch among you might recognize this as sort of a
    # parody of bad old Lisp style of data-structure handling.
    time_lines = []


    #######################################################################
    # Render the minutes and hours
    if bits[0].size   == 1   and bits[1].size    == 1 and
      bits[0][0].size == 1   and bits[1][0].size == 1 and
      bits[0][0][0]   != '*' and bits[1][0][0]   != '*'
      # It's a highly simplifiable time expression!
      # This is a very common case.  Like "46 13" -> 1:46pm
      # Formally: when minute and hour are each a single number.

      h = bits[1][0][0]
      if bits[0][0][0] == 0
        # Simply at the top of the hour, so just call it by the hour name.
        time_lines << MIL2AMPM[h]

      else
        # Can't say "noon:02", so use an always-numeric time format:
        time_lines << "%s:%02d%s" % [
          (h > 12) ? (h - 12) : h,
          bits[0][0][0],
          (h >= 12) ? 'pm' : 'am'
        ]
      end
      time_lines[time_lines.size - 1] += ' on'

    else
      # It's not a highly simplifiable time expression

      # First, minutes:
      if bits[0][0][0] == '*'
        if bits[0][0].size == 1 or bits[0][0][1] == 1
          time_lines << 'every minute of'
        else
          time_lines << "every #{freq(bits[0][0][1])} minute of"
        end

      elsif bits[0].size == 1 and bits[0][0][0] == 0
        # It's just a '0'.  Ignore it -- instead of bothering
        # to add a "0 minutes past"

      elsif bits[0].none?{|x| x.size > 1}
        # It's all like 7,10,15. Conjoinable
        time_lines << conj_and(bits[0].map{|x| x[0]}) + (bits[0][-1][0] == 1 ? ' minute past' : ' minutes past')

      else
        # It's just gonna be long.
        hunks = []
        bits[0].each do |bit|
          if bit.size == 1  # "7"
            hunks << (bit[0] == 1 ? '1 minute' : "#{bit[0]} minutes")

          elsif bit.size == 2 # "7-9"
            hunks << ("from %d to %d %s" % [*bit, bit[1] == 1 ? 'minute' : 'minutes'])

          elsif bit.size == 3 # "7-20/2"
            hunks << ("every %d %s from %d to %d" % [bit[2],
                                                     bit[2] == 1 ? 'minute' : 'minutes',
                                                     bit[0],
                                                     bit[1]])
          end
        end
        time_lines << (conj_and(hunks) + " past")
      end

      # Now hours
      if bits[1][0][0] == '*'
        if bits[1][0].size == 1 or bits[1][0][1] == 1
          time_lines << 'every hour of'
        else
          time_lines << "every #{freq(bits[1][0][1])} hour of"
        end

      else
        hunks = []
        bits[1].each do |bit|
          if bit.size == 1 # "7"
            hunks << (MIL2AMPM[bit[0]] || "HOUR_#{bit[0]}??")

          elsif bit.size == 2 # "7-9"
            hunks << ("from %s to %s" % [MIL2AMPM[bit[0]] || "HOUR_#{bit[0]}??",
                                         MIL2AMPM[bit[1]] || "HOUR_#{bit[1]}??"])

          elsif bit.size == 3 # "7-20/2"
            hunks << ("every %d %s from %s to %s" % [bit[2],
                                                     bit[2] == 1 ? 'hour' : 'hours',
                                                     MIL2AMPM[bit[0]] || "HOUR_#{bit[0]}??",
                                                     MIL2AMPM[bit[1]] || "HOUR_#{bit[2]}??"])
          end
        end
        time_lines << (conj_and(hunks) + " of")
      end
    end
    # End of hours and minutes

    #######################################################################
    # Day-of-month

    if bits[2][0][0] == '*'
      time_lines[-1].gsub!(/ on$/, '')
      if bits[2][0].size == 1 or bits[2][0][1] == 1
        time_lines << 'every day of'
      else
        time_lines << "every #{freq(bits[2][0][1])} day of"
      end
    else
      hunks = []
      bits[2].each do |bit|
        if bit.size == 1  # "7"
          hunks << "the #{ordinate(bit[0])}"

        elsif bit.size == 2 # "7-9"
          hunks << ("from the %s to the %s" % [ordinate(bit[0]), ordinate(bit[1])])

        elsif bit.size == 3 # "7-20/2"
          hunks << ("every %d %s from the %s to the %s" % [bit[2],
                                                           bit[2] == 1 ? 'day' : 'days',
                                                           ordinate(bit[0]),
                                                           ordinate(bit[1])])
        end
      end

      # collapse the "the"s, if all the elements have one
      if hunks.size > 1 and hunks.none?{|h| h !~ /^the /}
        hunks = hunks.map{|h| h.gsub(/^the /, '')}
        hunks[0] = "the #{hunks[0]}"
      end

      time_lines << "#{conj_and(hunks)} of"
    end

    #######################################################################
    # Month

    if bits[3][0][0] == '*'
      if bits[3][0].size == 1 or bits[3][0][1] == 1
        time_lines << 'every month'
      else
        time_lines << "every #{freq(bits[3][0][1])} month"
      end
    else
      hunks = []
      bits[3].each do |bit|
        if bit.size == 1 # "7"
          hunks << (NUM2MONTH_LONG[bit[0]] || "MONTH_#{bit[0]}??")

        elsif bit.size == 2 # "7-9"
          hunks << ("from %s to %s" % [NUM2MONTH_LONG[bit[0]] || "MONTH_#{bit[0]}??",
                                       NUM2MONTH_LONG[bit[1]] || "MONTH_#{bit[1]}??"])

        elsif bit.size == 3 # "7-20/2"
          hunks << ("every %d %s from %s to %s" % [bit[2],
                                                   bit[2] == 1 ? 'month' : 'months',
                                                   NUM2MONTH_LONG[bit[0]] || "MONTH_#{bit[0]}??",
                                                   NUM2MONTH_LONG[bit[1]] || "MONTH_#{bit[1]}??"])
        end
      end

      time_lines << conj_and(hunks)
    end

    #######################################################################
    # Weekday
   #
  #
 #
#
# 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, aren't *), 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.
#
# [But if both fields ARE *, then it just means "every day".
#  and if one but not both are *, then ignore the *'d one --
#  so   "1 2 3 4 *" means just 2:01, April 3rd
#  and  "1 2 * 4 5" means just 2:01, on every Friday in April
#  But  "1 2 3 4 5" means 2:01 of every 3rd or Friday in April. ]
#
 #
  #
   #
    # And that's a bit tricky.

    if bits[4][0][0] == '*' and (bits[4][0].size == 1 or bits[4][0][1] == 1)
      # Most common case: any weekday. Do nothing really.
      #
      # Hmm, does "*/1" really many "*" here, given the above note?

      # Tidy things up while we're here:
      if time_lines[-2] == 'every day of' and
         time_lines[-1] == 'every month'
        time_lines[-2] == 'every day'
        time_lines.pop
      end

    else
      # Ugh, there's some restriction on weekdays.

      # Translate the DOW-expression
      expression = nil
      hunks = []
      bits[4].each do |bit|
        if bit.size == 1
          hunks << (NUM2DOW_LONG[bit[0]] || "DOW_#{bit[0]}??")

        elsif bit.size == 2
          if bit[0] == '*'  # It's like */3
            # hunks << ("every %s day of the week" % freq(bit[1]))
            # The above was ambiguous: "every third day of the week"
            # sounds synonymous with just "3"
            if bit[1] == 2
              # common and unambiguous case.
              hunks << "every other day of the week"
            else
              # rare cases: N > 2
              hunks << "every #{bit[1]} days of the week"
              # sounds clunky, but it's a clunky concept
            end
          
          else
            # It's like "7-9"
            hunks << ("%s through %s" % [NUM2DOW_LONG[bit[0]] || "DOW_#{bit[0]}??",
                                         NUM2DOW_LONG[bit[1]] || "DOW_#{bit[1]}??"])
          end

        elsif bit.size == 3 # "7-20/2"
          hunks << ("every %s %s from %s through %s" % [ordinate_soft(bit[2]),
                                                        'day',
                                                        NUM2DOW_LONG[bit[0]] || "DOW_#{bit[0]}??",
                                                        NUM2DOW_LONG[bit[1]] || "DOW_#{bit[1]}??"])
        end
      end
      expression = conj_or(hunks)

      # Now figure out where to put it. . . .

      if time_lines[-2] == 'every day of'
        # Unrestricted day-of-month, hooray.
        if time_lines[-1] == 'every month'
          # change it to "every Thursday", killing the "of every month"
          time_lines[-2] = "every #{expression}"
          time_lines[-2].gsub!(%r{every every }, 'every ')
          time_lines.pop
        else
          # change it to "every Thursday in"
          time_lines[-2] = "every #{expression} in"
          time_lines[2].gsub!(%r{every every }, 'every ')
        end
      else
        # This is the messy case where there's a DOM and DOW restriction

        time_lines[-2] += " -- or every #{expression} in --"
        # Yes, dashes look very strange, but then this is a very rare case.
        time_lines[-2].gsub!(%r{every every }, 'every ')
      end
    end
    time_lines[-1].sub!(/ of$/, '')
    return time_lines
  end
conj_and(bits) click to toggle source
# File lib/cron2english/all.rb, line 389
def conj_and(bits)
  if bits.grep(/every|from/).any?
    # put in semicolons in case of complex constituency
    return bits.join('; and ') if bits.size < 2
    last = bits.pop
    return "#{bits.join('; ')}; and #{last}"
  else
    return bits.join(' and ') if bits.size < 3
    last = bits.pop
    return "#{bits.join(', ')}, and #{last}"
  end
end
conj_or(bits) click to toggle source
# File lib/cron2english/all.rb, line 402
def conj_or(bits)
  if bits.grep(/every|from/).any?
    # put in semicolons in case of complex constituency
    return bits.join('; or ') if bits.size < 2
    last = bits.pop
    return "#{bits.join('; ')}; or #{last}"
  else
    return bits.join(' or ') if bits.size < 3
    last = bits.pop
    return "#{bits.join(', ')}, or #{last}"
  end
end
freq(n=0) click to toggle source
# File lib/cron2english/all.rb, line 435
def freq(n=0)
  # frequentive form. Like ordinal, except that 2 -> 'other'
  # (as in every other)
  return 'other' if n == 2
  ORDINATIONS[n] || "#{n}#{ordsuf(n)}"
end
give_up(str) click to toggle source
# File lib/cron2english/all.rb, line 446
def give_up(str)
  raise Cron2English::ParseException.new("Unparseable crontab spec: #{str}")
end
ordinate(n=0) click to toggle source
# File lib/cron2english/all.rb, line 431
def ordinate(n=0)
  ORDINATIONS[n] || "#{n}#{ordsuf(n)}"
end
ordinate_soft(n=0) click to toggle source
# File lib/cron2english/all.rb, line 442
def ordinate_soft(n=0)
  "#{n}#{ordsuf(n)}"
end
ordsuf(n=nil) click to toggle source
# File lib/cron2english/all.rb, line 417
def ordsuf(n=nil)
  return 'th' if not n or n.to_i == 0
  # 'th' for undef, 0, or anything non-number
  n = n.abs
  return 'th' unless n == n.to_i
  n %= 100
  return 'th' if n == 11 or n == 12 or n == 13
  n %= 10
  return 'st' if n == 1
  return 'nd' if n == 2
  return 'rd' if n == 3
  return 'th'
end
process_trad(m, h, day_of_month, month, dow) click to toggle source
# File lib/cron2english/all.rb, line 67
def process_trad(m, h, day_of_month, month, dow)
  month = month.split(',').map{|month_part|
    if month_part =~ MONTH_REGEX
      month_part = MONTH2NUM[$1.downcase]
    elsif month_part =~ MONTH_RANGE_REGEX
      month_part = [$1, $2].map{|x| MONTH2NUM[x.downcase]}.join('-')
    end
    month_part
  }.join(',')
  month = month.to_s if month
  dow = dow.split(',').map{|dow_part|
    if dow_part =~ DOW_REGEX
      dow_part = DOW2NUM[$1.downcase]
    elsif dow_part =~ DOW_RANGE_REGEX
      dow_part = [$1, $2].map{|d| DOW2NUM[d.downcase]}.join('-')
    end
    dow_part
  }.join(',')
  dow = dow.to_s if dow
  bits = [m, h, day_of_month, month, dow]
  unparseable = []
  bits_segmented = []
  bits.each_with_index do |bit, i|
    segments = []
    if bit == '*'
      segments << ['*']

    elsif bit =~ %r<^\*/(\d+)$>
      # a hack for "*/3" etc
      segments << ['*', $1.to_i]

    elsif bit =~ ATOMS_REGEX
      bit.split(',').each do |thang|
        if thang =~ %r<^(?:(\d+)|(?:(\d+)-(\d+)(?:/(\d+))?))$>
          if $1
            segments << [$1.to_i]
          elsif $4
            segments << [$2.to_i, $3.to_i, $4.to_i]
          else
            segments << [$2.to_i, $3.to_i]
          end
        else
          unparseable << ("field %s: \"%s\"" % [i + 1, bit])
        end
      end
    else
      unparseable << ("field %s: \"%s\"" % [i + 1, bit])
    end
    bits_segmented << segments
  end

  give_up(unparseable.join("; ")) if unparseable.size > 0
  bits_to_english(bits_segmented)
end
process_vixie(str) click to toggle source
# File lib/cron2english/all.rb, line 63
def process_vixie(str)
  [str]
end