Below you'll find my solution to the quiz.  I approached it a
relatively standard object-oriented fashion.  For example,
TimeSpecifier acts as an abstract base class for Day and HourMinute to
bring together their commonalities.  And I did use some of that good
ol' Ruby duck typing so that a TimeRange can be used as a
TimeSpecifier.

Eric

----

Are you interested in on-site Ruby training that uses well-designed,
real-world, hands-on exercises? http://LearnRuby.com

====

# This is a solution to Ruby Quiz #144 (see http://www.rubyquiz.com/)
# by LearnRuby.com and released under the Creative Commons
# Attribution-Share Alike 3.0 United States License.  This source code
can
# also be found at:
#   http://learnruby.com/examples/ruby-quiz-144.shtml


# A TimeWindow is a specification for a time window.  It is specified
# by a string, and an instance of Time can be checked to see if it's
# included in the window.  The specification string is is best
# documented by quoting the Ruby Quiz #144 description:
#
#   0700-0900                     # every day between these times
#   Sat Sun                       # all day Sat and Sun, no other
times
#   Sat Sun 0700-0900             # 0700-0900 on Sat and Sun only
#   Mon-Fri 0700-0900             # 0700-0900 on Monday to Friday only
#   Mon-Fri 0700-0900; Sat Sun    # ditto plus all day Sat and Sun
#   Fri-Mon 0700-0900             # 0700-0900 on Fri Sat Sun Mon
#   Sat 0700-0800; Sun 0800-0900  # 0700-0800 on Sat, plus 0800-0900
on Sun
class TimeWindow


  # Represents a time range defined by a start and end TimeSpecifier.
  class TimeRange
    def initialize(start_t, end_t,
                   include_end, allow_reverse_range = false)
      raise "mismatched time specifiers in range (%s and %s)" %
        [start_t, end_t] unless
        start_t.class == end_t.class
      raise "reverse range not allowed \"%s-%s\"" % [start_t, end_t]
if
        start_t >= end_t && !allow_reverse_range
      @start_t, @end_t, @include_end = start_t, end_t, include_end
    end

    # Equality is defined as a TimeSpecifier on the RHS being in the
    # this range.
    def ==(time_spec)
      # do either a < or a <= when comparing the end of the range
      # depending on value of @include_end
      end_comparison = @include_end ? :<= : :<

      # NOTE: the call to the send method below is used to call the
      # method in end_comparison
      if @start_t < @end_t
        time_spec >= @start_t && time_spec.send(end_comparison,
@end_t)
      else  # a reverse range, such as "Fri-Mon", needs an ||
        time_spec >= @start_t || time_spec.send(end_comparison,
@end_t)
      end
    end

    def to_s
      "%s-%s" % [@start_t, @end_t]
    end
  end


  # This is an abstract base class for time specifiers, such as a day
  # of the week or a time of day.
  class TimeSpecifier
    include Comparable

    def <=>(other)
      raise "incompatible comparison (%s and %s)" % [self, other]
