[QUIZ] Time Window (#144)

The three rules of Ruby Quiz:

1. Please do not post any solutions or spoiler discussion for this quiz until
48 hours have passed from the time on this message.

2. Support Ruby Quiz by submitting ideas as often as you can:

http://www.rubyquiz.com/

3. Enjoy!

Suggestion: A [QUIZ] in the subject of emails about the problem helps everyone
on Ruby Talk follow the discussion. Please reply to the original quiz message,
if you can.

···

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

by Brian Candler

Write a Ruby class which can tell you whether the current time (or any given
time) is within a particular "time window". Time windows are defined by strings
in the following format:

  # 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

Time ranges should exclude the upper bound, i.e. 0700-0900 is 07:00:00 to
08:59:59. An empty time window means "all times everyday". Here are some test
cases to make it clearer:

  class TestTimeWindow < Test::Unit::TestCase
    def test_window_1
      w = TimeWindow.new("Sat-Sun; Mon Wed 0700-0900; Thu 0700-0900 1000-1200")
      
      assert ! w.include?(Time.mktime(2007,9,25,8,0,0)) # Tue
      assert w.include?(Time.mktime(2007,9,26,8,0,0)) # Wed
      assert ! w.include?(Time.mktime(2007,9,26,11,0,0))
      assert ! w.include?(Time.mktime(2007,9,27,6,59,59)) # Thu
      assert w.include?(Time.mktime(2007,9,27,7,0,0))
      assert w.include?(Time.mktime(2007,9,27,8,59,59))
      assert ! w.include?(Time.mktime(2007,9,27,9,0,0))
      assert w.include?(Time.mktime(2007,9,27,11,0,0))
      assert w.include?(Time.mktime(2007,9,29,11,0,0)) # Sat
      assert w.include?(Time.mktime(2007,9,29,0,0,0))
      assert w.include?(Time.mktime(2007,9,29,23,59,59))
    end
    
    def test_window_2
      w = TimeWindow.new("Fri-Mon")
      assert ! w.include?(Time.mktime(2007,9,27)) # Thu
      assert w.include?(Time.mktime(2007,9,28))
      assert w.include?(Time.mktime(2007,9,29))
      assert w.include?(Time.mktime(2007,9,30))
      assert w.include?(Time.mktime(2007,10,1))
      assert ! w.include?(Time.mktime(2007,10,2)) # Tue
    end
    
    def test_window_nil
      w = RDS::TimeWindow.new("")
      assert w.include?(Time.mktime(2007,9,25,1,2,3)) # all times
    end
  end

The three rules of Ruby Quiz:

1. Please do not post any solutions or spoiler discussion for this quiz
until 48 hours have passed from the time on this message.

2. Support Ruby Quiz by submitting ideas as often as you can:

http://www.rubyquiz.com/

3. Enjoy!

Suggestion: A [QUIZ] in the subject of emails about the problem helps
everyone on Ruby Talk follow the discussion. Please reply to the
original quiz message, if you can.

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

···

On Fri, 19 Oct 2007 21:14:00 +0900, Ruby Quiz wrote:
=-=-=-=-=

by Brian Candler

Write a Ruby class which can tell you whether the current time (or any
given time) is within a particular "time window". Time windows are
defined by strings in the following format:

  # 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

Time ranges should exclude the upper bound, i.e. 0700-0900 is 07:00:00
to 08:59:59. An empty time window means "all times everyday". Here are
some test cases to make it clearer:

I have rewritten the test cases to give more informative messages:

  class TestTimeWindow < Test::Unit::TestCase
    def test_window_1
      s = "Sat-Sun; Mon Wed 0700-0900; Thu 0700-0900 1000-1200"
      w = TimeWindow.new(s)
      
      assert ! w.include?(Time.mktime(2007,9,25,8,0,0)), "#{s.inspect} should not include Tue 8am"
      assert w.include?(Time.mktime(2007,9,26,8,0,0)), "#{s.inspect} should include Wed 8am"
      assert ! w.include?(Time.mktime(2007,9,26,11,0,0)), "#{s.inspect} should not include Wed 11am"
      assert ! w.include?(Time.mktime(2007,9,27,6,59,59)), "#{s.inspect} should not include Thurs 6:59am"
      assert w.include?(Time.mktime(2007,9,27,7,0,0)), "#{s.inspect} should include Thurs 7am"
      assert w.include?(Time.mktime(2007,9,27,8,59,59)), "#{s.inspect} should include Thurs 8:59am"
      assert ! w.include?(Time.mktime(2007,9,27,9,0,0)), "#{s.inspect} should not include Thurs 9am"
      assert w.include?(Time.mktime(2007,9,27,11,0,0)), "#{s.inspect} should include Thurs 11am"
      assert w.include?(Time.mktime(2007,9,29,11,0,0)), "#{s.inspect} should include Sat 11am"
      assert w.include?(Time.mktime(2007,9,29,0,0,0)), "#{s.inspect} should include Sat midnight"
      assert w.include?(Time.mktime(2007,9,29,23,59,59)),
                                    "#{s.inspect} should include Saturday one minute before midnight"
    end
    
    def test_window_2
      s = "Fri-Mon"
      w = TimeWindow.new(s)
      assert ! w.include?(Time.mktime(2007,9,27)), "#{s.inspect} should not include Thurs"
      assert w.include?(Time.mktime(2007,9,28)), "#{s.inspect} should include Fri"
      assert w.include?(Time.mktime(2007,9,29)), "#{s.inspect} should include Sat"
      assert w.include?(Time.mktime(2007,9,30)), "#{s.inspect} should include Sun"
      assert w.include?(Time.mktime(2007,10,1)), "#{s.inspect} should include Mon"
      assert ! w.include?(Time.mktime(2007,10,2)), "#{s.inspect} should not include Tues"
    end
    
    def test_window_nil
      w = TimeWindow.new("")
      assert w.include?(Time.mktime(2007,9,25,1,2,3)),"Empty string should include all times"
    end
  end

--
Ken Bloom. PhD candidate. Linguistic Cognition Laboratory.
Department of Computer Science. Illinois Institute of Technology.
http://www.iit.edu/~kbloom1/

Hello,

Here is my solution to the quiz. First I used smaller classes to simplify
TimeWindow:

# Class to store a single time range defined by a start/end
class TimeRange
  # Each Input in form of "HHMM"
  def initialize(start_str, end_str)
    @start = start_str.to_i
    @end = end_str.to_i
  end

  attr_reader :start, :end
end

# Represents a single time period for particular days and times
# A time window may contain several of these frames
class TimeFrame
  # Days - Bitmask of 7 fields (Sun @ 0, Mon @ 1, Tues @ 2, etc)
  # Time range - List of start/end time ranges defining the time frame
  def initialize(days, time_ranges)
    @days = days
    @time_ranges = time_ranges
  end

  # Does the given Time match this Time Frame?
  def include?(time)
    if @days[time.wday]
      # If no times then days matching is good enough
      return true if @time_ranges.size == 0

      # Check time range(s)
      for time_range in @time_ranges
        time_n = time.hour * 100 + time.min
        return true if time_n >= time_range.start and
                       time_n < time_range.end
      end
    end

    false
  end
end

The main class then simply parses the time window string at startup, saving
each individual time window to memory. Then the include? method iterates
through those time windows to determine if a particular time is in the
window:

# Defines a time window spanning multiple days and time ranges
class TimeWindow
  Days = ["Sun", "Mon", "Tues", "Wed", "Thu", "Fri", "Sat"]

  # Constructor accepting a string as defined in ruby quiz description
  def initialize(time_window)
    @timeframes = []

    for group in time_window.split(";")
      days, times = Array.new(7, false), []

      for item in group.split(" ")
        # Range of values?
        if item.include?("-")
          # Yes, Figure out if range is days or times
          range = item.split("-")

          if Days.include?(range[0])
            set_day_range(days, range[0], range[1])
          else
            times << TimeRange.new(range[0], range[1])
          end
        else
          days[Days.index(item)] = true if Days.include?(item)
        end
      end

      @timeframes << TimeFrame.new(days, times)
    end
  end

  # Set days in given range in the input array
  # Inputs: days - List of days in the time window
  # start_day, end_day - Day range to add to the window
  def set_day_range(days, start_day, end_day)
    pos = Days.index(start_day)
    while pos != (Days.index(end_day) + 1) % 7
      days[pos] = true
      pos = (pos + 1) % 7
    end
  end

  # Does the given Time match this time window?
  def include?(time)
    for time_frame in @timeframes
      return true if time_frame.include?(time)
    end

    return (@timeframes.size == 0) # Empty time string matches all times
  end

  private :set_day_range
end

Fortunately it passes all of the tests :slight_smile:
A pastie is available here: http://pastie.caboo.se/109346
Thanks,

Justin

···

On 10/19/07, Ruby Quiz <james@grayproductions.net> wrote:

The three rules of Ruby Quiz:

1. Please do not post any solutions or spoiler discussion for this quiz
until
48 hours have passed from the time on this message.

2. Support Ruby Quiz by submitting ideas as often as you can:

http://www.rubyquiz.com/

3. Enjoy!

Suggestion: A [QUIZ] in the subject of emails about the problem helps
everyone
on Ruby Talk follow the discussion. Please reply to the original quiz
message,
if you can.

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

by Brian Candler

Write a Ruby class which can tell you whether the current time (or any
given
time) is within a particular "time window". Time windows are defined by
strings
in the following format:

        # 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

Time ranges should exclude the upper bound, i.e. 0700-0900 is 07:00:00 to
08:59:59. An empty time window means "all times everyday". Here are some
test
cases to make it clearer:

        class TestTimeWindow < Test::Unit::TestCase
          def test_window_1
            w = TimeWindow.new("Sat-Sun; Mon Wed 0700-0900; Thu 0700-0900
1000-1200")

            assert ! w.include?(Time.mktime(2007,9,25,8,0,0)) # Tue
            assert w.include?(Time.mktime(2007,9,26,8,0,0)) # Wed
            assert ! w.include?(Time.mktime(2007,9,26,11,0,0))
            assert ! w.include?(Time.mktime(2007,9,27,6,59,59)) # Thu
            assert w.include?(Time.mktime(2007,9,27,7,0,0))
            assert w.include?(Time.mktime(2007,9,27,8,59,59))
            assert ! w.include?(Time.mktime(2007,9,27,9,0,0))
            assert w.include?(Time.mktime(2007,9,27,11,0,0))
            assert w.include?(Time.mktime(2007,9,29,11,0,0)) # Sat
            assert w.include?(Time.mktime(2007,9,29,0,0,0))
            assert w.include?(Time.mktime(2007,9,29,23,59,59))
          end

          def test_window_2
            w = TimeWindow.new("Fri-Mon")
            assert ! w.include?(Time.mktime(2007,9,27)) # Thu
            assert w.include?(Time.mktime(2007,9,28))
            assert w.include?(Time.mktime(2007,9,29))
            assert w.include?(Time.mktime(2007,9,30))
            assert w.include?(Time.mktime(2007,10,1))
            assert ! w.include?(Time.mktime(2007,10,2)) # Tue
          end

          def test_window_nil
            w = RDS::TimeWindow.new("")
            assert w.include?(Time.mktime(2007,9,25,1,2,3)) # all
times
          end
        end

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

Here's my solutions. I used Runt for the heavy lifting. I just had
to parse the string and create Runt temporal expressions.

require 'runt'

#adds ability to check Runt expressions against Time objects
class Time
  include Runt::DPrecision

  attr_accessor :date_precision

  def date_precision
    return @date_precision unless @date_precision.nil?
    return Runt::DPrecision::DEFAULT
  end
end

module Runt

  #extends REWeek to allow for spanning across weeks
  class REWeek

    def initialize(start_day,end_day=6)
      @start_day = start_day
      @end_day = end_day
    end

    def include?(date)
      return true if @start_day==@end_day
      if @start_day < @end_day
        @start_day<=date.wday && @end_day>=date.wday
      else
        (@start_day<=date.wday && 6 >=date.wday) ||
          (0 <=date.wday && @end_day >=date.wday)
      end
    end

  end

  class StringParser < Runt::Intersect

    def initialize(string)
      super()
      add parsed(string)
    end

    #recursive method to parse input string
    def parse(token)
      case token
      when ""
        REWeek.new(0)
      when /^(.+);(.+)/ # split at semicolons
        parse($1) | parse($2)
      when /(\D+) (\d.+)/ # split days and times
        parse($1) & parse($2)
      when /(\D+) (\D+)/, /(\d+-\d+) (\d+-\d+)/ # split at spaces
        parse($1) | parse($2)
      when /([A-Z][a-z][a-z])-([A-Z][a-z][a-z])/ # create range of days
        REWeek.new(Runt.const_get($1), Runt.const_get($2))
      when /([A-Z][a-z][a-z])/ # create single day
        DIWeek.new(Runt.const_get($1))
      when /(\d\d)(\d\d)-(\d\d)(\d\d)/ #create time range
        start = Time.mktime(2000,1,1,$1.to_i,$2.to_i)
        # 0600-0900 should work like 0600-0859,
        stop = Time.mktime(2000,1,1,$3.to_i,$4.to_i) - 60
        REDay.new(start.hour, start.min, stop.hour, stop.min)
      end
    end
    alias :parsed :parse

  end

end

class TimeWindow < Runt::StringParser
end

#!/usr/bin/env ruby

class TimeWindow
  DAYNAMES=%w[Sun Mon Tue Wed Thu Fri Sat]
  DAYNAME=%r{Sun|Mon|Tue|Wed|Thu|Fri|Sat}
  TIME=%r{[0-9]+}

  def initialize string
    string = " " if string == "" #make an empty string match everythingworking around the way clauses are split
    #splitting an empty string gives an empty array (i.e. no clauses)
    #splitting a " " gives a single clause with no day names (so all are used) and no times (so all are used)
    @myarray=Array.new(7){[]}
    
    #different clauses are split by semicolons
    string.split(/\s*;\s*/).each do |clause|

      #find the days that this clause applies to
      curdays=[]
      clause.scan(/(#{DAYNAME})(?:(?=\s)|$)|(#{DAYNAME})-(#{DAYNAME})/) do |single,start,finish|
        single &&= DAYNAMES.index(single)
        start &&= DAYNAMES.index(start)
        finish &&= DAYNAMES.index(finish)
        curdays << single if single
        if start and finish
          (start..finish).each{|x| curdays << x} if start<finish
          (start..6).each{|x| curdays << x} if finish<start
          (0..finish).each{|x| curdays << x} if finish<start
        end
      end

      #all days if no day names were given
      curdays=(0..6).to_a if curdays==[]

      #find the times that this clause applies to
      found=false
      clause.scan(/(#{TIME})-(#{TIME})/) do |start,finish|
        found=true
        curdays.each do |day|
          @myarray[day] << [start,finish]
        end
      end

      #all times if none were given
      if not found
        curdays.each {|day| @myarray[day] << ["0000","2400"]}
      end
    end
  end

  def include? time
    matchday=time.wday
    matchtime="%02d%02d" % [time.hour,time.min]
    @myarray[matchday].any?{|start,finish| start<=matchtime && matchtime<finish}
  end
  
  alias_method :===, :include?

end

···

On Fri, 19 Oct 2007 21:14:00 +0900, Ruby Quiz wrote:

The three rules of Ruby Quiz:

1. Please do not post any solutions or spoiler discussion for this quiz
until 48 hours have passed from the time on this message.

2. Support Ruby Quiz by submitting ideas as often as you can:

http://www.rubyquiz.com/

3. Enjoy!

Suggestion: A [QUIZ] in the subject of emails about the problem helps
everyone on Ruby Talk follow the discussion. Please reply to the
original quiz message, if you can.

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

by Brian Candler

Write a Ruby class which can tell you whether the current time (or any
given time) is within a particular "time window". Time windows are
defined by strings in the following format:

  # 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

Time ranges should exclude the upper bound, i.e. 0700-0900 is 07:00:00
to 08:59:59. An empty time window means "all times everyday". Here are
some test cases to make it clearer:

  class TestTimeWindow < Test::Unit::TestCase
    def test_window_1
      w = TimeWindow.new("Sat-Sun; Mon Wed 0700-0900; Thu 0700-0900
      1000-1200")
      
      assert ! w.include?(Time.mktime(2007,9,25,8,0,0)) # Tue assert
      w.include?(Time.mktime(2007,9,26,8,0,0)) # Wed assert !
      w.include?(Time.mktime(2007,9,26,11,0,0)) assert !
      w.include?(Time.mktime(2007,9,27,6,59,59)) # Thu assert
      w.include?(Time.mktime(2007,9,27,7,0,0)) assert
      w.include?(Time.mktime(2007,9,27,8,59,59)) assert !
      w.include?(Time.mktime(2007,9,27,9,0,0)) assert
      w.include?(Time.mktime(2007,9,27,11,0,0)) assert
      w.include?(Time.mktime(2007,9,29,11,0,0)) # Sat assert
      w.include?(Time.mktime(2007,9,29,0,0,0)) assert
      w.include?(Time.mktime(2007,9,29,23,59,59))
    end
    
    def test_window_2
      w = TimeWindow.new("Fri-Mon")
      assert ! w.include?(Time.mktime(2007,9,27)) # Thu assert
      w.include?(Time.mktime(2007,9,28)) assert
      w.include?(Time.mktime(2007,9,29)) assert
      w.include?(Time.mktime(2007,9,30)) assert
      w.include?(Time.mktime(2007,10,1)) assert !
      w.include?(Time.mktime(2007,10,2)) # Tue
    end
    
    def test_window_nil
      w = RDS::TimeWindow.new("")
      assert w.include?(Time.mktime(2007,9,25,1,2,3)) # all times
    end
  end

--
Ken Bloom. PhD candidate. Linguistic Cognition Laboratory.
Department of Computer Science. Illinois Institute of Technology.
http://www.iit.edu/~kbloom1/

Like Gordon, I used Runt a bit for my solution. Unlike Gordon, I
didn't use Runt *directly*. I remembered seeing it some time ago and
used what I could recall of the general ideas of implementation to
roll my own (probably not as well as Runt itself). And I believe the
naming of "Unbound Time" comes from Martin Fowler.

require 'date'

class TimeWindow
  attr_reader :intervals

  def initialize(string)
    @intervals = []

    parse(string)
  end

  def include?(time)
    intervals.any? { |int| int.include?(time) }
  end

  private

  attr_writer :intervals

  def parse(string)
    parts = string.split(';')
    parts = [''] if parts.empty?
    @intervals = parts.collect { |str| TimeInterval.new(str) }
  end

end

class TimeInterval
  DAYS = %w(Sun Mon Tue Wed Thu Fri Sat)

  UnboundTime = Struct.new(:hour, :minute) do
    include Comparable

    def <=>(time)
      raise TypeError, "I need a real Time object for comparison"
unless time.is_a?(Time)

      comp_date = Date.new(time.year, time.month, time.mday)
      comp_date += 1 if hour == 24

      Time.mktime(comp_date.year, comp_date.month, comp_date.day, hour
% 24, minute, 0) <=> time
    end
  end

  UnboundTimeRange = Struct.new(:start, :end)

  attr_reader :days, :times

  def initialize(string)
    @days = []
    @times = []

    parse(string)
  end

  def include?(time)
    day_ok?(time) and time_ok?(time)
  end

  private

  attr_writer :days, :times

  def parse(string)
    unless string.empty?
      string.strip.split(' ').each do |segment|
        if md = segment.match(/^(\d{4})-(\d{4})$/)
          self.times +=
[ UnboundTimeRange.new(UnboundTime.new(*md[1].unpack('A2A2').collect
{ |elem| elem.to_i }), UnboundTime.new(*md[2].unpack('A2A2').collect
{ |elem| elem.to_i })) ]
        elsif md = segment.match(/^(\w+)(-(\w+))?$/)
          if md[2]
            start_day = DAYS.index(md[1])
            end_day = DAYS.index(md[3])

            if start_day <= end_day
              self.days += (start_day .. end_day).to_a
            else
              self.days += (start_day .. DAYS.length).to_a + (0 ..
end_day).to_a
            end
          else
            self.days += [DAYS.index(md[1])]
          end
        else
          raise ArgumentError, "Segment #{segment} of time window
incomprehensible"
        end
      end
    end

    self.days = 0..DAYS.length if days.empty?
    self.times = [ UnboundTimeRange.new(UnboundTime.new(0, 0),
UnboundTime.new(24, 0)) ] if times.empty?
  end

  def day_ok?(time)
    days.any? { |d| d == time.wday }
  end

  def time_ok?(time)
    times.any? { |t| t.start <= time and t.end > time }
  end
end

All tests pass, which at the moment is good enough for me.

···

On Oct 19, 7:14 am, Ruby Quiz <ja...@grayproductions.net> wrote:

The three rules of Ruby Quiz:

1. Please do not post any solutions or spoiler discussion for this quiz until
48 hours have passed from the time on this message.

2. Support Ruby Quiz by submitting ideas as often as you can:

http://www.rubyquiz.com/

3. Enjoy!

Suggestion: A [QUIZ] in the subject of emails about the problem helps everyone
on Ruby Talk follow the discussion. Please reply to the original quiz message,
if you can.

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

by Brian Candler

Write a Ruby class which can tell you whether the current time (or any given
time) is within a particular "time window". Time windows are defined by strings
in the following format:

        # 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

Time ranges should exclude the upper bound, i.e. 0700-0900 is 07:00:00 to
08:59:59. An empty time window means "all times everyday". Here are some test
cases to make it clearer:

        class TestTimeWindow < Test::Unit::TestCase
          def test_window_1
            w = TimeWindow.new("Sat-Sun; Mon Wed 0700-0900; Thu 0700-0900 1000-1200")

            assert ! w.include?(Time.mktime(2007,9,25,8,0,0)) # Tue
            assert w.include?(Time.mktime(2007,9,26,8,0,0)) # Wed
            assert ! w.include?(Time.mktime(2007,9,26,11,0,0))
            assert ! w.include?(Time.mktime(2007,9,27,6,59,59)) # Thu
            assert w.include?(Time.mktime(2007,9,27,7,0,0))
            assert w.include?(Time.mktime(2007,9,27,8,59,59))
            assert ! w.include?(Time.mktime(2007,9,27,9,0,0))
            assert w.include?(Time.mktime(2007,9,27,11,0,0))
            assert w.include?(Time.mktime(2007,9,29,11,0,0)) # Sat
            assert w.include?(Time.mktime(2007,9,29,0,0,0))
            assert w.include?(Time.mktime(2007,9,29,23,59,59))
          end

          def test_window_2
            w = TimeWindow.new("Fri-Mon")
            assert ! w.include?(Time.mktime(2007,9,27)) # Thu
            assert w.include?(Time.mktime(2007,9,28))
            assert w.include?(Time.mktime(2007,9,29))
            assert w.include?(Time.mktime(2007,9,30))
            assert w.include?(Time.mktime(2007,10,1))
            assert ! w.include?(Time.mktime(2007,10,2)) # Tue
          end

          def test_window_nil
            w = RDS::TimeWindow.new("")
            assert w.include?(Time.mktime(2007,9,25,1,2,3)) # all times
          end
        end

--
-yossef

Here is my solution to the time window quiz. Range.create_from_string is the workhorse method and it would be nice if that was refactored into smaller pieces.

class TimeWindow
   def initialize(definition_string)
     @ranges = []
     definition_string.split(/;/).each do |part|
       @ranges << Range.create_from_string(part.strip)
     end
     @ranges << Range.create_from_string('') if @ranges.empty?
   end

   def include?(time)
     @ranges.any? {|r| r.include?(time)}
   end

   class Range < Struct.new(:day_parts, :time_parts)
     DAYS = %w{Sun Mon Tue Wed Thu Fri Sat}

     def self.create_from_string(str)
       time_parts = []
       day_parts = []
       str.split(/ /).each do |token|
         token.strip!
         if DAYS.include?(token)
           day_parts << token
         elsif token =~ /^(\w{3})-(\w{3})$/
           start_day, end_day = $1, $2
           start_found = false
           (DAYS * 2).each do |d|
             start_found = true if d == start_day
             day_parts << d if start_found
             break if d == end_day && start_found
           end
         elsif token =~ /^(\d{4})-(\d{4})$/
           time_parts << (($1.to_i)..($2.to_i - 1))
         else
           raise "Unrecognized token: #{token}"
         end
       end
       time_parts << (0..2399) if time_parts.empty?
       day_parts = DAYS.clone if day_parts.empty?
       self.new(day_parts, time_parts)
     end

     def include?(time)
       matches_day?(time) && matches_minute?(time)
     end

     def matches_day?(time)
       day = time.strftime('%a')
       self.day_parts.include?(day)
     end

     def matches_minute?(time)
       minute = time.strftime('%H%M').to_i
       self.time_parts.any? {|tp| tp.include?(minute) }
     end
   end
end

Hi,

This is my solution: nothing spectacular or too clever. The idea was
to convert every part of the window (everything between ";") into a
class that knows how to parse the ranges. That class (TimeRange)
converts the part into an array of day_of_week ranges and an array of
hour ranges. To include a time, this window needs to match at least
one day_of_week and at least one hour range. The time window, then,
has an array of those TimeRange objects, and tries to find at least
one that matches. One interesting thing is that I convert every time
definition into a range, even the ones with just one element, so I can
use Range#include? across all time ranges.

require 'time'

class TimeRange
  def initialize(s)
    @day_of_week = []
    @hour = []
    s.strip.split(" ").each do |range|
      if (match = range.match(/(\d{4})-(\d{4})/))
        @hour << (match[1].to_i...match[2].to_i)
      elsif (match = range.match(/([a-zA-Z]{3})-([a-zA-Z]{3})/))
        first = Time::RFC2822_DAY_NAME.index(match[1])
        second = Time::RFC2822_DAY_NAME.index(match[2])
        if (first < second)
          @day_of_week << (first..second)
        else
          @day_of_week << (first..(Time::RFC2822_DAY_NAME.size-1))
          @day_of_week << (0..second)
        end
      else
        @day_of_week <<
(Time::RFC2822_DAY_NAME.index(range)..Time::RFC2822_DAY_NAME.index(range))
      end
    end
  end

  def include?(time)
    dow = time.wday
    hour = time.strftime("%H%M").to_i
    any?(@day_of_week, dow) and any?(@hour, hour)
  end

  def any?(enum, value)
    return true if enum.empty?
    enum.any?{|x| x.include?(value)}
  end
end

class TimeWindow
  def initialize(s)
    @ranges = []
    s.split(";").each do |part|
      @ranges << TimeRange.new(part)
    end
  end

  def include?(time)
    return true if @ranges.empty?
    @ranges.any? {|x| x.include?(time)}
  end
end

Kind regards,

Jesus.

···

On 10/19/07, Ruby Quiz <james@grayproductions.net> wrote:

by Brian Candler

Write a Ruby class which can tell you whether the current time (or any given
time) is within a particular "time window". Time windows are defined by strings
in the following format:

        # 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

Time ranges should exclude the upper bound, i.e. 0700-0900 is 07:00:00 to
08:59:59. An empty time window means "all times everyday". Here are some test
cases to make it clearer:

Another solution for Time Window quiz:

I considered only when input has day ranges in ascending order ("Mon
Fri-Sun" or "Fri-Sun Mon", but not "Fri-Mon") and the first day of the
week is Monday.

class TimeWindow

  Days = { "Mon" => 0, "Tue" => 1, "Wed" => 2, "Thu" => 3, "Fri" => 4,
"Sat" => 5, "Sun" => 6}

  def initialize (window)
    @window = window
    @ranges = []
    parse_window
  end

  def include? (time)
    hour = time.strftime("%H%M").to_i
    day = time.strftime("%w").to_i
    req = (day-1)*10000+hour
    puts "#{req}"
    result = false
    @ranges.each{ |range|
      if range[0] <= req && req < range[1]
        result = true
      end
    }
    result
  end

  private

  #Parse the input
  def parse_window
    regex = /((?:Mon[ -]?|Tue[ -]?|Wed[ -]?|Thu[ -]?|Fri[ -]?|Sat[
-]?|Sun[ -]?)+)?((?:[012]\d[0-6]\d-[012]\d[0-6]\d[ ]?)+)?/
    @window.split(";").each { |window|
      window.strip!
      match = regex.match(window)

      # it has days
      if match[1]
        days = parse_days match[1]
      else
        days = [[0,6]] # everyday
      end

      # it has hours
      if match[2]
        time = parse_time match[2]
      else
        time = [[0,2400]] # all day
      end

      days.each {|dr|
        time.each {|tr|
          @ranges << [dr[0]*10000+tr[0], dr[1]*10000+tr[1]]
        }
      }
    }
  end

  def parse_days (days)
    result = []
    days.scan(/(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun)-(Mon|Tue|Wed|Thu|Fri|Sat|Sun)|(Mon|Tue|Wed|Thu|Fri|Sat|Sun))/)
{
      if $3 # it's just one day
        result << [Days[$3],Days[$3]]
      else # it's a range
        result << [Days[$1],Days[$2]]
      end
    }
    result
  end

  def parse_time (time)
    result = []
    time.scan(/([012]\d[0-6]\d)-([012]\d[0-6]\d)/) {
      result << [$1.to_i, $2.to_i]
    }
    result
  end
end

There was a bug in my code. I shouldn't subtract a minute from the
end of minute ranges, just a second. Here's the fixed code.

#time_window.rb
require 'runt_ext'

module Runt

  #extends REWeek to allow for spanning across weeks
  class REWeek

    def initialize(start_day,end_day=6)
      @start_day = start_day
      @end_day = end_day
    end

    def include?(date)
      return true if @start_day==@end_day
      if @start_day < @end_day
        @start_day<=date.wday && @end_day>=date.wday
      else
        (@start_day<=date.wday && 6 >=date.wday) || (0 <=date.wday &&
@end_day >=date.wday)
      end
    end

  end

  class StringParser < Runt::Intersect

    def initialize(string)
      super()
      add parsed(string)
    end

    #recursive method to parse input string
    def parse(token)
      case token
      when ""
        REWeek.new(0)
      when /^(.+);(.+)/ # split at semicolons
        parse($1) | parse($2)
      when /(\D+) (\d.+)/ # split days and times
        parse($1) & parse($2)
      when /(\D+) (\D+)/, /(\d+-\d+) (\d+-\d+)/ # split at spaces
        parse($1) | parse($2)
      when /([A-Z][a-z][a-z])-([A-Z][a-z][a-z])/ # create range of days
        REWeek.new(Runt.const_get($1), Runt.const_get($2))
      when /([A-Z][a-z][a-z])/ # create single day
        DIWeek.new(Runt.const_get($1))
      when /(\d\d)(\d\d)-(\d\d)(\d\d)/ #create time range
        start = Time.mktime(2000,1,1,$1.to_i,$2.to_i)
        # 0600-0900 should work like 0600-0859,
        stop = Time.mktime(2000,1,1,$3.to_i,$4.to_i) - 1
        REDay.new(start.hour, start.min, stop.hour, stop.min)
      end
    end
    alias :parsed :parse

  end

end

class TimeWindow < Runt::StringParser
end

···

On 10/21/07, Gordon Thiesfeld <gthiesfeld@gmail.com> wrote:

Here's my solutions. I used Runt for the heavy lifting. I just had
to parse the string and create Runt temporal expressions.