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

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.

--
-yossef