unless
        self.class == other.class
      @specifier <=> other.specifier
    end

    protected

    attr_reader :specifier

    # Given an "item" regular expression returns a hash of two regular
    # expressions.  One matches an individual item and the other a
    # range of items.  Both returned regular expressions use parens,
    # so the individual items can be extraced from a match.
    def self.build_regexps(regexp)
      individual_re = Regexp.new "^(%s)" % regexp
      range_re = Regexp.new "^(%s)\-(%s)" % [regexp, regexp]
      { :individual => individual_re, :range => range_re }
    end

    # Attempts to match str with the two regexps passed in.  regexps
    # is a hash that contains two regular expressions, one that
    # matches a single TimeSpecifier and one that matches a range of
    # TimeSpecifiers.  If there's a match then it returns either an
    # instance of klass or an instance of a TimeRange of klass (and
    # str is destructively modified to remove the matched text from
    # its beginning).  If there isn't a match, then nil is returned.
    # include_end determines whether the end specification of the
    # range is included in the range (e.g., if the specifier is
    # "Mon-Fri" whether or not Fri is included).  allow_reverse_range
    # determines whether a range in which the start is after the end
    # is allowed, as in "Fri-Mon"; this might be alright for days of
    # the week but not for times.
    def self.super_parse(str, klass, regexps,
                         include_end, allow_reverse_range)
      # first try a range match
      if match_data = regexps[:range].match(str)
        consume_front(str, match_data[0].size)
        TimeRange.new(klass.new_from_str(match_data[1]),
                      klass.new_from_str(match_data[2]),
                      include_end,
                      allow_reverse_range)
      # second try individual match
      elsif match_data = regexps[:individual].match(str)
        consume_front(str, match_data[0].size)
        klass.new_from_str(match_data[1])
      else
        nil
      end
    end

    # Consumes size characters from the front of str along with any
    # remaining whitespace at the front.  This modifies the actual
    # string.
    def self.consume_front(str, size)
      str[0..size] = ''
      str.lstrip!
    end
  end


  # Time specifier for a day of the week.
  class Day < TimeSpecifier
    Days = %w(Sun Mon Tue Wed Thu Fri Sat)
    @@regexps = TimeSpecifier.build_regexps(/[A-Za-z]{3}/)

    def initialize(day)
      raise "illegal day \"#{day}\"" unless (0...Days.size) === day
      @specifier = day
    end

    def to_s
      Days[@specifier]
    end

    def self.new_from_str(str)
      day = Days.index(str)
      raise "illegal day \"#{day_str}\"" if day.nil?
      new(day)
    end

    def self.parse(str)
      super_parse(str, Day, @@regexps, true, true)
    end
  end


  # Time specifier for a specific time of the day (i.e., hour and
minute).
  class HourMinute < TimeSpecifier
    @@regexps = TimeSpecifier.build_regexps(/\d{4}/)

    def initialize(hour_minute)
      hour = hour_minute / 100
      minute = hour_minute % 100
      raise "illegal time \"#{hour_minute}\"" unless
        (0..23) === hour && (0..59) === minute
      @specifier = hour_minute
    end

    def to_s
      "%04d" % @specifier
    end

    def self.new_from_str(str)
      new str.to_i
    end

    def self.parse(str)
      super_parse(str, HourMinute, @@regexps, false, false)
    end
  end


  # Creates a TimeWindow by parsing a string specifying some
combination
  # of day and hour-minutes, possibly in ranges.
  def initialize(str)
    # time_frame is a Day, HourMinute, or TimeRangeof either; it is
    # set here so when it's sent inside the block, it won't be scoped
    # to the block
    time_frame = nil

    @periods = []
    str.split(/ *; */).each do |period_str|
      # frame set is a hash where the keys are either the class Day or
      # HourMinute and the associated values are all time specifiers
      # for that class.  The default value is the empty array.
      period = Hash.new { |h, k| h[k] = [] }

      # process each time specifier in period_str by sequentially
      # processing andconsuming the beginning of the string
      until period_str.empty?
        # set frame_type and time_frame based on the first matching
        # parse
        frame_type = [Day, HourMinute].find { |specifier|
          time_frame = specifier.parse(period_str)
        }
        raise "illegal window specifier \"#{period_str}\"." if
          time_frame.nil?

        period[frame_type] << time_frame
      end

      @periods << period
    end
  end

  # Returns true if the TimeWindow includes the passed in time, false
  # otherwise.
  def include?(time)
    d = Day.new(time.wday)
    hm = HourMinute.new(time.hour * 100 + time.min)

    # see if any one period matches the time or if there are no
periods
    @periods.empty? || @periods.any? { |period|
      # a period matches if either there is no day specification or
      # one day specification matches, and if either there is no
      # hour-minute specification or one such specification matches
      (period[Day].empty? ||
         period[Day].any? { |day_period| day_period == d }) &&
        (period[HourMinute].empty? ||
           period[HourMinute].any? { |hm_period| hm_period == hm })
    }
  end

  def to_s
    @periods.map { |period|
      (period[Day] + period[HourMinute]).map { |time_spec|
        time_spec.to_s
      }.join(' ')
    }.join(' ; ')
  end
end