Time Window (#144)

The three rules of Ruby Q.:

  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 Q. by submitting ideas as often as you can:

http://www.rubyquiz.com/

  1. Enjoy!

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

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

by Brian C.

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

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

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

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

by Brian C.

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

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

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 Q. #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 Q. #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

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

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

defined by strings in the following format:
to 08:59:59. An empty time window means “all times everyday”. Here are
w.include?(Time.mktime(2007,9,27,6,59,59)) # Thu assert
w = TimeWindow.new(“Fri-Mon”)
assert w.include?(Time.mktime(2007,9,25,1,2,3)) # all times
end
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 Oct 19, 7:14 am, Ruby Q. [email protected] wrote:

in the following format:
08:59:59. An empty time window means “all times everyday”. Here are some test
assert w.include?(Time.mktime(2007,9,27,7,0,0))
assert ! w.include?(Time.mktime(2007,9,27)) # Thu
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 F…

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 10/19/07, Ruby Q. [email protected] wrote:

    #    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:

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/21/07, Gordon T. [email protected] wrote:

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

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

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